Add support for completion transitions
- Currently into as internal new feature, add state completed concecept and use it in various places. - Main focus for this commit is to add support using anonymous transitions with state do actions which requires proper completion schematics. - Internal functionality here will probably expose to user level in future releases using various other concepts. - Backport #466 - Relates #504
This commit is contained in:
@@ -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<String, String> plan =
|
||||
StateMachineTestPlanBuilder.<String, String>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"))));
|
||||
|
||||
@@ -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<S> {
|
||||
*
|
||||
* @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<S> {
|
||||
*
|
||||
* @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<S> {
|
||||
return result.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OrderedComposite list=" + list.size() + " hash=" + hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<S, E> extends LifecycleObjectSupport implements State<S, E> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(AbstractState.class);
|
||||
|
||||
private final S id;
|
||||
private final PseudoState<S, E> pseudoState;
|
||||
private final Collection<E> deferred;
|
||||
@@ -59,6 +63,7 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
|
||||
private final CompositeStateListener<S, E> stateListener = new CompositeStateListener<S, E>();
|
||||
private final List<ScheduledFuture<?>> cancellableActions = new ArrayList<>();
|
||||
private CompositeActionListener<S, E> actionListener;
|
||||
private final List<StateMachineListener<S, E>> completionListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Instantiates a new abstract state.
|
||||
@@ -196,6 +201,18 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
|
||||
|
||||
@Override
|
||||
public void exit(StateContext<S, E> context) {
|
||||
if (submachine != null) {
|
||||
for (StateMachineListener<S, E> l : completionListeners) {
|
||||
submachine.removeStateListener(l);
|
||||
}
|
||||
} else if (!regions.isEmpty()) {
|
||||
for (Region<S, E> region : regions) {
|
||||
for (StateMachineListener<S, E> l : completionListeners) {
|
||||
region.removeStateListener(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
completionListeners.clear();
|
||||
cancelStateActions();
|
||||
stateListener.onExit(context);
|
||||
disarmTriggers();
|
||||
@@ -203,6 +220,45 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
|
||||
|
||||
@Override
|
||||
public void entry(StateContext<S, E> context) {
|
||||
if (submachine != null) {
|
||||
final StateMachineListener<S, E> l = new StateMachineListenerAdapter<S, E>() {
|
||||
|
||||
@Override
|
||||
public void stateContext(StateContext<S, E> 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<S, E> region : regions) {
|
||||
final StateMachineListener<S, E> l = new StateMachineListenerAdapter<S, E>() {
|
||||
|
||||
@Override
|
||||
public void stateContext(StateContext<S, E> 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<S, E> extends LifecycleObjectSupport impleme
|
||||
* @param context the context
|
||||
*/
|
||||
protected void scheduleStateActions(StateContext<S, E> context) {
|
||||
AtomicInteger completionCount = null;
|
||||
if (isSimple()) {
|
||||
completionCount = new AtomicInteger(stateActions.size());
|
||||
}
|
||||
for (Action<S, E> 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<S, E> 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<S, E> action, final StateContext<S, E> context) {
|
||||
protected ScheduledFuture<?> scheduleAction(final Action<S, E> action, final StateContext<S, E> 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<S, E> 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<S, E> context) {
|
||||
stateListener.onComplete(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred + ", entryActions="
|
||||
|
||||
@@ -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<S, E> extends AbstractCompositeListener<Stat
|
||||
iterator.next().onExit(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(StateContext<S, E> context) {
|
||||
for (Iterator<StateListener<S, E>> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onComplete(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<S, E> extends AbstractPseudoState<S, E> {
|
||||
this.track = new ArrayList<State<S,E>>(joins);
|
||||
for (State<S, E> tt : joins) {
|
||||
final State<S, E> t = tt;
|
||||
t.addStateListener(new StateListener<S, E>() {
|
||||
t.addStateListener(new StateListenerAdapter<S, E>() {
|
||||
|
||||
@Override
|
||||
public void onEntry(StateContext<S, E> 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<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit(StateContext<S, E> context) {
|
||||
if (!notified && track.size() > 0) {
|
||||
public void onComplete(StateContext<S, E> context) {
|
||||
boolean trackSizeZero = false;
|
||||
synchronized (track) {
|
||||
track.remove(t);
|
||||
if (track.size() == 0) {
|
||||
notified = true;
|
||||
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
|
||||
trackSizeZero = true;
|
||||
}
|
||||
}
|
||||
if (!notified && trackSizeZero) {
|
||||
log.debug("Join complete");
|
||||
notified = true;
|
||||
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<S, E> extends AbstractSimpleState<S, E> {
|
||||
|
||||
@Override
|
||||
public void entry(StateContext<S, E> context) {
|
||||
super.entry(context);
|
||||
for (Action<S, E> action : getEntryActions()) {
|
||||
try {
|
||||
executeAction(action, context);
|
||||
@@ -161,6 +160,7 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
|
||||
log.error("Action execution resulted error", e);
|
||||
}
|
||||
}
|
||||
super.entry(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<S, E> {
|
||||
* @param context the state context
|
||||
*/
|
||||
void onExit(StateContext<S, E> context);
|
||||
|
||||
/**
|
||||
* Called when {@link State} want to notify of its completion.
|
||||
*
|
||||
* @param context the state context
|
||||
*/
|
||||
void onComplete(StateContext<S, E> context);
|
||||
}
|
||||
|
||||
@@ -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 <S> the type of state
|
||||
* @param <E> the type of event
|
||||
*/
|
||||
public class StateListenerAdapter<S, E> implements StateListener<S, E> {
|
||||
|
||||
@Override
|
||||
public void onEntry(StateContext<S, E> context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit(StateContext<S, E> context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(StateContext<S, E> context) {
|
||||
}
|
||||
}
|
||||
@@ -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<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
}
|
||||
|
||||
for (State<S, E> state : states) {
|
||||
for (final State<S, E> state : states) {
|
||||
|
||||
state.addStateListener(new StateListenerAdapter<S, E>() {
|
||||
public void onComplete(StateContext<S, E> context) {
|
||||
((AbstractStateMachine<S, E>)getRelayStateMachine()).executeTriggerlessTransitions(AbstractStateMachine.this, context, state);
|
||||
};
|
||||
});
|
||||
|
||||
if (state.isSubmachineState()) {
|
||||
StateMachine<S, E> submachine = ((AbstractState<S, E>)state).getSubmachine();
|
||||
submachine.addStateListener(new StateMachineListenerRelay());
|
||||
@@ -358,10 +366,6 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
}
|
||||
|
||||
protected StateMachineExecutor<S, E> 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<S, E> extends StateMachineObjectSuppo
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
protected void executeTriggerlessTransitions(StateMachine<S, E> stateMachine, StateContext<S, E> stateContext, State<S, E> state) {
|
||||
this.stateMachineExecutor.executeTriggerlessTransitions(stateContext, state);
|
||||
State<S, E> cs = currentState;
|
||||
if (cs != null && cs.isOrthogonal()) {
|
||||
Collection<Region<S, E>> regions = ((AbstractState<S, E>)cs).getRegions();
|
||||
for (Region<S, E> region : regions) {
|
||||
((AbstractStateMachine<S, E>)region).executeTriggerlessTransitions(this, stateContext, state);
|
||||
}
|
||||
} else if (cs != null && cs.isSubmachineState()) {
|
||||
StateMachine<S, E> submachine = ((AbstractState<S, E>)cs).getSubmachine();
|
||||
((AbstractStateMachine<S, E>)submachine).executeTriggerlessTransitions(this, stateContext, state);
|
||||
}
|
||||
}
|
||||
|
||||
protected StateMachineExecutor<S, E> getStateMachineExecutor() {
|
||||
return stateMachineExecutor;
|
||||
}
|
||||
|
||||
protected synchronized boolean acceptEvent(Message<E> message) {
|
||||
if ((currentState != null && currentState.shouldDefer(message))) {
|
||||
log.info("Current state " + currentState + " deferred event " + message);
|
||||
@@ -1012,12 +1034,18 @@ public abstract class AbstractStateMachine<S, E> 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<S, E> 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<S, E> extends StateMachineObjectSuppo
|
||||
State<S, E> 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<S, E> extends StateMachineObjectSuppo
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
log.debug("Trying Enter state=[" + state + "]");
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Trying Enter state=[" + state + "]");
|
||||
}
|
||||
|
||||
@@ -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<S, E> extends LifecycleObjectSupport im
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private final TransitionComparator<S, E> transitionComparator;;
|
||||
private final TransitionComparator<S, E> transitionComparator;
|
||||
|
||||
private final TransitionConflictPolicy transitionConflictPolicy;
|
||||
|
||||
/**
|
||||
* Instantiates a new default state machine executor.
|
||||
@@ -128,6 +131,7 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
|
||||
this.initialTransition = initialTransition;
|
||||
this.initialEvent = initialEvent;
|
||||
this.transitionComparator = new TransitionComparator<S, E>(transitionConflictPolicy);
|
||||
this.transitionConflictPolicy = transitionConflictPolicy;
|
||||
// anonymous transitions are fixed, sort those now
|
||||
this.triggerlessTransitions.sort(transitionComparator);
|
||||
registerTriggerListener();
|
||||
@@ -204,6 +208,10 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
|
||||
private final Set<State<S, E>> joinSyncStates = new HashSet<>();
|
||||
|
||||
private boolean handleTriggerTrans(List<Transition<S, E>> trans, Message<E> queuedMessage) {
|
||||
return handleTriggerTrans(trans, queuedMessage, null);
|
||||
}
|
||||
|
||||
private boolean handleTriggerTrans(List<Transition<S, E>> trans, Message<E> queuedMessage, State<S, E> completion) {
|
||||
boolean transit = false;
|
||||
for (Transition<S, E> t : trans) {
|
||||
if (t == null) {
|
||||
@@ -221,6 +229,16 @@ public class DefaultStateMachineExecutor<S, E> 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<S, E> extends LifecycleObjectSupport im
|
||||
trans.sort(transitionComparator);
|
||||
handleTriggerTrans(trans, queuedMessage);
|
||||
}
|
||||
|
||||
List<Transition<S, E>> transWithGuards = new ArrayList<>();
|
||||
for (Transition<S, E> t : triggerlessTransitions) {
|
||||
if (((AbstractTransition<S, E>)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<S, E> context, State<S, E> state) {
|
||||
if (stateMachine.getState() != null) {
|
||||
handleTriggerTrans(triggerlessTransitions, context.getMessage(), state);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized boolean processDeferList() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<S, E> {
|
||||
*/
|
||||
void queueDeferredEvent(Message<E> message);
|
||||
|
||||
/**
|
||||
* Execute and check all triggerless transitions.
|
||||
*
|
||||
* @param context the state context
|
||||
* @param state the state
|
||||
*/
|
||||
void executeTriggerlessTransitions(StateContext<S, E> context, State<S, E> state);
|
||||
|
||||
/**
|
||||
* Execute {@code StateMachineExecutor} logic.
|
||||
*/
|
||||
|
||||
@@ -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<S, E> implements Transition<S, E> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Guard<S, E> getGuard() {
|
||||
return guard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionKind getKind() {
|
||||
return kind;
|
||||
|
||||
@@ -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<S, E> {
|
||||
*/
|
||||
State<S,E> getTarget();
|
||||
|
||||
/**
|
||||
* Gets the guard of this transition.
|
||||
*
|
||||
* @return the guard
|
||||
*/
|
||||
Guard<S, E> getGuard();
|
||||
|
||||
/**
|
||||
* Gets the transition actions.
|
||||
*
|
||||
|
||||
@@ -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<TestStates4, TestEvents> {
|
||||
|
||||
public volatile CountDownLatch stateChangedLatch = new CountDownLatch(6);
|
||||
public volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public void stateChanged(State<TestStates4, TestEvents> from, State<TestStates4, TestEvents> to) {
|
||||
stateChangedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateMachineStarted(StateMachine<TestStates4, TestEvents> stateMachine) {
|
||||
stateMachineStartedLatch.countDown();
|
||||
}
|
||||
|
||||
public void reset(int c1, int c2) {
|
||||
stateChangedLatch = new CountDownLatch(c1);
|
||||
stateMachineStartedLatch = new CountDownLatch(c2);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String,String> 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<String,String> 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<String,String> 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<String,String> 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<String,String> 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<String,String> 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<String,String> 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<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("S1")
|
||||
.stateDo("S2", testAction2())
|
||||
.state("S3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> 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<String, String> 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<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("S1")
|
||||
.state("S2")
|
||||
.state("S3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source("S1")
|
||||
.target("S2")
|
||||
.event("E1")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2")
|
||||
.target("S3");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config3 extends StateMachineConfigurerAdapter<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
|
||||
|
||||
int count = 0;
|
||||
StateContext<String, String> context;
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
public TestCountAction() {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(StateContext<String, String> context) {
|
||||
this.context = context;
|
||||
count++;
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<TestStates4,TestEvents> 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<TestStates4,TestEvents> 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<TestStates4,TestEvents> 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<TestStates, TestEvents> {
|
||||
@@ -496,4 +565,131 @@ public class EndStateTests extends AbstractStateMachineTests {
|
||||
.event(TestEvents.E2);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config8 extends EnumStateMachineConfigurerAdapter<TestStates4, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> 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<TestStates4, TestEvents> 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<TestStates4, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> 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<TestStates4, TestEvents> 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<TestStates4, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> 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<TestStates4, TestEvents> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, SpelEvents>(SpelStates.S2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Guard<SpelStates, SpelEvents> getGuard() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Action<SpelStates, SpelEvents>> getActions() {
|
||||
return null;
|
||||
|
||||
@@ -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<TestStates, TestEvents> context) {
|
||||
log.info("XXX11");
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
testHeader = context.getMessageHeaders().get("testHeader", String.class);
|
||||
log.info("XXX12");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<States,Events> 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<Object, Object> 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<Object, Object> 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);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %t %m%n</pattern>
|
||||
<charset>utf8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
<logger name="org.springframework.statemachine" level="DEBUG"/>
|
||||
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user