I'm introducing Spring Statemachine into an existing project, with the hope of amalgamating and clarifying our business logic. We have various JPA entities with interconnected states and I'm having some trouble with setting a persisted state as the current state of an existing state machine.
I'm using a StateMachineFactory to create a new StateMachine instance for each entity instance. I'm storing the current state of the StateMachine in a separate field for Hibernate to persist and ideally need to sync the value of the persisted field with the StateMachine. My question is around how this should be typically achieved in Spring Statemachine.
@Entity
@EntityListeners(MyEntityListener.class)
public class MyEntity {
@Column
private MyState internalState; // Using AttributeConverter
@Transient
private StateMachine<MyState, Event> stateMachine;
}
public class MyEntityListener {
@PostLoad
public void postLoad(MyEntity entity) {
// TODO Set StateMachine's current state to entity's internal state
);
}
One approach may be to define local transitions to move the initial state into the persisted state. I could then do a conditional check to find an event tied to a local transition, which would move the source state into the target state. This seems a little messy to me and I'd like to keep my state machine's configuration as clean as possible.
I can't see how I can set the StateMachine's current state through a public API without moving through a transition and so another approach I explored is to wrap the StateMachine instance to expose the following method (as it's conveniently default scope):
package org.springframework.statemachine.support;
public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSupport<S, E> implements StateMachine<S, E>, StateMachineAccess<S, E> {
void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine)
}
package org.springframework.statemachine.support;
public class MyStateMachineWrapper<S, E> {
private AbstractStateMachine<S, E> stateMachine;
public MyStateMachineWrapper(StateMachine<S, E> stateMachine) {
if (stateMachine instanceof AbstractStateMachine) {
this.stateMachine = (AbstractStateMachine<S, E>)stateMachine;
} else {
throw new IllegalArgumentException("Provided StateMachine is not a valid type");
}
}
public void setCurrentState(S status) {
stateMachine.setCurrentState(findState(status), null, null, false, stateMachine);
}
private State<S, E> findState(S status) {
for (State<S, E> state : stateMachine.getStates()) {
if (state.getId() == status) {
return state;
}
}
throw new IllegalArgumentException("Specified status does not equate to valid State");
}
}
I could then throw the following code into MyEntityListener.postLoad:
MyStateMachineWrapper<MyState, Event> myStateMachineWrapper = new MyStateMachineWrapper<>(entity.getStateMachine());
myStateMachineWrapper.setCurrentState(entity.getInternalState());
The above approach seems to work fine but I can't imagine this is how it was envisioned to work. Surely there's a cleaner method to achieve this or maybe the project isn't mature enough and doesn't include this functionality yet?
Thanks for any thoughts and opinions.
I've cleaned up option #2 above, changing the wrapper class to a utils class. To be clear, this approach takes advantage of the setCurrentState method having a default accessor and so this may end up being a brittle solution.
package org.springframework.statemachine.support;
public abstract class MyStateMachineUtils extends StateMachineUtils {
public static <S, E> void setCurrentState(StateMachine<S, E> stateMachine, S state) {
if (stateMachine instanceof AbstractStateMachine) {
setCurrentState((AbstractStateMachine<S, E>)stateMachine, state);
} else {
throw new IllegalArgumentException("Provided StateMachine is not a valid type");
}
}
public static <S, E> void setCurrentState(AbstractStateMachine<S, E> stateMachine, S state) {
stateMachine.setCurrentState(findState(stateMachine, state), null, null, false, stateMachine);
}
private static <S, E> State<S, E> findState(AbstractStateMachine<S, E> stateMachine, S stateId) {
for (State<S, E> state : stateMachine.getStates()) {
if (state.getId() == stateId) {
return state;
}
}
throw new IllegalArgumentException("Specified State ID is not valid");
}
}
This can then be used quite nicely like so:
MyStateMachineUtils.setCurrentState(entity.getStateMachine(), entity.getInternalState());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With