Skip to content

Commit eb6c5b2

Browse files
Merge pull request #11 from aljoshakoecher/prepare-for-release
New release 1.2.0 bringing StateChangeObservers
2 parents 452ddfc + 728b121 commit eb6c5b2

5 files changed

Lines changed: 167 additions & 6 deletions

File tree

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,19 @@ You can pass in an action and the name of an active state to add this action to
133133

134134

135135
### Getting notified on state changes
136-
Work in progress, coming soon
136+
You can create an observer that is notified whenever the state machine changes its state. To do that, you have to create your observer implementing the IStateChangeOberser interface. This interface's method is called on every state change, you can do whatever you like in this function. Here's an example for such a class:
137+
138+
```java
139+
class ExampleObserver implements IStateChangeObserver {
140+
141+
@Override
142+
public void onStateChanged(IState newState) {
143+
System.out.println("State has changed, new State is: " + newState.getClass().getSimpleName());
144+
}
145+
};
146+
```
147+
148+
To add a new observer to the state machine simply call `stateMachine.addStateChangeObserver(IStateChangeObserver observer)` passing in an instance of you observer class. In case an observer should no longer be notified on state changes, simply remove it by calling `stateMachine.removeStateChangeObserver(IStateChangeObserver observer)`.
137149

138150

139151
## Usage
@@ -146,7 +158,7 @@ Releases can be found on the Maven Central repository. Just add this dependency
146158
<dependency>
147159
<groupId>com.github.aljoshakoecher</groupId>
148160
<artifactId>isa88-state-machine</artifactId>
149-
<version>1.1.0</version>
161+
<version>1.2.0</version>
150162
</dependency>
151163
```
152164

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>com.github.aljoshakoecher</groupId>
66
<artifactId>isa88-state-machine</artifactId>
7-
<version>1.1.1</version>
7+
<version>1.2.0</version>
88
<packaging>bundle</packaging>
99

1010
<name>ISA 88 State Machine</name>
@@ -108,7 +108,7 @@
108108
<plugin>
109109
<groupId>org.apache.maven.plugins</groupId>
110110
<artifactId>maven-javadoc-plugin</artifactId>
111-
<version>2.9.1</version>
111+
<version>3.2.0</version>
112112
<executions>
113113
<execution>
114114
<id>attach-javadocs</id>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package statemachine;
2+
3+
import states.IState;
4+
5+
/**
6+
* Defines an Observer that can be attached to a state machine in order to get notified on state changes
7+
*/
8+
public interface IStateChangeObserver {
9+
10+
/**
11+
* Gets called every time the state of a state machine changes
12+
* @param newState The new state that the state machine is in.
13+
*/
14+
public void onStateChanged(IState newState);
15+
}

src/main/java/statemachine/StateMachine.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package statemachine;
22

3+
import java.util.ArrayList;
4+
35
import states.State;
46
import states.TransitionName;
57

68
public class StateMachine {
79

810
private State currentState;
911
private StateActionManager stateActionManager = new StateActionManager();
12+
private ArrayList<IStateChangeObserver> stateChangeObservers = new ArrayList<>();
1013

1114
/**
1215
* Instantiates a new {@link StateMachine} with the a given initial state
@@ -16,9 +19,10 @@ public class StateMachine {
1619
StateMachine(State initialState) {
1720
this.currentState = initialState;
1821
}
19-
22+
2023
/**
2124
* Invokes a transition on the state machine.
25+
*
2226
* @param transitionName Name of the transition that shall be invoked.
2327
*/
2428
public void invokeTransition(TransitionName transitionName) {
@@ -105,7 +109,8 @@ public void stop() {
105109
}
106110

107111
/**
108-
* Execute an abort command. Can be used to transition from all 'normal' and 'stopping'-states to Aborted. Alias for invokeTransition(TransitionName.abort).
112+
* Execute an abort command. Can be used to transition from all 'normal' and 'stopping'-states to Aborted. Alias for
113+
* invokeTransition(TransitionName.abort).
109114
*/
110115
public void abort() {
111116
this.currentState.abort(this);
@@ -144,6 +149,10 @@ protected void setState(State state) {
144149
public void setStateAndRunAction(State state) {
145150
this.currentState = state;
146151

152+
for (IStateChangeObserver observer : stateChangeObservers) {
153+
observer.onStateChanged(state);
154+
}
155+
147156
new Thread(() -> {
148157
state.executeActionAndComplete(this);
149158
}).start();
@@ -158,4 +167,22 @@ public StateActionManager getStateActionManager() {
158167
return this.stateActionManager;
159168
}
160169

170+
/**
171+
* Adds a new {@link IStateChangeObserver} instance to the list of observers.
172+
*
173+
* @param observer The new observer to add.
174+
*/
175+
public void addStateChangeObserver(IStateChangeObserver observer) {
176+
this.stateChangeObservers.add(observer);
177+
}
178+
179+
/**
180+
* Removes a given {@link IStateChangeObserver} instance from the list of observers.
181+
*
182+
* @param observer The observer that is going to be removed.
183+
*/
184+
public void removeStateChangeObserver(IStateChangeObserver observer) {
185+
this.stateChangeObservers.remove(observer);
186+
}
187+
161188
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package observer;
2+
3+
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.Order;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.TestMethodOrder;
10+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
11+
12+
import statemachine.IStateChangeObserver;
13+
import statemachine.StateMachine;
14+
import statemachine.StateMachineBuilder;
15+
import states.IState;
16+
import states.IStateAction;
17+
18+
@TestMethodOrder(OrderAnnotation.class)
19+
class TestObserving {
20+
21+
static class ExampleObserver implements IStateChangeObserver {
22+
public String observedStateName;
23+
@Override
24+
public void onStateChanged(IState newState) {
25+
observedStateName = newState.getClass().getSimpleName();
26+
}
27+
};
28+
29+
private final static int dummyActionTime = 300;
30+
private static IStateAction dummyAction;
31+
private static StateMachine stateMachine;
32+
private static ExampleObserver firstObserver;
33+
private static ExampleObserver secondObserver;
34+
35+
@BeforeAll
36+
static void setUp() {
37+
firstObserver = new ExampleObserver();
38+
secondObserver = new ExampleObserver();
39+
// ExampleObserver secondObserver = new ExampleObserver();
40+
// Create a dummy action that just pauses the thread
41+
dummyAction = new IStateAction() {
42+
@Override
43+
public void execute() {
44+
try {
45+
Thread.sleep(dummyActionTime);
46+
} catch (InterruptedException e) {
47+
e.printStackTrace();
48+
}
49+
}
50+
};
51+
52+
StateMachineBuilder builder = new StateMachineBuilder();
53+
stateMachine = builder.withActionInAborting(dummyAction).withActionInClearing(dummyAction).withActionInCompleting(dummyAction)
54+
.withActionInExecute(dummyAction).withActionInHolding(dummyAction).withActionInResetting(dummyAction).withActionInStarting(dummyAction)
55+
.withActionInStopping(dummyAction).withActionInSuspending(dummyAction).withActionInUnholding(dummyAction).withActionInUnsuspending(dummyAction)
56+
.build();
57+
58+
}
59+
60+
@Test
61+
@Order(1)
62+
void addFirstObserverAndTestStart() {
63+
stateMachine.addStateChangeObserver(firstObserver);
64+
stateMachine.start();
65+
assertEquals(firstObserver.observedStateName, "StartingState", "Observer should be notified that the state machine is now in Starting");
66+
}
67+
68+
@Test
69+
@Order(2)
70+
void testResetWithFirstObserver() throws InterruptedException {
71+
Thread.sleep(dummyActionTime*4); // Wait for execution of starting, execute, completing + safetyTime
72+
stateMachine.reset();
73+
Thread.sleep(dummyActionTime*2); // Wait for execution of resetting + safetyTime
74+
assertEquals(firstObserver.observedStateName, "IdleState", "Observer should be notified that the state machine is now in Idle");
75+
}
76+
77+
78+
@Test
79+
@Order(3)
80+
void addSecondObserverAndTestStart() {
81+
stateMachine.addStateChangeObserver(secondObserver);
82+
stateMachine.start();
83+
assertEquals(secondObserver.observedStateName, "StartingState", "Second observer should be notified that the state machine is now in Starting");
84+
}
85+
86+
@Test
87+
@Order(4)
88+
void makeSureFirstObserverStillWorking() throws InterruptedException {
89+
Thread.sleep(dummyActionTime*4); // Wait for execution of starting, execute, completing + safetyTime
90+
assertEquals(firstObserver.observedStateName, "CompleteState", "First observer should have tracked changes and should now be in CompleteState");
91+
}
92+
93+
@Test
94+
@Order(5)
95+
void removeSecondObserverAndMakeSureFirstObserverStillWorking() throws InterruptedException {
96+
stateMachine.removeStateChangeObserver(secondObserver);
97+
stateMachine.reset();
98+
Thread.sleep(dummyActionTime*2); // Wait for execution of resetting + safetyTime
99+
assertEquals(firstObserver.observedStateName, "IdleState", "First observer should now be in IdleState");
100+
}
101+
102+
@Test
103+
@Order(6)
104+
void testSecondOberserverNoLongerNotified() throws InterruptedException {
105+
assertEquals(secondObserver.observedStateName, "CompleteState", "Second observer should not have been notified after removal and should still be in CompleteState");
106+
}
107+
}

0 commit comments

Comments
 (0)