Skip to content

Commit

Permalink
Backport to Java 7
Browse files Browse the repository at this point in the history
  • Loading branch information
lind committed Nov 10, 2015
1 parent 745e40d commit b170f90
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 94 deletions.
Binary file removed ATMStateMachine.bmp
Binary file not shown.
Binary file removed ATMStateMachineDiagram.png
Binary file not shown.
29 changes: 8 additions & 21 deletions README.md
Expand Up @@ -3,19 +3,19 @@
[![Dependency Status](https://www.versioneye.com/user/projects/550195a04a1064db0e000328/badge.svg?style=flat)](https://www.versioneye.com/user/projects/550195a04a1064db0e000328)
[![Coverage Status](https://coveralls.io/repos/lind/machineprocess/badge.svg?branch=master)](https://coveralls.io/r/lind/machineprocess?branch=master)

Simple State Machine with transitions, guards, composite state.
Simple State Machine with transitions, guards and actions.

...but without sub-state machine, deferred signals, event queues...
...but without composite state, sub-state machine, deferred signals, event queues...

Define the State Machine by subclassing State Machine and using the builders in the constructor. Se tests for usage.

<!-- language: lang-java -->
State onHold = state(ON_HOLD)
.transition(HURL_PHONE).guardedBy(event -> PHONE_HURLED_AGAINST_WALL.equals(event.getName()))
.transition(HURL_PHONE).guardedBy(PHONE_HURLED_AGAINST_WALL)
.to(phoneDestroyed)
.transition(HANG_UP).guardedBy(event -> HUNG_UP.equals(event.getName()))
.transition(HANG_UP).guardedBy(HUNG_UP)
.to(offHook)
.transition(TAKE_OFF_HOLD).onTransition(stopMuzak).guardedBy(event -> TOOK_OFF_HOLD.equals(event.getName()))
.transition(TAKE_OFF_HOLD).onTransition(stopMuzak).guardedBy(TOOK_OFF_HOLD)
.to(connected)
.build();

Expand All @@ -24,21 +24,8 @@ Define the State Machine by subclassing State Machine and using the builders in
### Phone State Machine Diagram
Inspired by [simplestatemachine](http://simplestatemachine.codeplex.com/)

**Phone State Machine** (Generated from code by [Graphviz](http://www.graphviz.org/))
Generated from code by [Graphviz](http://www.graphviz.org/) - Usage: dot -o outputfile -Tbmp inputfile.dot

![Phone State Machine Diagram](PhoneStateMachine.bmp "Phone State Machine Diagram")

### ATM State Machine Diagram
Inspired by [ATM Bank](http://www.uml-diagrams.org/bank-atm-uml-state-machine-diagram-example.html)

**ATM State Machine** (Generated from code by [Graphviz](http://www.graphviz.org/))

![ATM State Machine](ATMStateMachine.bmp)
See: [Graphviz command-line-invocation](http://www.graphviz.org/content/command-line-invocation)

The composite state **ServingCustomer**

![Serving Customer](ServingCustomer.bmp)

**A model containing the ATM state machine and the inner states of the composite state** (not generated from code)

![ATM State Machine Diagram](ATMStateMachineDiagram.png)
![Phone State Machine Diagram](PhoneStateMachine.bmp "Phone State Machine Diagram")
Binary file removed ServingCustomer.bmp
Binary file not shown.
10 changes: 5 additions & 5 deletions pom.xml
Expand Up @@ -10,10 +10,10 @@
<name>Machine Process</name>

<properties>
<assertj.version>3.0.0</assertj.version>
<assertj.version>3.2.0</assertj.version>
<junit.version>4.12</junit.version>
<jdk.version>1.8</jdk.version>
<logback.version>1.1.2</logback.version>
<jdk.version>1.7</jdk.version>
<logback.version>1.1.3</logback.version>
<slf4j.version>1.7.12</slf4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Expand Down Expand Up @@ -61,12 +61,12 @@
<configuration>
<repoToken>jxpXA5E9hYylQJWwgh1uZbcpVMMHzPiTT</repoToken>
</configuration>
<version>3.1.0</version>
<version>4.0.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.4.201502262128</version>
<version>0.7.5.201505241946</version>
<executions>
<execution>
<id>prepare-agent</id>
Expand Down
1 change: 0 additions & 1 deletion src/main/java/org/nextstate/statemachine/Action.java
@@ -1,6 +1,5 @@
package org.nextstate.statemachine;

@FunctionalInterface
public interface Action {
void perform();
}
58 changes: 33 additions & 25 deletions src/main/java/org/nextstate/statemachine/SimpleState.java
Expand Up @@ -2,16 +2,15 @@

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SimpleState implements State {
private final Logger log = LoggerFactory.getLogger(this.getClass());

protected Optional<Action> entry = Optional.empty();
protected Optional<Action> exit = Optional.empty();
protected Action entry = null;
protected Action exit = null;

protected final String name;
protected final List<Transition> transitions = new ArrayList<>();
Expand All @@ -33,42 +32,51 @@ public void addTransitions(List<Transition> transitionList) {
}

@Override public void onEntry() {
entry.ifPresent(Action::perform);
if (entry != null) {
entry.perform();
}
}

@Override public void onExit() {
exit.ifPresent(Action::perform);
if (exit != null) {
exit.perform();
}
}

@Override public Optional<State> execute(String event) {
@Override public State execute(String event) {
return stateTransition(event);
}

public Optional<State> stateTransition(String event) {
public State stateTransition(String event) {

Optional<Transition> matchedTransition = transitions.stream()
.filter(t -> t.guardEvent == null || t.guardEvent.equals(event))
.findFirst();
Transition matchedTransition = null;
for (Transition t : transitions) {
if (t.guardEvent == null || t.guardEvent.equals(event)) {
matchedTransition = t;
}
}

log.debug("execute - {} event: {} ", name, event, (matchedTransition.isPresent() ?
" transition to state: " + matchedTransition.get().getTargetState().getName() :
log.debug("stateTransition state:{} - event:{} {}", name, event, (matchedTransition != null ?
" transition to state: " + matchedTransition.getTargetState().getName() :
" no transition match."));

if (!matchedTransition.isPresent()) {
log.debug("execute - No transition match event: {} in state: {}", event, name);
return Optional.empty();
if (matchedTransition != null) {
if (matchedTransition.onTransition != null) {
log.debug("stateTransition - Transition match event: {} in state: {}", event, name);
matchedTransition.onTransition.perform();
}
return matchedTransition.getTargetState();
}

Transition transition = matchedTransition.get();
transition.onTransition.ifPresent(Action::perform);
return Optional.ofNullable(transition.getTargetState());
return null;
}

public boolean transitionToFinalState() {
Optional<Transition> transition = transitions.stream().filter(
t -> t.guardEvent.equals(FinalState.FINAL_EVENT)).findFirst();

return transition.isPresent() && (transition.get().getTargetState() instanceof FinalState);
for (Transition t : transitions) {
if (t.guardEvent.equals(FinalState.FINAL_EVENT)) {
return t.getTargetState() instanceof FinalState;
}
}
return false;
}

@Override public void toDot(StringBuilder sb) {
Expand Down Expand Up @@ -112,12 +120,12 @@ public Transition.TransitionBuilder<StateBuilder> transition(String name) {
}

public StateBuilder onEntry(Action action) {
state.entry = Optional.ofNullable(action);
state.entry = action;
return this;
}

public StateBuilder onExit(Action action) {
state.exit = Optional.ofNullable(action);
state.exit = action;
return this;
}

Expand Down
4 changes: 1 addition & 3 deletions src/main/java/org/nextstate/statemachine/State.java
@@ -1,14 +1,12 @@
package org.nextstate.statemachine;

import java.util.Optional;

public interface State {

String getName();

boolean transitionToFinalState();

Optional<State> execute(String event);
State execute(String event);

void onEntry();

Expand Down
28 changes: 16 additions & 12 deletions src/main/java/org/nextstate/statemachine/StateMachine.java
Expand Up @@ -2,7 +2,6 @@

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -56,12 +55,15 @@ public String getActiveStateConfiguration() {

public void activeStateConfiguration(String stateName) {

Optional<State> state = states.stream().filter(s -> stateName.equals(s.getName())).findFirst();
state.orElseThrow(() -> new IllegalStateException("No state named " + stateName
+ " exists. Add all states to the StateMachine before setting active state configuration."));

activeState = state.get();
log.debug("activeStateConfiguration - active state: {}", activeState.getName());
for (State state : states) {
if (state.getName().equals(stateName)) {
activeState = state;
log.debug("activeStateConfiguration - active state: {}", activeState.getName());
return;
}
}
throw new IllegalStateException("No state named " + stateName
+ " exists. Add all states to the StateMachine before setting active state configuration.");
}

public void execute(String event) {
Expand All @@ -70,12 +72,12 @@ public void execute(String event) {
}
log.debug("execute - activeState: " + activeState.getName() + " event: " + event);

Optional<State> state = activeState.execute(event);
State state = activeState.execute(event);

// Check if new active state and execute onExit on the old and onEntry on the new ...
if (state.isPresent()) {
if (state != null) {
activeState.onExit();
activeState = state.get();
activeState = state;
activeState.onEntry();
log.debug("execute - new active state: {}", activeState.getName());
}
Expand Down Expand Up @@ -109,15 +111,17 @@ public String toDot(boolean showActiveState) {

getActiveState().toDot(sb);

getStates().stream().filter(state -> state != getActiveState()).forEach(state -> {
for (State state :states){
if (state != activeState) {
sb.append(state.getName().replaceAll("\\s+", "_"));
sb.append("[label=\"");
sb.append(state.getName());
sb.append("\"");
sb.append("];");
sb.append(System.lineSeparator());
state.toDot(sb);
});
}
}
sb.append("} ");
sb.append(System.lineSeparator());
return sb.toString();
Expand Down
5 changes: 2 additions & 3 deletions src/main/java/org/nextstate/statemachine/Transition.java
Expand Up @@ -2,13 +2,12 @@

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

public class Transition {
private final String name;
private State targetState;
protected String guardEvent;
protected Optional<Action> onTransition;
protected Action onTransition;

public Transition(String guardEvent, State state, String name, Action onTransition) {
if (state == null) {
Expand All @@ -20,7 +19,7 @@ public Transition(String guardEvent, State state, String name, Action onTransiti
this.guardEvent = guardEvent;
this.targetState = state;
this.name = name;
this.onTransition = Optional.ofNullable(onTransition);
this.onTransition = onTransition;
}

public State getTargetState() {
Expand Down
41 changes: 22 additions & 19 deletions src/test/java/org/nextstate/statemachine/PhoneStateMachine.java
Expand Up @@ -64,41 +64,44 @@ class PhoneStateMachine extends StateMachine {
private static final String TAKE_OFF_HOLD = "TakeOffHold";
private static final String HURL_PHONE = "HurlPhone";

{
Action playMuzak = () -> System.out.println("PlayMuzak");
Action stopMuzak = () -> System.out.println("StopMuzak");
Action playMuzak = new Action() {
@Override public void perform() {
System.out.println("PlayMuzak");
}
};
Action stopMuzak = new Action() {
@Override public void perform() {
System.out.println("StopMuzak");
}
};

{
SimpleState offHook = state(OFF_HOOK)
.onEntry(() -> System.out.println("In Sysout Action!")).build();
.onEntry(new Action() {
@Override public void perform() {
System.out.println("In Sysout Action!");
}}).build();
State phoneDestroyed = state(PHONE_DESTROYED).build();
SimpleState connected = state(CONNECTED).build();
State onHold = state(ON_HOLD)
.transition(HURL_PHONE).guardedBy(PHONE_HURLED_AGAINST_WALL)
.to(phoneDestroyed)
.transition(HANG_UP).guardedBy(HUNG_UP)
.to(offHook)
.transition(TAKE_OFF_HOLD).onTransition(stopMuzak)
.guardedBy(TOOK_OFF_HOLD)
.transition(TAKE_OFF_HOLD).onTransition(stopMuzak).guardedBy(TOOK_OFF_HOLD)
.to(connected)
.build();
connected.addTransitions(transitions()
.transition(PLACE_ON_HOLD)
.onTransition(playMuzak).guardedBy(PLACED_ON_HOLD)
.to(onHold)
.transition(HANG_UP).guardedBy(HUNG_UP)
.to(offHook)
.transition(LEAVE_MESSAGE).guardedBy(MESSAGE_LEFT)
.to(offHook)
.transition(PLACE_ON_HOLD).onTransition(playMuzak).guardedBy(PLACED_ON_HOLD).to(onHold)
.transition(HANG_UP).guardedBy(HUNG_UP).to(offHook)
.transition(LEAVE_MESSAGE).guardedBy(MESSAGE_LEFT).to(offHook)
.build());
State ringing = state(RINGING)
.transition(CONNECT_CALL).guardedBy(CALL_CONNECTED)
.to(connected)
.transition(HANG_UP).guardedBy(HUNG_UP)
.to(offHook)
.transition(CONNECT_CALL).guardedBy(CALL_CONNECTED).to(connected)
.transition(HANG_UP).guardedBy(HUNG_UP).to(offHook)
.build();
offHook.addTransitions(transitions()
.transition(CALL_DIAL).guardedBy(CALL_DIALED)
.to(ringing).build());
.transition(CALL_DIAL).guardedBy(CALL_DIALED).to(ringing).build());
addStates(Arrays.asList(offHook, phoneDestroyed, onHold, connected, ringing));
activeState(offHook);
validate();
Expand Down
Expand Up @@ -15,6 +15,7 @@ public void one_transition() {
// Given
StateMachine phone = new PhoneStateMachine();

System.out.println(phone.toDot(false));
// When
phone.execute(PhoneStateMachine.CALL_DIALED);

Expand Down
8 changes: 3 additions & 5 deletions src/test/java/org/nextstate/statemachine/StateTest.java
Expand Up @@ -3,8 +3,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.nextstate.statemachine.Transition.transitions;

import java.util.Optional;

import org.junit.Test;

public class StateTest {
Expand All @@ -20,10 +18,10 @@ public void transition_match() {
.transition(CALL_DIAL).guardedBy(CALL_DIALED)
.to(to).build());

Optional<State> target = from.execute(CALL_DIALED);
State target = from.execute(CALL_DIALED);

assertThat(target.isPresent());
assertThat(target.get().getName()).isEqualTo("To");
assertThat(target).isNotNull();
assertThat(target.getName()).isEqualTo("To");
}

@Test
Expand Down

0 comments on commit b170f90

Please sign in to comment.