Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,10 @@ To define the context event, user has two way, annotation or builder API.

* **Using History States to Save and Restore the Current State**

The history pseudo-state allows a state machine to remember its state configuration. A transition taking the history state as its target will return the state machine to this recorded configuration. If the 'type' of a history is "shallow", the state machine processor must record the direct active children of its parent before taking any transition that exits the parent. If the 'type' of a history is "deep", the state machine processor must record all the active descendants of the parent before taking any transition that exits the parent.
The history pseudo-state allows a state machine to remember its state configuration. A transition taking the history state as its target will return the state machine to this recorded configuration. Different types of history are available:
- "shallow": the history pseudo-state represents the last active child of this composite state.
- "recursive": the history pseudo-state represents the last active child of the configuration of this composite state, including the nested composite states down to the last composite state with history type "recursive".
- "deep": the history pseudo-state represents the last active child of the whole configuration of this composite state, including the nested composite states down to the leaf state.
Both API and annotation are supported to define history type of state. e.g.

```java
Expand All @@ -516,6 +519,7 @@ To define the context event, user has two way, annotation or builder API.
```

**Note:** Before 0.3.7, user need to define "HistoryType.DEEP" for each level of historical state, which is not quite convenient.(Thanks to [Voskuijlen](https://github.com/Voskuijlen) to provide solution [Issue33](https://github.com/hekailiang/squirrel/issues/33)). Now user only define "HistoryType.DEEP" at the top level of historical state, and all its children state historical information will be remembered.
**Note:** "HistoryType.RECURSIVE" is not UML compliant and adds more control on the depth of the history.

* **Transition Types**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public enum HistoryType {
*/
SHALLOW,

/**
* The state enters into its last active sub-state. The sub-state itself enters its last active sub-state if it has
* recursive history too and so on until the sub-state has no recursive history or the innermost nested state is reached.
*/
RECURSIVE,

/**
* The state enters into its last active sub-state. The sub-state itself enters into-its last active state and so on until the innermost
* nested state is reached.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,20 @@ public interface ImmutableState<T extends StateMachine<T, S, E, C>, S, E, C> ex
*/
ImmutableState<T, S, E, C> enterDeep(StateContext<T, S, E, C> stateContext);

/**
* Enters this state is recursive mode: The entry action is executed and the
* last activate state is entered if it is in recursive mode itself,
* the initial state is entered otherwise.
* @param stateContext
* @return child state entered by recursive
*/
ImmutableState<T, S, E, C> enterRecursive(StateContext<T, S, E, C> stateContext);

/**
* Enters this state is shallow mode: The entry action is executed and the
* initial state is entered in shallow mode if there is one.
* @param stateContext
* @return child state entered by shadow
* @return child state entered by shallow
*/
ImmutableState<T, S, E, C> enterShallow(StateContext<T, S, E, C> stateContext);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public interface StateMachine<T extends StateMachine<T, S, E, C>, S, E, C> exten
*/
void fireImmediate(E event, C context);

/**
* Immediately processes the given event. If current state machine is busy,
* this call blocks until it is available for event processing.
* @param event the event
* @param context external context
* @return true if the event has been accepted,
* false otherwise (because no valid transition or because it was rejected by transition guards)
*/
boolean processImmediate(E event, C context);

/**
* Test transition result under circumstance
* @param event test event
Expand All @@ -57,6 +67,15 @@ public interface StateMachine<T extends StateMachine<T, S, E, C>, S, E, C> exten
*/
void fireImmediate(E event);

/**
* Immediately processes the given event. If current state machine is busy,
* this call blocks until it is available for event processing.
* @param event the event
* @return true if the event has been accepted,
* false otherwise (because no valid transition or because it was rejected by transition guards)
*/
boolean processImmediate(E event);

/**
* Test event
* @param event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,22 @@ public void fireImmediate(E event, C context) {
fire(event, context, true);
}

@Override
public boolean processImmediate(E event, C context) {
boolean eventAccepted = false;
writeLock.lock();
setStatus(StateMachineStatus.BUSY);
eventAccepted = processEvent(event, context, data, executor, isDataIsolateEnabled);
ImmutableState<T, S, E, C> rawState = data.read().currentRawState();
if(isAutoTerminateEnabled && rawState.isRootState() && rawState.isFinalState()) {
terminate(context);
}
if(getStatus()==StateMachineStatus.BUSY)
setStatus(StateMachineStatus.IDLE);
writeLock.unlock();
return eventAccepted;
}

@Override
public void fire(E event) {
fire(event, null);
Expand All @@ -303,6 +319,11 @@ public void fireImmediate(E event) {
fireImmediate(event, null);
}

@Override
public boolean processImmediate(E event) {
return processImmediate(event, null);
}

/**
* Clean all queued events
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ public void visitOnEntry(ImmutableState<?, ?, ?, ?> visitable) {
String stateLabel = visitable.getStateId().toString();
if (visitable.hasChildStates()) {
writeLine("subgraph cluster_" + stateId + " {\nlabel=\"" + stateLabel + "\";");
if (visitable.getHistoryType() == HistoryType.DEEP) {
writeLine(stateId + "History" + " [label=\"\"];");
} else if (visitable.getHistoryType() == HistoryType.SHALLOW) {
writeLine(stateId + "History" + " [label=\"\"];");
if (visitable.getHistoryType() != HistoryType.NONE) {
writeLine(stateId + "History" + " [label=\""+visitable.getHistoryType()+"\"];");
}
} else {
writeLine(stateId + " [label=\"" + stateLabel + "\"];");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ public ImmutableState<T, S, E, C> enterByHistory(StateContext<T, S, E, C> stateC
case SHALLOW:
result = enterHistoryShallow(stateContext);
break;
case RECURSIVE:
result = enterHistoryRecursive(stateContext);
break;
case DEEP:
result = enterHistoryDeep(stateContext);
break;
Expand All @@ -266,6 +269,18 @@ public ImmutableState<T, S, E, C> enterDeep(StateContext<T, S, E, C> stateContex
return lastActiveState == null ? this : lastActiveState.enterDeep(stateContext);
}

@Override
public ImmutableState<T, S, E, C> enterRecursive(StateContext<T, S, E, C> stateContext) {
entry(stateContext);
if (this.getHistoryType() == HistoryType.RECURSIVE) {
final ImmutableState<T, S, E, C> lastActiveState =
getLastActiveChildStateOf(this, stateContext.getStateMachineData().read());
return lastActiveState == null ? this : lastActiveState.enterRecursive(stateContext);
} else {
return childInitialState!=null ? childInitialState.enterShallow(stateContext) : this;
}
}

@Override
public ImmutableState<T, S, E, C> enterShallow(StateContext<T, S, E, C> stateContext) {
entry(stateContext);
Expand All @@ -284,6 +299,18 @@ private ImmutableState<T, S, E, C> enterHistoryShallow(StateContext<T, S, E, C>
return lastActiveState != null ? lastActiveState.enterShallow(stateContext) : this;
}

/**
* Enters this instance with history type = recursive.
*
* @param stateContext
* state context
* @return the entered state
*/
private ImmutableState<T, S, E, C> enterHistoryRecursive(StateContext<T, S, E, C> stateContext) {
final ImmutableState<T, S, E, C> lastActiveState = getLastActiveChildStateOf(this, stateContext.getStateMachineData().read());
return lastActiveState != null ? lastActiveState.enterRecursive(stateContext) : this;
}

/**
* Enters with history type = none.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ public void startElement(String uri, String localName, String qName,
getCurrentState().setHistoryType(HistoryType.DEEP);
} else if(historyType.equals("shallow")) {
getCurrentState().setHistoryType(HistoryType.SHALLOW);
} else if(historyType.equals("recursive")) {
getCurrentState().setHistoryType(HistoryType.RECURSIVE);
}
} else if(qName.equals("onentry")) {
isEntryAction = Boolean.TRUE;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.squirrelframework.foundation.fsm.samples;

import org.squirrelframework.foundation.fsm.annotation.State;
import org.squirrelframework.foundation.fsm.annotation.States;
import org.squirrelframework.foundation.fsm.annotation.Transit;
import org.squirrelframework.foundation.fsm.annotation.Transitions;
import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine;
import org.squirrelframework.foundation.fsm.StateMachineBuilderFactory;
import org.squirrelframework.foundation.fsm.AnonymousCondition;
import org.squirrelframework.foundation.fsm.HistoryType;
import org.squirrelframework.foundation.fsm.StateMachineBuilder;

enum SampleStates {
L1_A, L1_B, L1_C, L2_A, L2_B, L2_C, L31_A, L31_B, L32_A, L32_B
}

enum SampleEvents {
E1, E2, E3, E4, E5, E6, E7, E8, E9
}

class SampleContext {
public boolean isOK;
}

class G1 extends AnonymousCondition<SampleContext> {
@Override
public boolean isSatisfied(SampleContext context) {
return context.isOK;
}
}

@States({
@State(name = "L1_A", initialState = true),
@State(name = "L1_B", historyType = HistoryType.RECURSIVE),
@State(name = "L1_C"),
@State(name = "L2_A", parent = "L1_B", initialState = true),
@State(name = "L2_B", parent = "L1_B", historyType = HistoryType.RECURSIVE),
@State(name = "L2_C", parent = "L1_B"),
@State(name = "L31_A", parent = "L2_B", initialState = true),
@State(name = "L31_B", parent = "L2_B"),
@State(name = "L32_A", parent = "L2_C", initialState = true),
@State(name = "L32_B", parent = "L2_C"),
})
@Transitions({
@Transit(from = "L1_A", to = "L1_B", on = "E1"),
@Transit(from = "L2_A", to = "L2_B", on = "E2"),
@Transit(from = "L2_A", to = "L2_C", on = "E3"),
@Transit(from = "L31_A", to = "L31_B", on = "E4"),
@Transit(from = "L32_A", to = "L32_B", on = "E5"),
@Transit(from = "L1_B", to = "L1_C", on = "E6"),
@Transit(from = "L1_C", to = "L1_B", on = "E7"),
@Transit(from = "L1_C", to = "L2_A", on = "E8"),
@Transit(from = "L1_A", to = "L1_C", on = "E9", when = G1.class),
})
public class RecursiveHistorySample extends
AbstractStateMachine<RecursiveHistorySample, SampleStates, SampleEvents, SampleContext> {

public static RecursiveHistorySample create() {
StateMachineBuilder<RecursiveHistorySample, SampleStates, SampleEvents, SampleContext> builder = StateMachineBuilderFactory
.create(RecursiveHistorySample.class, SampleStates.class, SampleEvents.class, SampleContext.class);

return builder.newStateMachine(SampleStates.L1_A);
}

public static void main(String[] args) {
final SampleContext sampleContext = new SampleContext();

RecursiveHistorySample sampleController = RecursiveHistorySample.create();
sampleController.start();

sampleContext.isOK = false;
System.out.println("Should not be accepted (false): "
+ sampleController.processImmediate(SampleEvents.E9, sampleContext));
sampleContext.isOK = true;
System.out.println(
"Should be accepted (true): " + sampleController.processImmediate(SampleEvents.E9, sampleContext));
System.out.println("Current state should be L1_C : " + sampleController.getCurrentState());

// Demonstrate recursive history through two levels
sampleController.fire(SampleEvents.E8);
sampleController.fire(SampleEvents.E2);
sampleController.fire(SampleEvents.E4);
System.out.println("Current state should be L31_B: " + sampleController.getCurrentState());
sampleController.fire(SampleEvents.E6);
System.out.println("Current state should be L1_C : " + sampleController.getCurrentState());
sampleController.fire(SampleEvents.E7);
System.out.println("Current state should be L31_B: " + sampleController.getCurrentState());

// Start back, do not use history
sampleController.fire(SampleEvents.E6);
sampleController.fire(SampleEvents.E8);

// Demonstrate recursive history through one level only, using initial state on
// second level
sampleController.fire(SampleEvents.E3);
sampleController.fire(SampleEvents.E5);
System.out.println("Current state should be L32_B: " + sampleController.getCurrentState());
sampleController.fire(SampleEvents.E6);
System.out.println("Current state should be L1_C : " + sampleController.getCurrentState());
sampleController.fire(SampleEvents.E7);
System.out.println("Current state should be L32_A: " + sampleController.getCurrentState());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void execute(Object from, Object to, Object event,
if (logger.length() > 0) {
logger.append('.');
}
logger.append("AToBOnFIRST");
logger.append("AToAOnFIRST");
}
});
builder.transition().from("A").to("B").on("SECOND");
Expand All @@ -63,12 +63,15 @@ public void onTransitionDeclined() {
});
fsm.start();
try {
TimeUnit.MILLISECONDS.sleep(500);
// the first EVENT is delayed of 10ms, so we should wait at least 410ms,
// and at most 500ms to ensure that exactly 5 FIRST events are triggered by A.
// Don't forget to keep enough time to process SECOND event before a 6th FIRST event is triggered.
TimeUnit.MILLISECONDS.sleep(450);
} catch (InterruptedException e) {
}
fsm.fire("SECOND");
fsm.terminate();
assertEquals("AToBOnFIRST.AToBOnFIRST.AToBOnFIRST.AToBOnFIRST.AToBOnFIRST", logger.toString());
assertEquals("AToAOnFIRST.AToAOnFIRST.AToAOnFIRST.AToAOnFIRST.AToAOnFIRST", logger.toString());
}

@Test
Expand Down