Support fork/join with regions

- Adding first changes for supporting fork and join
  concepts with regions.
- Change of some pseudostate concepts to have
  better model for this and future work.
- Fixes #39
This commit is contained in:
Janne Valkealahti
2015-05-05 19:58:31 +01:00
parent a26f4f08d6
commit 1186ae4a71
29 changed files with 1191 additions and 54 deletions

View File

@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import org.springframework.beans.factory.BeanFactory;
@@ -33,15 +34,17 @@ import org.springframework.statemachine.config.builders.StateMachineTransitions.
import org.springframework.statemachine.config.builders.StateMachineTransitions.TransitionData;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.state.ChoicePseudoState;
import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData;
import org.springframework.statemachine.state.DefaultPseudoState;
import org.springframework.statemachine.state.EnumState;
import org.springframework.statemachine.state.ForkPseudoState;
import org.springframework.statemachine.state.HistoryPseudoState;
import org.springframework.statemachine.state.JoinPseudoState;
import org.springframework.statemachine.state.PseudoState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.RegionState;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.state.StateMachineState;
import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.LifecycleObjectSupport;
import org.springframework.statemachine.support.tree.Tree;
@@ -152,6 +155,20 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL));
if (stateData != null) {
stateMap.put(stateData.getState(), rstate);
} else {
// TODO: don't like that we create a last machine here
Collection<State<S, E>> states = new ArrayList<State<S, E>>();
states.add(rstate);
EnumStateMachine<S, E> m = new EnumStateMachine<S, E>(states, new ArrayList<Transition<S, E>>(), rstate,
null, null, defaultExtendedState);
if (contextEvents != null) {
m.setContextEventsEnabled(contextEvents);
}
if (getBeanFactory() != null) {
m.setBeanFactory(getBeanFactory());
}
m.afterPropertiesSet();
machine = m;
}
} else {
machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory(),
@@ -306,6 +323,10 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
} else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_DEEP) {
pseudoState = new HistoryPseudoState<S, E>(PseudoStateKind.HISTORY_DEEP);
historyState = pseudoState;
} else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) {
continue;
} else if (stateData.getPseudoStateKind() == PseudoStateKind.FORK) {
continue;
} else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) {
continue;
}
@@ -334,6 +355,41 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
stateData.getEntryActions(), stateData.getExitActions(), pseudoState);
states.add(state);
stateMap.put(stateData.getState(), state);
} else if (stateData.getPseudoStateKind() == PseudoStateKind.FORK) {
S s = stateData.getState();
List<S> list = stateMachineTransitions.getForks().get(s);
List<State<S, E>> forks = new ArrayList<State<S,E>>();
for (S fs : list) {
forks.add(stateMap.get(fs));
}
PseudoState<S, E> pseudoState = new ForkPseudoState<S, E>(forks);
state = new EnumState<S, E>(stateData.getState(), stateData.getDeferred(),
stateData.getEntryActions(), stateData.getExitActions(), pseudoState);
states.add(state);
stateMap.put(stateData.getState(), state);
} else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) {
S s = stateData.getState();
List<S> list = stateMachineTransitions.getJoins().get(s);
List<State<S, E>> joins = new ArrayList<State<S,E>>();
for (S fs : list) {
joins.add(stateMap.get(fs));
}
JoinPseudoState<S, E> pseudoState = new JoinPseudoState<S, E>(joins);
state = new EnumState<S, E>(stateData.getState(), stateData.getDeferred(),
stateData.getEntryActions(), stateData.getExitActions(), pseudoState);
states.add(state);
stateMap.put(stateData.getState(), state);
// find joins sources and associate
for (Entry<S, State<S, E>> e : stateMap.entrySet()) {
State<S, E> value = e.getValue();
if (value.isOrthogonal()) {
Collection<State<S, E>> states2 = value.getStates();
if (states2.containsAll(joins)) {
((RegionState<S, E>)value).setJoin(pseudoState);
}
}
}
}
}

View File

@@ -30,10 +30,14 @@ import org.springframework.statemachine.config.common.annotation.ObjectPostProce
import org.springframework.statemachine.config.configurers.ChoiceTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultChoiceTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultForkTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultInternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultJoinTransitionConfigurer;
import org.springframework.statemachine.config.configurers.DefaultLocalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ForkTransitionConfigurer;
import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer;
import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.transition.TransitionKind;
@@ -53,6 +57,8 @@ public class StateMachineTransitionBuilder<S, E>
private final Collection<TransitionData<S, E>> transitionData = new ArrayList<TransitionData<S, E>>();
private final Map<S, List<ChoiceData<S, E>>> choices = new HashMap<S, List<ChoiceData<S, E>>>();
private final Map<S, List<S>> forks = new HashMap<S, List<S>>();
private final Map<S, List<S>> joins = new HashMap<S, List<S>>();
public StateMachineTransitionBuilder() {
super();
@@ -69,7 +75,7 @@ public class StateMachineTransitionBuilder<S, E>
@Override
protected StateMachineTransitions<S, E> performBuild() throws Exception {
return new StateMachineTransitions<S, E>(transitionData, choices);
return new StateMachineTransitions<S, E>(transitionData, choices, forks, joins);
}
@Override
@@ -92,6 +98,16 @@ public class StateMachineTransitionBuilder<S, E>
return apply(new DefaultChoiceTransitionConfigurer<S, E>());
}
@Override
public ForkTransitionConfigurer<S, E> withFork() throws Exception {
return apply(new DefaultForkTransitionConfigurer<S, E>());
}
@Override
public JoinTransitionConfigurer<S, E> withJoin() throws Exception {
return apply(new DefaultJoinTransitionConfigurer<S, E>());
}
public void add(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions,
Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, actions, guard, kind));
@@ -101,4 +117,12 @@ public class StateMachineTransitionBuilder<S, E>
this.choices.put(source, choices);
}
public void addFork(S source, List<S> targets) {
this.forks.put(source, targets);
}
public void addJoin(S target, List<S> sources) {
this.joins.put(target, sources);
}
}

View File

@@ -17,7 +17,9 @@ package org.springframework.statemachine.config.builders;
import org.springframework.statemachine.config.configurers.ChoiceTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ForkTransitionConfigurer;
import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer;
import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer;
/**
@@ -65,9 +67,25 @@ public interface StateMachineTransitionConfigurer<S, E> {
/**
* Gets a configurer for transition from a choice pseudostate.
*
* @return {@link LocalTransitionConfigurer} for chaining
* @return {@link ChoiceTransitionConfigurer} for chaining
* @throws Exception if configuration error happens
*/
ChoiceTransitionConfigurer<S, E> withChoice() throws Exception;
/**
* Gets a configurer for transition from a fork pseudostate.
*
* @return {@link ForkTransitionConfigurer} for chaining
* @throws Exception if configuration error happens
*/
ForkTransitionConfigurer<S, E> withFork() throws Exception;
/**
* Gets a configurer for transition from a join pseudostate.
*
* @return {@link JoinTransitionConfigurer} for chaining
* @throws Exception if configuration error happens
*/
JoinTransitionConfigurer<S, E> withJoin() throws Exception;
}

View File

@@ -35,10 +35,15 @@ public class StateMachineTransitions<S, E> {
private Collection<TransitionData<S, E>> transitions;
private Map<S, List<ChoiceData<S, E>>> choices;
private Map<S, List<S>> forks;
private Map<S, List<S>> joins;
public StateMachineTransitions(Collection<TransitionData<S, E>> transitions, Map<S, List<ChoiceData<S, E>>> choices) {
public StateMachineTransitions(Collection<TransitionData<S, E>> transitions,
Map<S, List<ChoiceData<S, E>>> choices, Map<S, List<S>> forks, Map<S, List<S>> joins) {
this.transitions = transitions;
this.choices = choices;
this.forks = forks;
this.joins = joins;
}
public Collection<TransitionData<S, E>> getTransitions() {
@@ -49,6 +54,14 @@ public class StateMachineTransitions<S, E> {
return choices;
}
public Map<S, List<S>> getForks() {
return forks;
}
public Map<S, List<S>> getJoins() {
return joins;
}
public static class TransitionData<S, E> {
S source;
S target;

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.List;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
/**
* Default implementation of a {@link ForkTransitionConfigurer}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultForkTransitionConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>>
implements ForkTransitionConfigurer<S, E> {
private S source;
private final List<S> targets = new ArrayList<S>();
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.addFork(source, targets);
}
@Override
public ForkTransitionConfigurer<S, E> source(S source) {
this.source = source;
return this;
}
@Override
public ForkTransitionConfigurer<S, E> target(S target) {
targets.add(target);
return this;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.List;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
/**
* Default implementation of a {@link JoinTransitionConfigurer}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultJoinTransitionConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>>
implements JoinTransitionConfigurer<S, E> {
private S target;
private final List<S> sources = new ArrayList<S>();
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.addJoin(target, sources);
}
@Override
public JoinTransitionConfigurer<S, E> source(S source) {
this.sources.add(source);
return this;
}
@Override
public JoinTransitionConfigurer<S, E> target(S target) {
this.target = target;
return this;
}
}

View File

@@ -59,6 +59,10 @@ public class DefaultStateConfigurer<S, E>
private final Collection<S> choices = new ArrayList<S>();
private final Collection<S> forks = new ArrayList<S>();
private final Collection<S> joins = new ArrayList<S>();
@Override
public void configure(StateMachineStateBuilder<S, E> builder) throws Exception {
// before passing state datas to builder, update structure
@@ -76,6 +80,10 @@ public class DefaultStateConfigurer<S, E>
}
if (choices.contains(s.getState())) {
s.setPseudoStateKind(PseudoStateKind.CHOICE);
} else if (forks.contains(s.getState())) {
s.setPseudoStateKind(PseudoStateKind.FORK);
} else if (joins.contains(s.getState())) {
s.setPseudoStateKind(PseudoStateKind.JOIN);
}
if (s.getState() == history) {
if (History.SHALLOW == historyType) {
@@ -163,6 +171,20 @@ public class DefaultStateConfigurer<S, E>
return this;
}
@Override
public StateConfigurer<S, E> fork(S fork) {
state(fork);
forks.add(fork);
return this;
}
@Override
public StateConfigurer<S, E> join(S join) {
state(join);
joins.add(join);
return this;
}
@Override
public StateConfigurer<S, E> history(S history, History type) {
this.history = history;

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.transition.Transition;
/**
* {@code TransitionConfigurer} interface for configuring {@link Transition}
* from a fork pseudo state.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface ForkTransitionConfigurer<S, E>
extends AnnotationConfigurerBuilder<StateMachineTransitionConfigurer<S, E>> {
/**
* Specify a source state {@code S} for this {@link Transition}.
*
* @param source the source state {@code S}
* @return configurer for chaining
*/
ForkTransitionConfigurer<S, E> source(S source);
/**
* Specify a target state {@code S} for this {@link Transition}.
*
* @param target the target state {@code S}
* @return configurer for chaining
*/
ForkTransitionConfigurer<S, E> target(S target);
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.transition.Transition;
/**
* {@code TransitionConfigurer} interface for configuring {@link Transition}
* from a join pseudo state.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface JoinTransitionConfigurer<S, E>
extends AnnotationConfigurerBuilder<StateMachineTransitionConfigurer<S, E>> {
/**
* Specify a source state {@code S} for this {@link Transition}.
*
* @param source the source state {@code S}
* @return configurer for chaining
*/
JoinTransitionConfigurer<S, E> source(S source);
/**
* Specify a target state {@code S} for this {@link Transition}.
*
* @param target the target state {@code S}
* @return configurer for chaining
*/
JoinTransitionConfigurer<S, E> target(S target);
}

View File

@@ -123,6 +123,22 @@ public interface StateConfigurer<S, E> extends
*/
StateConfigurer<S, E> choice(S choice);
/**
* Specify a state {@code S} to be fork pseudo state.
*
* @param fork the fork pseudo state
* @return configurer for chaining
*/
StateConfigurer<S, E> fork(S fork);
/**
* Specify a state {@code S} to be join pseudo state.
*
* @param join the join pseudo state
* @return configurer for chaining
*/
StateConfigurer<S, E> join(S join);
/**
* Specify a state {@code S} to be history pseudo state.
*

View File

@@ -29,6 +29,8 @@ public abstract class AbstractPseudoState<S, E> implements PseudoState<S, E> {
private final PseudoStateKind kind;
private final CompositePseudoStateListener<S, E> pseudoStateListener = new CompositePseudoStateListener<S, E>();
/**
* Instantiates a new abstract pseudo state.
*
@@ -44,8 +46,26 @@ public abstract class AbstractPseudoState<S, E> implements PseudoState<S, E> {
}
@Override
public State<S, E> entry(E event, StateContext<S, E> context) {
public State<S, E> entry(StateContext<S, E> context) {
return null;
}
@Override
public void exit(StateContext<S, E> context) {
}
@Override
public void addPseudoStateListener(PseudoStateListener<S, E> listener) {
pseudoStateListener.register(listener);
}
/**
* Notify all {@link PseudoStateListener}s of a new context.
*
* @param context the new context
*/
protected void notifyContext(PseudoStateContext<S, E> context) {
pseudoStateListener.onContext(context);
}
}

View File

@@ -42,7 +42,7 @@ public class ChoicePseudoState<S, E> implements PseudoState<S, E> {
}
@Override
public State<S, E> entry(E event, StateContext<S, E> context) {
public State<S, E> entry(StateContext<S, E> context) {
State<S, E> s = null;
for (ChoiceStateData<S, E> c : choices) {
s = c.getState();
@@ -52,6 +52,14 @@ public class ChoicePseudoState<S, E> implements PseudoState<S, E> {
}
return s;
}
@Override
public void exit(StateContext<S, E> context) {
}
@Override
public void addPseudoStateListener(PseudoStateListener<S, E> listener) {
}
public static class ChoiceStateData<S, E> {
private final State<S, E> state;

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import java.util.Iterator;
import org.springframework.statemachine.listener.AbstractCompositeListener;
public class CompositePseudoStateListener<S, E> extends AbstractCompositeListener<PseudoStateListener<S, E>> implements
PseudoStateListener<S, E> {
@Override
public void onContext(PseudoStateContext<S, E> context) {
for (Iterator<PseudoStateListener<S, E>> iterator = getListeners().reverse(); iterator.hasNext();) {
PseudoStateListener<S, E> listener = iterator.next();
listener.onContext(context);
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
/**
* Default implementation of a {@link PseudoStateContext}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultPseudoStateContext<S, E> implements PseudoStateContext<S, E> {
private final PseudoState<S, E> pseudoState;
private final PseudoAction pseudoAction;
/**
* Instantiates a new default pseudo state context.
*
* @param pseudoState the pseudo state
* @param pseudoAction the pseudo action
*/
public DefaultPseudoStateContext(PseudoState<S, E> pseudoState, PseudoAction pseudoAction) {
this.pseudoState = pseudoState;
this.pseudoAction = pseudoAction;
}
@Override
public PseudoState<S, E> getPseudoState() {
return pseudoState;
}
@Override
public PseudoAction getPseudoAction() {
return pseudoAction;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import java.util.List;
import org.springframework.statemachine.StateContext;
/**
* Fork implementation of a {@link PseudoState}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class ForkPseudoState<S, E> extends AbstractPseudoState<S, E> {
private final List<State<S, E>> forks;
public ForkPseudoState(List<State<S, E>> forks) {
super(PseudoStateKind.FORK);
this.forks = forks;
}
@Override
public State<S, E> entry(StateContext<S, E> context) {
return null;
}
public List<State<S, E>> getForks() {
return forks;
}
}

View File

@@ -41,6 +41,11 @@ public class HistoryPseudoState<S, E> extends AbstractPseudoState<S, E> {
"Pseudo state must be either shallow or deep");
}
@Override
public State<S, E> entry(StateContext<S, E> context) {
return state;
}
/**
* Sets the current recorded state.
*
@@ -59,9 +64,4 @@ public class HistoryPseudoState<S, E> extends AbstractPseudoState<S, E> {
return state;
}
@Override
public State<S, E> entry(E event, StateContext<S, E> context) {
return state;
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import java.util.List;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.PseudoStateContext.PseudoAction;
/**
* Join implementation of a {@link PseudoState}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
private final List<State<S, E>> joins;
private volatile JoinTracker tracker;
public JoinPseudoState(List<State<S, E>> joins) {
super(PseudoStateKind.JOIN);
this.joins = joins;
}
@Override
public State<S, E> entry(StateContext<S, E> context) {
tracker = new JoinTracker(this, joins);
context.getStateMachine().addStateListener(tracker);
return null;
}
@Override
public void exit(StateContext<S, E> context) {
tracker = null;
}
public List<State<S, E>> getJoins() {
return joins;
}
private class JoinTracker extends StateMachineListenerAdapter<S, E> {
private final PseudoState<S, E> pseudoState;
private final List<State<S, E>> track;
// TOOO use flat till we can unregister listener
private boolean done = false;
public JoinTracker(PseudoState<S, E> pseudoState, List<State<S, E>> track) {
this.pseudoState = pseudoState;
this.track = track;
}
@Override
public void stateChanged(State<S, E> from, State<S, E> to) {
if (done) {
return;
}
track.remove(to);
if (track.size() == 0) {
done = true;
notifyContext(new DefaultPseudoStateContext<S, E>(pseudoState, PseudoAction.JOIN_COMPLETED));
}
}
}
}

View File

@@ -46,10 +46,23 @@ public interface PseudoState<S, E> {
* Initiate an entry sequence for the state and return a next
* state where state machine should go.
*
* @param event the event
* @param context the context
* @return the next state or null
*/
State<S, E> entry(E event, StateContext<S, E> context);
State<S, E> entry(StateContext<S, E> context);
/**
* Initiate an exit sequence for the state.
*
* @param context the context
*/
void exit(StateContext<S, E> context);
/**
* Registers a new {@link PseudoStateListener}.
*
* @param listener the listener
*/
void addPseudoStateListener(PseudoStateListener<S, E> listener);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
/**
* Context object using in {@link PseudoStateListener}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface PseudoStateContext<S, E> {
/**
* Gets the pseudo state.
*
* @return the pseudo state
*/
PseudoState<S, E> getPseudoState();
/**
* Gets the pseudo action.
*
* @return the pseudo action
*/
PseudoAction getPseudoAction();
/**
* The PseudoAction enumeration.
*/
public enum PseudoAction {
/**
* Indication that states has been joined.
*/
JOIN_COMPLETED;
}
}

View File

@@ -37,6 +37,12 @@ public enum PseudoStateKind {
HISTORY_DEEP,
/** History shallow kind */
HISTORY_SHALLOW
HISTORY_SHALLOW,
/** Fork kind */
FORK,
/** Join kind */
JOIN
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
/**
* {@code PseudoStateListener} for various pseudo state events.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface PseudoStateListener<S, E> {
/**
* Called when {@link PseudoState} want to notify of
* a new {@link PseudoStateContext}.
*
* @param context the context
*/
void onContext(PseudoStateContext<S, E> context);
}

View File

@@ -33,6 +33,8 @@ import org.springframework.statemachine.region.Region;
*/
public class RegionState<S, E> extends AbstractState<S, E> {
private JoinPseudoState<S, E> join;
/**
* Instantiates a new region state.
*
@@ -96,13 +98,13 @@ public class RegionState<S, E> extends AbstractState<S, E> {
@Override
public boolean sendEvent(Message<E> event) {
boolean accept = false;
if (getRegions() != null) {
for (Region<S, E> r : getRegions()) {
r.sendEvent(event);
accept |= r.sendEvent(event);
}
return true;
}
return false;
return accept;
}
@Override
@@ -121,6 +123,9 @@ public class RegionState<S, E> extends AbstractState<S, E> {
@Override
public void entry(StateContext<S, E> context) {
if (join != null) {
join.entry(context);
}
Collection<? extends Action<S, E>> actions = getEntryActions();
if (actions != null) {
for (Action<S, E> action : actions) {
@@ -144,7 +149,9 @@ public class RegionState<S, E> extends AbstractState<S, E> {
@Override
public Collection<S> getIds() {
ArrayList<S> ids = new ArrayList<S>();
ids.add(getId());
if (getId() != null) {
ids.add(getId());
}
for (Region<S, E> r : getRegions()) {
State<S, E> s = r.getState();
if (s != null) {
@@ -164,4 +171,14 @@ public class RegionState<S, E> extends AbstractState<S, E> {
return states;
}
public void setJoin(JoinPseudoState<S, E> join) {
this.join = join;
}
@Override
public String toString() {
return "RegionState [getIds()=" + getIds() + ", getClass()=" + getClass() + ", hashCode()=" + hashCode()
+ ", toString()=" + super.toString() + "]";
}
}

View File

@@ -49,9 +49,12 @@ import org.springframework.statemachine.processor.StateMachineOnTransitionHandle
import org.springframework.statemachine.processor.StateMachineRuntime;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.state.AbstractState;
import org.springframework.statemachine.state.ForkPseudoState;
import org.springframework.statemachine.state.HistoryPseudoState;
import org.springframework.statemachine.state.PseudoState;
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.transition.Transition;
import org.springframework.statemachine.transition.TransitionKind;
@@ -60,6 +63,7 @@ import org.springframework.statemachine.trigger.TimerTrigger;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.statemachine.trigger.TriggerListener;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Base implementation of a {@link StateMachine} loosely modelled from UML state
@@ -222,6 +226,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
return;
}
registerTriggerListener();
registerPseudoStateListener();
switchToState(initialState, initialEvent, null, this);
// TODO: for now execute outside of switchToState
if (initialTransition != null) {
@@ -278,6 +283,10 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
for (State<S, E> s : all) {
buf.append(s.getId() + " ");
}
buf.append(" / ");
if (currentState != null) {
buf.append(StringUtils.collectionToCommaDelimitedString(currentState.getIds()));
}
return buf.toString();
}
@@ -322,8 +331,13 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
if (kind == PseudoStateKind.CHOICE || kind == PseudoStateKind.HISTORY_SHALLOW
|| kind == PseudoStateKind.HISTORY_DEEP) {
StateContext<S, E> stateContext = buildStateContext(message, transition, stateMachine);
State<S, E> toState = state.getPseudoState().entry(message.getPayload(), stateContext);
State<S, E> toState = state.getPseudoState().entry(stateContext);
setCurrentState(toState, message, transition, true, stateMachine);
} else if (kind == PseudoStateKind.FORK) {
ForkPseudoState<S, E> fps = (ForkPseudoState<S, E>) state.getPseudoState();
for (State<S, E> ss : fps.getForks()) {
setCurrentState(ss, message, transition, false, stateMachine);
}
} else {
setCurrentState(state, message, transition, true, stateMachine);
}
@@ -341,6 +355,32 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
}
private void registerPseudoStateListener() {
for (State<S, E> state : states) {
PseudoState<S, E> p = state.getPseudoState();
if (p != null) {
p.addPseudoStateListener(new PseudoStateListener<S, E>() {
@Override
public void onContext(PseudoStateContext<S, E> context) {
PseudoState<S, E> pseudoState = context.getPseudoState();
State<S, E> toState = findStateWithPseudoState(pseudoState);
pseudoState.exit(null);
switchToState(toState, null, null, AbstractStateMachine.this);
}
});
}
}
}
private State<S, E> findStateWithPseudoState(PseudoState<S, E> pseudoState) {
for (State<S, E> s : states) {
if (s.getPseudoState() == pseudoState) {
return s;
}
}
return null;
}
private StateContext<S, E> buildStateContext(Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine) {
E event = message != null ? message.getPayload() : null;
MessageHeaders messageHeaders = message != null ? message.getHeaders() : new MessageHeaders(
@@ -384,22 +424,48 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
exitCurrentState(state, message, transition, stateMachine);
}
if (currentState == findDeep) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
if (submachine.getState() == state) {
if (currentState == findDeep) {
if (isTargetSubOf) {
entryToState(currentState, message, transition, stateMachine);
if (currentState.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
if (submachine.getState() == state) {
if (currentState == findDeep) {
if (isTargetSubOf) {
entryToState(currentState, message, transition, stateMachine);
}
currentState = findDeep;
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, message, transition, false, stateMachine);
return;
}
currentState = findDeep;
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, message, transition, false, stateMachine);
return;
}
} else if (currentState.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)currentState).getRegions();
for (Region<S, E> region : regions) {
if (region.getState() == state) {
if (currentState == findDeep) {
if (isTargetSubOf) {
entryToState(currentState, message, transition, stateMachine);
}
currentState = findDeep;
((AbstractStateMachine<S, E>)region).setCurrentState(state, message, transition, false, stateMachine);
return;
}
}
}
}
}
currentState = findDeep;
entryToState(currentState, message, transition, stateMachine);
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, message, transition, false, stateMachine);
if (currentState.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, message, transition, false, stateMachine);
} else if (currentState.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)currentState).getRegions();
for (Region<S, E> region : regions) {
((AbstractStateMachine<S, E>)region).setCurrentState(state, message, transition, false, stateMachine);
}
}
}
}
if (history != null) {
@@ -424,33 +490,38 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
}
private void exitFromState(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
private void exitFromState(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
if (state == null) {
return;
}
log.trace("Trying Exit state=[" + state + "]");
StateContext<S, E> stateContext = buildStateContext(message, transition, stateMachine);
State<S, E> findDeep = findDeepParent(transition.getTarget());
boolean isTargetSubOfOtherState = findDeep != null && findDeep != currentState;
boolean isTargetSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget());
boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState);
boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState);
if (transition != null) {
// TODO: this and entry below should be done via a separate
// voter of some sort which would reveal transition path
// we could make a choice on.
if (currentState == transition.getSource() && currentState == transition.getTarget()) {
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) {
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) {
} else if (isTargetSubOfOtherState) {
} else if (!isSubOfSource && !isSubOfTarget && findDeep == null) {
} else if (!isSubOfSource && !isSubOfTarget) {
return;
}
State<S, E> findDeep = findDeepParent(transition.getTarget());
boolean isTargetSubOfOtherState = findDeep != null && findDeep != currentState;
boolean isTargetSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget());
boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState);
boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState);
// TODO: this and entry below should be done via a separate
// voter of some sort which would reveal transition path
// we could make a choice on.
if (currentState == transition.getSource() && currentState == transition.getTarget()) {
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) {
} else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) {
} else if (isTargetSubOfOtherState) {
} else if (!isSubOfSource && !isSubOfTarget && findDeep == null) {
} else if (!isSubOfSource && !isSubOfTarget) {
return;
}
if (transition.getSource() == currentState && isTargetSubOfSource) {
return;
}
if (transition.getSource() == currentState && isTargetSubOfSource) {
return;
}
log.debug("Exit state=[" + state + "]");

View File

@@ -52,8 +52,8 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
*/
protected StateMachineEventPublisher getStateMachineEventPublisher() {
if(stateMachineEventPublisher == null && getBeanFactory() != null) {
if(log.isDebugEnabled()) {
log.debug("getting stateMachineEventPublisher service from bean factory " + getBeanFactory());
if(log.isTraceEnabled()) {
log.trace("getting stateMachineEventPublisher service from bean factory " + getBeanFactory());
}
stateMachineEventPublisher = StateMachineContextUtils.getEventPublisher(getBeanFactory());
}

View File

@@ -219,9 +219,7 @@ public class RegionMachineTests extends AbstractStateMachineTests {
assertThat(exitActionS112.stateContexts.size(), is(0));
}
// effectively broken now until we get more fixes
// due to work with region fork/join
//@Test
@Test
public void testMultiRegion() throws Exception {
context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config1.class);
context.refresh();

View File

@@ -16,6 +16,7 @@
package org.springframework.statemachine;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -100,6 +101,24 @@ public class StateMachineTests extends AbstractStateMachineTests {
assertThat(testAction2.stateContexts.size(), is(timedTriggered));
}
@Test
@SuppressWarnings("unchecked")
public void testForkJoin() {
context.register(BaseConfig.class, Config3.class);
context.refresh();
EnumStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
assertThat(machine, notNullValue());
machine.start();
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.SI));
machine.sendEvent(TestEvents.E1);
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S20, TestStates.S30));
machine.sendEvent(TestEvents.E2);
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S30));
machine.sendEvent(TestEvents.E3);
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S4));
}
private static class LoggingAction implements Action<TestStates, TestEvents> {
private static final Log log = LogFactory.getLog(StateMachineTests.LoggingAction.class);
@@ -237,4 +256,68 @@ public class StateMachineTests extends AbstractStateMachineTests {
}
@Configuration
@EnableStateMachine
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.SI)
.state(TestStates.SI)
.fork(TestStates.S1)
.state(TestStates.S2)
.end(TestStates.SF)
.join(TestStates.S3)
.state(TestStates.S4)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S20)
.state(TestStates.S20)
.state(TestStates.S21)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S30)
.state(TestStates.S30)
.state(TestStates.S31);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.SI)
.target(TestStates.S2)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S20)
.target(TestStates.S21)
.event(TestEvents.E2)
.and()
.withExternal()
.source(TestStates.S30)
.target(TestStates.S31)
.event(TestEvents.E3)
.and()
.withFork()
.source(TestStates.S1)
.target(TestStates.S20)
.target(TestStates.S30)
.and()
.withJoin()
.source(TestStates.S21)
.source(TestStates.S31)
.target(TestStates.S3)
.and()
.withExternal()
.source(TestStates.S3)
.target(TestStates.S4);
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
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.EnumStateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public class ForkStateTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@Test
@SuppressWarnings("unchecked")
public void testForkEventPassed() {
context.register(BaseConfig.class, Config1.class);
context.refresh();
EnumStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
TestEntryAction s20EntryAction = context.getBean("s20EntryAction", TestEntryAction.class);
TestEntryAction s21EntryAction = context.getBean("s21EntryAction", TestEntryAction.class);
TestEntryAction s30EntryAction = context.getBean("s30EntryAction", TestEntryAction.class);
TestEntryAction s31EntryAction = context.getBean("s31EntryAction", TestEntryAction.class);
assertThat(machine, notNullValue());
machine.start();
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("foo", "bar").build());
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S31));
assertThat(s20EntryAction.stateContexts.size(), is(1));
assertThat(s21EntryAction.stateContexts.size(), is(1));
assertThat(s30EntryAction.stateContexts.size(), is(1));
assertThat(s31EntryAction.stateContexts.size(), is(1));
assertThat((String)s20EntryAction.stateContexts.get(0).getMessageHeader("foo"), nullValue());
assertThat((String)s21EntryAction.stateContexts.get(0).getMessageHeader("foo"), is("bar"));
assertThat((String)s30EntryAction.stateContexts.get(0).getMessageHeader("foo"), nullValue());
assertThat((String)s31EntryAction.stateContexts.get(0).getMessageHeader("foo"), is("bar"));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.SI)
.fork(TestStates.S1)
.state(TestStates.SI)
.state(TestStates.S2)
.end(TestStates.SF)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S20)
.state(TestStates.S20, s20EntryAction(), null)
.state(TestStates.S21, s21EntryAction(), null)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S30)
.state(TestStates.S30, s30EntryAction(), null)
.state(TestStates.S31, s31EntryAction(), null);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.SI)
.target(TestStates.S1)
.event(TestEvents.E1)
.and()
.withFork()
.source(TestStates.S1)
.target(TestStates.S21)
.target(TestStates.S31);
}
@Bean
public TestEntryAction s20EntryAction() {
return new TestEntryAction();
}
@Bean
public TestEntryAction s21EntryAction() {
return new TestEntryAction();
}
@Bean
public TestEntryAction s30EntryAction() {
return new TestEntryAction();
}
@Bean
public TestEntryAction s31EntryAction() {
return new TestEntryAction();
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.EnumStateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public class JoinStateTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@Test
@SuppressWarnings("unchecked")
public void testJoin() {
context.register(BaseConfig.class, Config1.class);
context.refresh();
EnumStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
assertThat(machine, notNullValue());
machine.start();
machine.sendEvent(TestEvents.E1);
machine.sendEvent(TestEvents.E2);
machine.sendEvent(TestEvents.E3);
assertThat(machine.getState().getIds(), contains(TestStates.S4));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.SI)
.state(TestStates.SI)
.state(TestStates.S2)
.end(TestStates.SF)
.join(TestStates.S3)
.state(TestStates.S4)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S20)
.state(TestStates.S20)
.state(TestStates.S21)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S30)
.state(TestStates.S30)
.state(TestStates.S31);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.SI)
.target(TestStates.S2)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S20)
.target(TestStates.S21)
.event(TestEvents.E2)
.and()
.withExternal()
.source(TestStates.S30)
.target(TestStates.S31)
.event(TestEvents.E3)
.and()
.withJoin()
.source(TestStates.S21)
.source(TestStates.S31)
.target(TestStates.S3)
.and()
.withExternal()
.source(TestStates.S3)
.target(TestStates.S4);
}
}
}

View File

@@ -4,5 +4,5 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} [%t] - %m%n
log4j.category.org.springframework.statemachine=TRACE
log4j.category.org.springframework.statemachine=DEBUG