Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue-15: Support action on transition #55

Merged
merged 4 commits into from Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Expand Up @@ -53,6 +53,7 @@ Most standard state machine constructs are supported:
* Hierarchical states
* Entry/exit events for states
* Guard clauses to support conditional transitions
* User-defined actions can be executed when transitioning
* Introspection


Expand Down Expand Up @@ -89,6 +90,23 @@ methods being called repeatedly because the `OnHold` state is a substate of the
Entry/Exit event handlers can be supplied with a parameter of type `Transition` that describes the trigger,
source and destination states.

Action on transition
===================
It is possible to execute a user-defined action when doing a transition.
For a 'normal' or 're-entrant' transition this action will be called
without any parameters. For 'dynamic' transitions (those who compute the
target state based on trigger-given parameters) the parameters of the
trigger will be given to the action.

This action is only executed if the transition is actually taken; so if
the transition is guarded and the guard forbids a transition, then the
action is not executed.

If the transition is taken, the action will be executed between the
`onExit` handler of the current state and the `onEntry` handler of the
target state (which might be the same state in case of a re-entrant
transition.

License
=======
Apache 2.0 License
Apache 2.0 License
8 changes: 7 additions & 1 deletion examples/PhoneCallJava7.java
Expand Up @@ -23,6 +23,12 @@ public void doIt() {
stopCallTimer();
}
};
Action reportLeftMessage = new Action() {
@Override
public void doIt() {
System.out.println("Received 'LeftMessage' in 'Connected'");
}
};

StateMachineConfig<State, Trigger> phoneCallConfig = new StateMachineConfig<>();

Expand All @@ -36,7 +42,7 @@ public void doIt() {
phoneCallConfig.configure(State.Connected)
.onEntry(callStartTimer)
.onExit(callStopTimer)
.permit(Trigger.LeftMessage, State.OffHook)
.permit(Trigger.LeftMessage, State.OffHook, reportLeftMessage)
.permit(Trigger.HungUp, State.OffHook)
.permit(Trigger.PlacedOnHold, State.OnHold);

Expand Down
345 changes: 336 additions & 9 deletions src/main/java/com/github/oxo42/stateless4j/StateConfiguration.java

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/main/java/com/github/oxo42/stateless4j/StateMachine.java
Expand Up @@ -89,7 +89,7 @@ public StateMachine(S initialState, Func<S> stateAccessor, Action1<S> stateMutat
public StateConfiguration<S, T> configure(S state) {
return config.configure(state);
}

public StateMachineConfig<S, T> configuration() {
return config;
}
Expand Down Expand Up @@ -203,6 +203,7 @@ protected void publicFire(T trigger, Object... args) {
Transition<S, T> transition = new Transition<>(source, destination.get(), trigger);

getCurrentRepresentation().exit(transition);
triggerBehaviour.performAction(args);
setState(destination.get());
getCurrentRepresentation().enter(transition, args);
}
Expand Down
@@ -1,16 +1,24 @@
package com.github.oxo42.stateless4j.transitions;

import com.github.oxo42.stateless4j.OutVar;
import com.github.oxo42.stateless4j.delegates.Action;
import com.github.oxo42.stateless4j.delegates.FuncBoolean;
import com.github.oxo42.stateless4j.triggers.TriggerBehaviour;

public class TransitioningTriggerBehaviour<S, T> extends TriggerBehaviour<S, T> {

private final S destination;
private final Action action;

public TransitioningTriggerBehaviour(T trigger, S destination, FuncBoolean guard) {
public TransitioningTriggerBehaviour(T trigger, S destination, FuncBoolean guard, Action action) {
super(trigger, guard);
this.destination = destination;
this.action = action;
}

@Override
public void performAction(Object[] args) {
action.doIt();
}

@Override
Expand Down
@@ -1,17 +1,25 @@
package com.github.oxo42.stateless4j.triggers;

import com.github.oxo42.stateless4j.OutVar;
import com.github.oxo42.stateless4j.delegates.Action1;
import com.github.oxo42.stateless4j.delegates.Func2;
import com.github.oxo42.stateless4j.delegates.FuncBoolean;

public class DynamicTriggerBehaviour<S, T> extends TriggerBehaviour<S, T> {

private final Func2<Object[], S> destination;
private final Action1<Object[]> action;

public DynamicTriggerBehaviour(T trigger, Func2<Object[], S> destination, FuncBoolean guard) {
public DynamicTriggerBehaviour(T trigger, Func2<Object[], S> destination, FuncBoolean guard, Action1<Object[]> action) {
super(trigger, guard);
assert destination != null : "destination is null";
this.destination = destination;
this.action = action;
}

@Override
public void performAction(Object[] args) {
action.doIt(args);
}

@Override
Expand Down
Expand Up @@ -8,6 +8,11 @@ public class IgnoredTriggerBehaviour<TState, TTrigger> extends TriggerBehaviour<
public IgnoredTriggerBehaviour(TTrigger trigger, FuncBoolean guard) {
super(trigger, guard);
}

@Override
public void performAction(Object[] args) {
// no need to do anything. This is never called (no transition => no action)
}

@Override
public boolean resultsInTransitionFrom(TState source, Object[] args, OutVar<TState> dest) {
Expand Down
Expand Up @@ -17,6 +17,8 @@ public T getTrigger() {
return trigger;
}

public abstract void performAction(Object[] args);

public boolean isGuardConditionMet() {
return guard.call();
}
Expand Down
@@ -0,0 +1,184 @@
package com.github.oxo42.stateless4j;

import java.util.ArrayList;
import java.util.List;

import com.github.oxo42.stateless4j.delegates.Action;
import com.github.oxo42.stateless4j.delegates.Action1;
import com.github.oxo42.stateless4j.delegates.Action2;
import com.github.oxo42.stateless4j.delegates.Action3;
import com.github.oxo42.stateless4j.delegates.Action4;
import com.github.oxo42.stateless4j.delegates.Func;
import com.github.oxo42.stateless4j.delegates.Func2;
import com.github.oxo42.stateless4j.delegates.Func3;
import com.github.oxo42.stateless4j.delegates.Func4;
import com.github.oxo42.stateless4j.triggers.TriggerWithParameters1;
import com.github.oxo42.stateless4j.triggers.TriggerWithParameters2;
import com.github.oxo42.stateless4j.triggers.TriggerWithParameters3;

import org.junit.Test;
import static org.junit.Assert.*;

public class DynamicTransitionActionTests {

final Enum StateA = State.A, StateB = State.B, StateC = State.C,
TriggerX = Trigger.X, TriggerY = Trigger.Y;

final private TriggerWithParameters1<Integer, State, Trigger> TriggerX1 =
new TriggerWithParameters1<Integer, State, Trigger>(Trigger.X, Integer.class);

final private TriggerWithParameters2<Integer, Integer, State, Trigger> TriggerY2 =
new TriggerWithParameters2<Integer, Integer, State, Trigger>(Trigger.Y, Integer.class, Integer.class);

final private TriggerWithParameters3<Integer, Integer, Integer, State, Trigger> TriggerY3 =
new TriggerWithParameters3<Integer, Integer, Integer, State, Trigger>(Trigger.Y, Integer.class, Integer.class, Integer.class);


private class DynamicallyGotoState<T> implements Func<State>, Func2<T, State>, Func3<T, T, State>, Func4<T, T, T, State> {

private State targetState;

public DynamicallyGotoState(State whereToGo) {
this.targetState = whereToGo;
}

@Override
public State call() {
return this.targetState;
}

@Override
public State call(T value) {
return this.targetState;
}

@Override
public State call(T val1, T val2) {
return this.targetState;
}

@Override
public State call(T val1, T val2, T val3) {
return this.targetState;
}
};

private DynamicallyGotoState<Integer> gotoA = new DynamicallyGotoState<Integer>(State.A);
private DynamicallyGotoState<Integer> gotoB = new DynamicallyGotoState<Integer>(State.B);

private class AccumulatingAction<T> implements Action1<T>, Action2<T,T>, Action3<T,T,T>, Action4<T,T,T,T> {
private List<T> accumulator;

public AccumulatingAction(List<T> accumulator) {
this.accumulator = accumulator;
}

@Override
public void doIt(T arg1, T arg2, T arg3, T arg4) {
accumulator.add(arg1);
accumulator.add(arg2);
accumulator.add(arg3);
accumulator.add(arg4);
}

@Override
public void doIt(T arg1, T arg2, T arg3) {
accumulator.add(arg1);
accumulator.add(arg2);
accumulator.add(arg3);
}

@Override
public void doIt(T arg1, T arg2) {
accumulator.add(arg1);
accumulator.add(arg2);
}

@Override
public void doIt(T arg1) {
accumulator.add(arg1);
}
}

private class FixedAccumulator<T> extends AccumulatingAction<T> implements Action {
private T fixedItem;

public FixedAccumulator(List<T> accumulator, T fixedItem) {
super(accumulator);
this.fixedItem = fixedItem;
}

@Override
public void doIt() {
doIt(this.fixedItem);
}
}

@Test
public void UnguardedDynamicTransitionActionsArePerformed() {
StateMachineConfig<State, Trigger> config = new StateMachineConfig<>();

List<Integer> list = new ArrayList<Integer>();
FixedAccumulator<Integer> actionZero = new FixedAccumulator<Integer>(list, new Integer(0));
AccumulatingAction<Integer> actionOne = new AccumulatingAction<Integer>(list);
AccumulatingAction<Integer> actionTwo = new AccumulatingAction<Integer>(list);
AccumulatingAction<Integer> actionThree = new AccumulatingAction<Integer>(list);

config.configure(State.A)
.permitDynamic(Trigger.X, gotoB, actionZero)
.permitDynamic(TriggerY2, gotoB, actionTwo);
config.configure(State.B)
.permitDynamic(TriggerX1, gotoA, actionOne)
.permitDynamic(TriggerY3, gotoA, actionThree);

StateMachine<State, Trigger> sm = new StateMachine<>(State.A, config);
sm.fire(Trigger.X);
sm.fire(TriggerX1, new Integer(2));
sm.fire(TriggerY2, new Integer(4), new Integer(6));
sm.fire(TriggerY3, new Integer(8), new Integer(10), new Integer(12));

assertEquals(State.A, sm.getState());
assertEquals(7, list.size());
assertEquals(new Integer(0), list.get(0));
assertEquals(new Integer(2), list.get(1));
assertEquals(new Integer(4), list.get(2));
assertEquals(new Integer(6), list.get(3));
assertEquals(new Integer(8), list.get(4));
assertEquals(new Integer(10), list.get(5));
assertEquals(new Integer(12), list.get(6));
}

@Test
public void GuardedDynamicTransitionActionsArePerformed() {
StateMachineConfig<State, Trigger> config = new StateMachineConfig<>();

List<Integer> list = new ArrayList<Integer>();
FixedAccumulator<Integer> actionZero = new FixedAccumulator<Integer>(list, new Integer(0));
AccumulatingAction<Integer> actionOne = new AccumulatingAction<Integer>(list);
AccumulatingAction<Integer> actionTwo = new AccumulatingAction<Integer>(list);
AccumulatingAction<Integer> actionThree = new AccumulatingAction<Integer>(list);

config.configure(State.A)
.permitDynamicIf(Trigger.X, gotoB, IgnoredTriggerBehaviourTests.returnTrue, actionZero)
.permitDynamicIf(TriggerY2, gotoB, IgnoredTriggerBehaviourTests.returnTrue, actionTwo);
config.configure(State.B)
.permitDynamicIf(TriggerX1, gotoA, IgnoredTriggerBehaviourTests.returnTrue, actionOne)
.permitDynamicIf(TriggerY3, gotoA, IgnoredTriggerBehaviourTests.returnTrue, actionThree);

StateMachine<State, Trigger> sm = new StateMachine<>(State.A, config);
sm.fire(Trigger.X);
sm.fire(TriggerX1, new Integer(3));
sm.fire(TriggerY2, new Integer(6), new Integer(9));
sm.fire(TriggerY3, new Integer(12), new Integer(15), new Integer(18));

assertEquals(State.A, sm.getState());
assertEquals(7, list.size());
assertEquals(new Integer(0), list.get(0));
assertEquals(new Integer(3), list.get(1));
assertEquals(new Integer(6), list.get(2));
assertEquals(new Integer(9), list.get(3));
assertEquals(new Integer(12), list.get(4));
assertEquals(new Integer(15), list.get(5));
assertEquals(new Integer(18), list.get(6));
}
}
@@ -1,5 +1,6 @@
package com.github.oxo42.stateless4j;

import com.github.oxo42.stateless4j.delegates.Action;
import com.github.oxo42.stateless4j.delegates.FuncBoolean;
import com.github.oxo42.stateless4j.triggers.IgnoredTriggerBehaviour;
import org.junit.Test;
Expand All @@ -26,6 +27,13 @@ public boolean call() {
}
};

public static Action nopAction = new Action() {

@Override
public void doIt() {
}
};

@Test
public void StateRemainsUnchanged() {
IgnoredTriggerBehaviour<State, Trigger> ignored = new IgnoredTriggerBehaviour<>(Trigger.X, returnTrue);
Expand Down
Expand Up @@ -319,21 +319,24 @@ public void WhenTransitionExists_TriggerCanBeFired() {
@Test
public void WhenTransitionExistsButGuardConditionNotMet_TriggerCanBeFired() {
StateRepresentation<State, Trigger> rep = CreateRepresentation(State.B);
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(Trigger.X, IgnoredTriggerBehaviourTests.returnFalse));
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(
Trigger.X, IgnoredTriggerBehaviourTests.returnFalse));
assertFalse(rep.canHandle(Trigger.X));
}

@Test
public void WhenTransitionDoesNotExist_TriggerCannotBeFired() {
StateRepresentation<State, Trigger> rep = CreateRepresentation(State.B);
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(Trigger.X, IgnoredTriggerBehaviourTests.returnTrue));
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(
Trigger.X, IgnoredTriggerBehaviourTests.returnTrue));
assertTrue(rep.canHandle(Trigger.X));
}

@Test
public void WhenTransitionExistsInSupersate_TriggerCanBeFired() {
StateRepresentation<State, Trigger> rep = CreateRepresentation(State.B);
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(Trigger.X, IgnoredTriggerBehaviourTests.returnTrue));
rep.addTriggerBehaviour(new IgnoredTriggerBehaviour<State, Trigger>(
Trigger.X, IgnoredTriggerBehaviourTests.returnTrue));
StateRepresentation<State, Trigger> sub = CreateRepresentation(State.C);
sub.setSuperstate(rep);
rep.addSubstate(sub);
Expand Down