diff --git a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/LinkedRegionsTests.java b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/LinkedRegionsTests.java index bb4f734f..b595de73 100644 --- a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/LinkedRegionsTests.java +++ b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/LinkedRegionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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,7 +51,7 @@ public class LinkedRegionsTests extends AbstractBuildTests { StateMachineTestPlan plan = StateMachineTestPlanBuilder.builder() .stateMachine(stateMachine) - .step().expectStateChanged(19).expectStates("S3").and() + .step().expectStateChanged(15).expectStates("S3").and() .build(); plan.test(); assertThat(listener.statesEntered, not(hasItem(startsWith("JOIN")))); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/listener/OrderedComposite.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/listener/OrderedComposite.java index 2e7bfa37..6791025f 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/listener/OrderedComposite.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/listener/OrderedComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -66,7 +66,7 @@ public class OrderedComposite { * * @param item item */ - public void add(S item) { + public synchronized void add(S item) { if (item instanceof Ordered) { if (!ordered.contains(item)) { ordered.add(item); @@ -89,7 +89,7 @@ public class OrderedComposite { * * @param item item */ - public void remove(S item) { + public synchronized void remove(S item) { ordered.remove(item); unordered.remove(item); Collections.sort(ordered, comparator); @@ -120,4 +120,8 @@ public class OrderedComposite { return result.iterator(); } + @Override + public String toString() { + return "OrderedComposite list=" + list.size() + " hash=" + hashCode(); + } } 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 e29b2485..98ab9797 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 @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2018 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. @@ -20,17 +20,22 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.messaging.Message; import org.springframework.scheduling.TaskScheduler; import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateContext.Stage; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.ActionListener; import org.springframework.statemachine.action.CompositeActionListener; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.support.LifecycleObjectSupport; import org.springframework.statemachine.trigger.Trigger; @@ -46,7 +51,6 @@ import org.springframework.statemachine.trigger.Trigger; public abstract class AbstractState extends LifecycleObjectSupport implements State { private static final Log log = LogFactory.getLog(AbstractState.class); - private final S id; private final PseudoState pseudoState; private final Collection deferred; @@ -59,6 +63,7 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme private final CompositeStateListener stateListener = new CompositeStateListener(); private final List> cancellableActions = new ArrayList<>(); private CompositeActionListener actionListener; + private final List> completionListeners = new CopyOnWriteArrayList<>(); /** * Instantiates a new abstract state. @@ -196,6 +201,18 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme @Override public void exit(StateContext context) { + if (submachine != null) { + for (StateMachineListener l : completionListeners) { + submachine.removeStateListener(l); + } + } else if (!regions.isEmpty()) { + for (Region region : regions) { + for (StateMachineListener l : completionListeners) { + region.removeStateListener(l); + } + } + } + completionListeners.clear(); cancelStateActions(); stateListener.onExit(context); disarmTriggers(); @@ -203,6 +220,45 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme @Override public void entry(StateContext context) { + if (submachine != null) { + final StateMachineListener l = new StateMachineListenerAdapter() { + + @Override + public void stateContext(StateContext stateContext) { + if (stateContext.getStage() == Stage.STATEMACHINE_STOP) { + if (stateContext.getStateMachine() == submachine && submachine.isComplete()) { + completionListeners.remove(this); + submachine.removeStateListener(this); + if (completionListeners.isEmpty()) { + notifyStateOnComplete(stateContext); + } + } + } + } + }; + submachine.addStateListener(l); + } else if (!regions.isEmpty()) { + for (final Region region : regions) { + final StateMachineListener l = new StateMachineListenerAdapter() { + + @Override + public void stateContext(StateContext stateContext) { + if (stateContext.getStage() == Stage.STATEMACHINE_STOP) { + if (stateContext.getStateMachine() == region && region.isComplete()) { + completionListeners.remove(this); + region.removeStateListener(this); + if (completionListeners.isEmpty()) { + notifyStateOnComplete(stateContext); + } + } + } + } + }; + completionListeners.add(l); + region.addStateListener(l); + } + } + stateListener.onEntry(context); armTriggers(); scheduleStateActions(context); @@ -378,12 +434,19 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme * @param context the context */ protected void scheduleStateActions(StateContext context) { + AtomicInteger completionCount = null; + if (isSimple()) { + completionCount = new AtomicInteger(stateActions.size()); + } for (Action action : stateActions) { - ScheduledFuture future = scheduleAction(action, context); + ScheduledFuture future = scheduleAction(action, context, completionCount); if (future != null) { cancellableActions.add(future); } } + if (isSimple() && stateActions.size() == 0) { + notifyStateOnComplete(context); + } } /** @@ -409,9 +472,10 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme * * @param action the action * @param context the context + * @param completionCount the completion count tracker * @return the scheduled future */ - protected ScheduledFuture scheduleAction(final Action action, final StateContext context) { + protected ScheduledFuture scheduleAction(final Action action, final StateContext context, final AtomicInteger completionCount) { TaskScheduler taskScheduler = getTaskScheduler(); if (taskScheduler == null) { log.error("Unable to schedule action as taskSchedule is not set, action=[" + action + "]"); @@ -422,11 +486,18 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme @Override public void run() { executeAction(action, context); + if (completionCount != null && completionCount.decrementAndGet() <= 0) { + notifyStateOnComplete(context); + } } }, new Date()); return future; } + protected void notifyStateOnComplete(StateContext context) { + stateListener.onComplete(context); + } + @Override public String toString() { return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred + ", entryActions=" diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositeStateListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositeStateListener.java index 4a739303..3371acd0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositeStateListener.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositeStateListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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. @@ -44,4 +44,11 @@ public class CompositeStateListener extends AbstractCompositeListener context) { + for (Iterator> iterator = getListeners().reverse(); iterator.hasNext();) { + iterator.next().onComplete(context); + } + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java index db384c20..9a695396 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -24,7 +24,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.state.PseudoStateContext.PseudoAction; -import org.springframework.statemachine.support.StateMachineUtils; import org.springframework.util.Assert; /** @@ -112,31 +111,22 @@ public class JoinPseudoState extends AbstractPseudoState { this.track = new ArrayList>(joins); for (State tt : joins) { final State t = tt; - t.addStateListener(new StateListener() { + t.addStateListener(new StateListenerAdapter() { @Override - public void onEntry(StateContext context) { - if (context.getTransition() != null && StateMachineUtils - .isPseudoState(context.getTransition().getTarget(), PseudoStateKind.END)) { - if (!notified && track.size() > 0) { - track.remove(t); - if (track.size() == 0) { - notified = true; - notifyContext(new DefaultPseudoStateContext(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED)); - } - } - } - } - - @Override - public void onExit(StateContext context) { - if (!notified && track.size() > 0) { + public void onComplete(StateContext context) { + boolean trackSizeZero = false; + synchronized (track) { track.remove(t); if (track.size() == 0) { - notified = true; - notifyContext(new DefaultPseudoStateContext(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED)); + trackSizeZero = true; } } + if (!notified && trackSizeZero) { + log.debug("Join complete"); + notified = true; + notifyContext(new DefaultPseudoStateContext(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED)); + } } }); } 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 c314b7aa..6f48923c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -153,7 +153,6 @@ public class ObjectState extends AbstractSimpleState { @Override public void entry(StateContext context) { - super.entry(context); for (Action action : getEntryActions()) { try { executeAction(action, context); @@ -161,6 +160,7 @@ public class ObjectState extends AbstractSimpleState { log.error("Action execution resulted error", e); } } + super.entry(context); } @Override diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java index 01ced84a..1faf73fa 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2018 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. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListener.java index 48c6c166..597e3ad5 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListener.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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. @@ -40,4 +40,11 @@ public interface StateListener { * @param context the state context */ void onExit(StateContext context); + + /** + * Called when {@link State} want to notify of its completion. + * + * @param context the state context + */ + void onComplete(StateContext context); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListenerAdapter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListenerAdapter.java new file mode 100644 index 00000000..16a674e3 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateListenerAdapter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 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 org.springframework.statemachine.StateContext; + +/** + * Adapter implementation of {@link StateListener} implementing all + * methods which extended implementation can override. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class StateListenerAdapter implements StateListener { + + @Override + public void onEntry(StateContext context) { + } + + @Override + public void onExit(StateContext context) { + } + + @Override + public void onComplete(StateContext context) { + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 7072abe1..6534d2dc 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2015-2018 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. @@ -49,6 +49,7 @@ import org.springframework.statemachine.state.PseudoStateContext; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.PseudoStateListener; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.state.StateListenerAdapter; import org.springframework.statemachine.support.StateMachineExecutor.StateMachineExecutorTransit; import org.springframework.statemachine.transition.InitialTransition; import org.springframework.statemachine.transition.Transition; @@ -260,7 +261,14 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } - for (State state : states) { + for (final State state : states) { + + state.addStateListener(new StateListenerAdapter() { + public void onComplete(StateContext context) { + ((AbstractStateMachine)getRelayStateMachine()).executeTriggerlessTransitions(AbstractStateMachine.this, context, state); + }; + }); + if (state.isSubmachineState()) { StateMachine submachine = ((AbstractState)state).getSubmachine(); submachine.addStateListener(new StateMachineListenerRelay()); @@ -358,10 +366,6 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } - protected StateMachineExecutor getStateMachineExecutor() { - return stateMachineExecutor; - } - @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { // last change to set factory because this maybe be called per @@ -801,6 +805,24 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo this.id = id; } + protected void executeTriggerlessTransitions(StateMachine stateMachine, StateContext stateContext, State state) { + this.stateMachineExecutor.executeTriggerlessTransitions(stateContext, state); + State cs = currentState; + if (cs != null && cs.isOrthogonal()) { + Collection> regions = ((AbstractState)cs).getRegions(); + for (Region region : regions) { + ((AbstractStateMachine)region).executeTriggerlessTransitions(this, stateContext, state); + } + } else if (cs != null && cs.isSubmachineState()) { + StateMachine submachine = ((AbstractState)cs).getSubmachine(); + ((AbstractStateMachine)submachine).executeTriggerlessTransitions(this, stateContext, state); + } + } + + protected StateMachineExecutor getStateMachineExecutor() { + return stateMachineExecutor; + } + protected synchronized boolean acceptEvent(Message message) { if ((currentState != null && currentState.shouldDefer(message))) { log.info("Current state " + currentState + " deferred event " + message); @@ -1012,12 +1034,18 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (states.contains(state)) { if (exit) { - exitCurrentState(state, message, transition, stateMachine, sources, targets); + try { + exitCurrentState(state, message, transition, stateMachine, sources, targets); + } catch (Throwable t) { + log.error("Error calling exitCurrentState", t); + } } State notifyFrom = currentState; currentState = state; entryToState(state, message, transition, stateMachine); - notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, state)); + if (!StateMachineUtils.isPseudoState(state, PseudoStateKind.JOIN)) { + notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, state)); + } nonDeepStatePresent = true; if (!isRunning() && !isComplete()) { start(); @@ -1029,7 +1057,9 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo State notifyFrom = currentState; currentState = findDeep; entryToState(findDeep, message, transition, stateMachine); - notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, findDeep)); + if (!StateMachineUtils.isPseudoState(state, PseudoStateKind.JOIN)) { + notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, findDeep)); + } if (!isRunning() && !isComplete()) { start(); } @@ -1214,6 +1244,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (state == null) { return; } + log.debug("Trying Enter state=[" + state + "]"); if (log.isTraceEnabled()) { log.trace("Trying Enter state=[" + state + "]"); } 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 4073d352..7b6f12bd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -46,6 +46,7 @@ import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.state.JoinPseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.AbstractTransition; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionConflictPolicy; import org.springframework.statemachine.trigger.DefaultTriggerContext; @@ -102,7 +103,9 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im private final ReentrantLock lock = new ReentrantLock(); - private final TransitionComparator transitionComparator;; + private final TransitionComparator transitionComparator; + + private final TransitionConflictPolicy transitionConflictPolicy; /** * Instantiates a new default state machine executor. @@ -128,6 +131,7 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im this.initialTransition = initialTransition; this.initialEvent = initialEvent; this.transitionComparator = new TransitionComparator(transitionConflictPolicy); + this.transitionConflictPolicy = transitionConflictPolicy; // anonymous transitions are fixed, sort those now this.triggerlessTransitions.sort(transitionComparator); registerTriggerListener(); @@ -204,6 +208,10 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im private final Set> joinSyncStates = new HashSet<>(); private boolean handleTriggerTrans(List> trans, Message queuedMessage) { + return handleTriggerTrans(trans, queuedMessage, null); + } + + private boolean handleTriggerTrans(List> trans, Message queuedMessage, State completion) { boolean transit = false; for (Transition t : trans) { if (t == null) { @@ -221,6 +229,16 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im continue; } + if (transitionConflictPolicy != TransitionConflictPolicy.PARENT && completion != null && !source.getId().equals(completion.getId())) { + if (source.isOrthogonal()) { + continue; + } + else if (!StateMachineUtils.isSubstate(source, completion)) { + continue; + + } + } + // special handling of join if (StateMachineUtils.isPseudoState(t.getTarget(), PseudoStateKind.JOIN)) { if (joinSyncStates.isEmpty()) { @@ -429,14 +447,30 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im trans.sort(transitionComparator); handleTriggerTrans(trans, queuedMessage); } + + List> transWithGuards = new ArrayList<>(); + for (Transition t : triggerlessTransitions) { + if (((AbstractTransition)t).getGuard() != null) { + transWithGuards.add(t); + } + } + if (stateMachine.getState() != null) { // loop triggerless transitions here so that // all "chained" transitions will get queue message boolean transit = false; do { - transit = handleTriggerTrans(triggerlessTransitions, queuedMessage); + transit = handleTriggerTrans(transWithGuards, queuedMessage); } while (transit); } + + } + + @Override + public void executeTriggerlessTransitions(StateContext context, State state) { + if (stateMachine.getState() != null) { + handleTriggerTrans(triggerlessTransitions, context.getMessage(), state); + } } private synchronized boolean processDeferList() { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java index 831fc1fe..98adc437 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/LifecycleObjectSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -133,7 +133,12 @@ public abstract class LifecycleObjectSupport implements InitializingBean, Dispos @Override public final void stop() { - this.lifecycleLock.lock(); + if (!this.lifecycleLock.tryLock()) { + if (log.isDebugEnabled()) { + log.debug("already stopping " + this); + } + return; + } try { if (this.running) { this.doStop(); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java index e39560fe..01fef1c9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -21,6 +21,7 @@ import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.access.StateMachineAccess; +import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.trigger.Trigger; @@ -56,6 +57,14 @@ public interface StateMachineExecutor { */ void queueDeferredEvent(Message message); + /** + * Execute and check all triggerless transitions. + * + * @param context the state context + * @param state the state + */ + void executeTriggerlessTransitions(StateContext context, State state); + /** * Execute {@code StateMachineExecutor} logic. */ diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java index 157bb649..e449b683 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -115,6 +115,11 @@ public abstract class AbstractTransition implements Transition { return true; } + @Override + public Guard getGuard() { + return guard; + } + @Override public TransitionKind getKind() { return kind; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java index f46af055..21370ebc 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -18,6 +18,7 @@ package org.springframework.statemachine.transition; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.ActionListener; +import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.state.State; import org.springframework.statemachine.trigger.Trigger; @@ -64,6 +65,13 @@ public interface Transition { */ State getTarget(); + /** + * Gets the guard of this transition. + * + * @return the guard + */ + Guard getGuard(); + /** * Gets the transition actions. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index 9c9467c7..57895c21 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -89,6 +89,11 @@ public abstract class AbstractStateMachineTests { TASKS, T1, T1E, T2, T2E, T3, T3E } + public static enum TestStates4 { + READY, DONE, + TASKS, T1, T1E, T2, T2E + } + public static enum TestEvents2 { PLAY, STOP, PAUSE, EJECT, LOAD } @@ -273,4 +278,25 @@ public abstract class AbstractStateMachineTests { } + public static class TestStateMachineListener4 extends StateMachineListenerAdapter { + + public volatile CountDownLatch stateChangedLatch = new CountDownLatch(6); + public volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + public void reset(int c1, int c2) { + stateChangedLatch = new CountDownLatch(c1); + stateMachineStartedLatch = new CountDownLatch(c2); + } + + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/CompletionEventTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/CompletionEventTests.java new file mode 100644 index 00000000..225c9c88 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/CompletionEventTests.java @@ -0,0 +1,419 @@ +/* + * Copyright 2018 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 static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +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.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +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 CompletionEventTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSimpleStateWithStateActionCompletes() throws Exception { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class); + + machine.start(); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + + assertThat(testAction2.latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testAction2.count, is(1)); + Thread.sleep(1000); + assertThat(machine.getState().getId(), is("S3")); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSimpleStateWithStateActionCompletesThreading() throws Exception { + context.register(Config1.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class); + + machine.start(); + Thread.sleep(1000); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + + assertThat(testAction2.latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testAction2.count, is(1)); + Thread.sleep(1000); + assertThat(machine.getState().getId(), is("S3")); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSimpleStateWithoutStateActionCompletes() throws Exception { + context.register(Config2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + machine.start(); + assertThat(machine.getState().getId(), is("S1")); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + assertThat(machine.getState().getId(), is("S3")); + } + + public void testSubmachineWithStateActionCompletes() throws Exception { + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSubmachineWithoutStateActionCompletes() throws Exception { + context.register(Config3.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + machine.start(); + assertThat(machine.getState().getId(), is("S1")); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + assertThat(machine.getState().getId(), is("S3")); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSubmachineWithoutStateActionCompletes2() throws Exception { + context.register(Config5.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + machine.start(); + assertThat(machine.getState().getId(), is("S1")); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + assertThat(machine.getState().getId(), is("S3")); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testSubmachineWithoutStateActionCompletesThreading() throws Exception { + context.register(Config3.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + machine.start(); + Thread.sleep(200); + assertThat(machine.getState().getId(), is("S1")); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + Thread.sleep(200); + assertThat(machine.getState().getId(), is("S3")); + } + + public void testRegionWithStateActionCompletes() throws Exception { + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testRegionWithoutStateActionCompletes() throws Exception { + context.register(Config4.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + + machine.start(); + assertThat(machine.getState().getId(), is("S1")); + + machine.sendEvent(MessageBuilder.withPayload("E1").build()); + assertThat(machine.getState().getId(), is("S3")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .stateDo("S2", testAction2()) + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3"); + } + + @Bean + public TestCountAction testAction2() { + return new TestCountAction() { + @Override + public void execute(StateContext context) { + for (int i = 0; i < 10; i++) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + super.execute(context); + } + }; + } + } + + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3"); + } + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3") + .and() + .withStates() + .parent("S2") + .initial("S21") + .state("S22") + .end("S23"); + + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S21") + .target("S22") + .and() + .withExternal() + .source("S22") + .target("S23") + .and() + .withExternal() + .source("S2") + .target("S3"); + } + } + + @Configuration + @EnableStateMachine + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3") + .and() + .withStates() + .parent("S2") + .initial("S201") + .state("S202") + .end("S203") + .and() + .withStates() + .parent("S2") + .initial("S211") + .state("S212") + .end("S213"); + + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S201") + .target("S202") + .and() + .withExternal() + .source("S202") + .target("S203") + .and() + .withExternal() + .source("S211") + .target("S212") + .and() + .withExternal() + .source("S212") + .target("S213") + .and() + .withExternal() + .source("S2") + .target("S3"); + } + } + + @Configuration + @EnableStateMachine + static class Config5 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .state("S3") + .and() + .withStates() + .parent("S2") + .initial("S21") + .state("S22") + .end("S23") + .and() + .withStates() + .parent("S22") + .initial("S221") + .state("S222") + .end("S223"); + + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S21") + .target("S22") + .and() + .withExternal() + .source("S22") + .target("S23") + .and() + .withExternal() + .source("S221") + .target("S222") + .and() + .withExternal() + .source("S222") + .target("S223") + .and() + .withExternal() + .source("S2") + .target("S3"); + } + } + + private static class TestCountAction implements Action { + + int count = 0; + StateContext context; + CountDownLatch latch = new CountDownLatch(1); + + public TestCountAction() { + count = 0; + } + + @Override + public void execute(StateContext context) { + this.context = context; + count++; + latch.countDown(); + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java index fa0a776e..f0c2d105 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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,12 +16,14 @@ package org.springframework.statemachine.state; 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.EnumSet; +import java.util.concurrent.TimeUnit; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -165,6 +167,73 @@ public class EndStateTests extends AbstractStateMachineTests { assertThat(machine.getState().getIds(), contains(TestStates.S2)); } + @Test + public void testEndStatesWithRegionsCompletionCompletes() throws InterruptedException { + context.register(Config8.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + TestStateMachineListener4 listener = new TestStateMachineListener4(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates4.DONE)); + } + + @Test + public void testEndStatesWithSubmachineCompletionCompletes() throws InterruptedException { + context.register(Config9.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + TestStateMachineListener4 listener = new TestStateMachineListener4(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates4.DONE)); + } + + @Test + public void testEndStatesWithRegionsCompletionCompletes2() throws InterruptedException { + context.register(Config10.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + TestStateMachineListener4 listener = new TestStateMachineListener4(); + machine.addStateListener(listener); + listener.reset(1, 1); + + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates4.READY)); + + listener.reset(3, 0); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates4.TASKS, TestStates4.T1, TestStates4.T2)); + + listener.reset(1, 0); + machine.sendEvent(TestEvents.E2); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates4.TASKS, TestStates4.T1E, TestStates4.T2)); + + listener.reset(2, 0); + machine.sendEvent(TestEvents.E3); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates4.DONE)); + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -496,4 +565,131 @@ public class EndStateTests extends AbstractStateMachineTests { .event(TestEvents.E2); } } + + @Configuration + @EnableStateMachine + static class Config8 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(TestStates4.READY) + .state(TestStates4.TASKS) + .state(TestStates4.DONE) + .and() + .withStates() + .parent(TestStates4.TASKS) + .initial(TestStates4.T1) + .end(TestStates4.T1E) + .and() + .withStates() + .parent(TestStates4.TASKS) + .initial(TestStates4.T2) + .end(TestStates4.T2E); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(TestStates4.READY).target(TestStates4.TASKS) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates4.T1).target(TestStates4.T1E) + .and() + .withExternal() + .source(TestStates4.T2).target(TestStates4.T2E) + .and() + .withExternal() + .source(TestStates4.TASKS).target(TestStates4.DONE); + } + + } + + @Configuration + @EnableStateMachine + static class Config9 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(TestStates4.READY) + .state(TestStates4.TASKS) + .state(TestStates4.DONE) + .and() + .withStates() + .parent(TestStates4.TASKS) + .initial(TestStates4.T1) + .end(TestStates4.T1E); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(TestStates4.READY).target(TestStates4.TASKS) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates4.T1).target(TestStates4.T1E) + .and() + .withExternal() + .source(TestStates4.TASKS).target(TestStates4.DONE); + } + + } + + @Configuration + @EnableStateMachine + static class Config10 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(TestStates4.READY) + .state(TestStates4.TASKS) + .state(TestStates4.DONE) + .and() + .withStates() + .parent(TestStates4.TASKS) + .initial(TestStates4.T1) + .end(TestStates4.T1E) + .and() + .withStates() + .parent(TestStates4.TASKS) + .initial(TestStates4.T2) + .end(TestStates4.T2E); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(TestStates4.READY).target(TestStates4.TASKS) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates4.T1).target(TestStates4.T1E) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates4.T2).target(TestStates4.T2E) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates4.TASKS).target(TestStates4.DONE); + } + + } + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java index d63de650..655acf41 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java index 0c142958..db299efd 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -37,6 +37,7 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.access.StateMachineAccessor; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.ActionListener; +import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.state.EnumState; @@ -118,6 +119,11 @@ public class StateContextExpressionMethodsTests { return new EnumState(SpelStates.S2); } + @Override + public Guard getGuard() { + return null; + } + @Override public Collection> getActions() { return null; diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java index db92e4ba..147651c7 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -348,7 +348,7 @@ public class TransitionTests extends AbstractStateMachineTests { listener.reset(3); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("testHeader", "testValue").build()); assertThat(testAction1.latch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(listener.s20Latch.getCount(), is(1L)); + assertThat(listener.s20Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(3)); @@ -832,13 +832,11 @@ public class TransitionTests extends AbstractStateMachineTests { @Override public void execute(StateContext context) { - log.info("XXX11"); try { Thread.sleep(500); } catch (InterruptedException e) { } testHeader = context.getMessageHeaders().get("testHeader", String.class); - log.info("XXX12"); latch.countDown(); } diff --git a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java index 5ddb22f6..aea46909 100644 --- a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java +++ b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -25,6 +25,8 @@ import java.util.Map; 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.After; import org.junit.Before; import org.junit.Test; @@ -38,6 +40,7 @@ import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; +import org.springframework.util.StringUtils; import demo.CommonConfiguration; import demo.tasks.Application.Events; @@ -45,6 +48,8 @@ import demo.tasks.Application.States; public class TasksTests { + private final static Log log = LogFactory.getLog(TasksTests.class); + private AnnotationConfigApplicationContext context; private StateMachine machine; @@ -61,9 +66,9 @@ public class TasksTests { @Test public void testRunOnce() throws InterruptedException { - listener.reset(8, 0, 0); + listener.reset(8, 8, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); Map variables = machine.getExtendedState().getVariables(); assertThat(variables.size(), is(3)); @@ -71,23 +76,38 @@ public class TasksTests { @Test public void testRunTwice() throws InterruptedException { - listener.reset(8, 0, 0); + listener.reset(8, 8, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); Map variables = machine.getExtendedState().getVariables(); assertThat(variables.size(), is(3)); - listener.reset(8, 0, 0); + listener.reset(8, 8, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); variables = machine.getExtendedState().getVariables(); assertThat(variables.size(), is(3)); } + @Test + public void testRunSmoke() throws InterruptedException { + for (int i = 0; i < 20; i++) { + log.info("testRunSmoke SMOKE START " + i); + listener.reset(8, 8, 0); + tasks.run(); + + boolean await = listener.stateEnteredLatch.await(8, TimeUnit.SECONDS); + String reason = "Machine was " + machine + " " + StringUtils.collectionToCommaDelimitedString(listener.statesEntered); + assertThat(reason , await, is(true)); + assertThat(machine.getState().getIds(), contains(States.READY)); + log.info("testRunSmoke SMOKE STOP " + i); + } + } + @Test public void testFailAutomaticFix() throws InterruptedException { listener.reset(10, 0, 0); diff --git a/spring-statemachine-samples/tasks/src/test/resources/logback.xml b/spring-statemachine-samples/tasks/src/test/resources/logback.xml new file mode 100644 index 00000000..cb6447aa --- /dev/null +++ b/spring-statemachine-samples/tasks/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss} %t %m%n + utf8 + + + + + + + + + +