Fix history state reset

- Fixing a persist/restore bug which failed to properly
  reset history state via StateMachineContext history mappings.
- Effectively caused by too early `break` from a loop resetting
  states where history reset needs to check all states to find
  substate machines.
- Ported test given with a #188.
- Relates to #188
This commit is contained in:
Janne Valkealahti
2016-03-22 12:07:18 +00:00
parent 936be90bf4
commit a82ff5b936
2 changed files with 330 additions and 31 deletions

View File

@@ -509,45 +509,15 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
S state = stateMachineContext.getState();
boolean stateSet = false;
// handle state reset
for (State<S, E> s : getStates()) {
for (State<S, E> ss : s.getStates()) {
if (state != null && ss.getIds().contains(state)) {
currentState = s;
// setting history for 'this' machine
if (history != null && stateMachineContext.getHistoryStates() != null) {
State<S, E> h = null;
for (State<S, E> hh : getStates()) {
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(null))) {
h = hh;
break;
}
}
if (h != null) {
((HistoryPseudoState<S, E>)history).setState(h);
}
}
// TODO: not sure about starting submachine/regions here, though
// needed if we only transit to super state or reset regions
if (s.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)s).getSubmachine();
// setting history for submachine state machine
PseudoState<S, E> submachineHistory = ((AbstractStateMachine<S, E>)submachine).getHistoryState();
if (submachineHistory != null) {
State<S, E> h = null;
for (State<S, E> hh : submachine.getStates()) {
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(s.getId()))) {
h = hh;
break;
}
}
if (h != null) {
((HistoryPseudoState<S, E>)submachineHistory).setState(h);
}
}
for (final StateMachineContext<S, E> child : stateMachineContext.getChilds()) {
submachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
@@ -606,6 +576,41 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
break;
}
}
// handle history reset here as above state reset loop breaks out
if (history != null && stateMachineContext.getHistoryStates() != null) {
// setting history for 'this' machine
State<S, E> h = null;
for (State<S, E> hh : getStates()) {
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(null))) {
h = hh;
break;
}
}
if (h != null) {
((HistoryPseudoState<S, E>) history).setState(h);
}
}
for (State<S, E> s : getStates()) {
// setting history for 'submachines'
if (s.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>) s).getSubmachine();
PseudoState<S, E> submachineHistory = ((AbstractStateMachine<S, E>) submachine).getHistoryState();
if (submachineHistory != null) {
State<S, E> h = null;
for (State<S, E> hh : submachine.getStates()) {
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(s.getId()))) {
h = hh;
break;
}
}
if (h != null) {
((HistoryPseudoState<S, E>) submachineHistory).setState(h);
}
}
}
}
if (stateSet && stateMachineContext.getExtendedState() != null) {
this.extendedState = stateMachineContext.getExtendedState();
}

View File

@@ -0,0 +1,294 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.persist;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.configurers.StateConfigurer;
public class StateMachinePersistTests2 extends AbstractStateMachineTests {
// States
public static final String RECORD_AWAITING_CONTENT = "RECORD_AWAITING_CONTENT";
public static final String RECORD_LOGGING_ACTIVE = "RECORD_LOGGING_ACTIVE";
public static final String RECORD_LOGGED = "RECORD_LOGGED";
public static final String RECORD_DISCARDED = "RECORD_DISCARDED";
public static final String RECORD_LOGGING_ON_HOLD_WITH_ERROR = "RECORD_LOGGING_ON_HOLD_WITH_ERROR";
public static final String RECORD_LOGGING_IN_PROGRESS = "RECORD_LOGGING_IN_PROGRESS";
public static final String RECORD_AWAITING_LOGGING = "RECORD_AWAITING_LOGGING";
public static final String RECORD_LOGGING_ON_HOLD = "RECORD_LOGGING_ON_HOLD";
public static final String HISTORY = "HISTORY";
// Events
public static final String UPLOAD_RECORD = "UPLOAD_RECORD";
public static final String SUSPEND_RECORD_LOGGING_WITH_ERROR = "SUSPEND_RECORD_LOGGING_WITH_ERROR";
public static final String DISCARD_RECORD = "DISCARD_RECORD";
public static final String CANCEL_RECORD_LOGGING = "CANCEL_RECORD_LOGGING";
public static final String LOG_RECORD = "LOG_RECORD";
public static final String SUSPEND_RECORD_LOGGING = "SUSPEND_RECORD_LOGGING";
public static final String START_LOGGING_RECORD = "START_LOGGING_RECORD";
public static final String RESUME_RECORD_LOGGING = "RESUME_RECORD_LOGGING";
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@SuppressWarnings("unchecked")
@Test
public void testVariousPersistOperations() throws Exception {
context.register(Config1.class);
context.refresh();
StateMachinePersist<String, String, String> stateMachinePersist = new InMemoryStateMachinePersist();
StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<String, String, String>(
stateMachinePersist);
StateMachineFactory<String, String> factory = context.getBean("LOG_RECORD", StateMachineFactory.class);
StateMachine<String,String> m = factory.getStateMachine();
assertThat(m.getState().getId(), equalTo(RECORD_AWAITING_CONTENT));
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(UPLOAD_RECORD), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[] { RECORD_LOGGING_ACTIVE, RECORD_AWAITING_LOGGING }));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_AWAITING_LOGGING}));
persister.persist(m, "xxx");
assertThat(m.sendEvent(SUSPEND_RECORD_LOGGING), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_LOGGING_ON_HOLD}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_LOGGING_ON_HOLD}));
persister.persist(m, "xxx");
assertThat(m.sendEvent(START_LOGGING_RECORD), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_IN_PROGRESS}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(CANCEL_RECORD_LOGGING), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_LOGGING_ON_HOLD}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(RESUME_RECORD_LOGGING), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_AWAITING_LOGGING}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(START_LOGGING_RECORD), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_IN_PROGRESS}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(CANCEL_RECORD_LOGGING), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_AWAITING_LOGGING}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(SUSPEND_RECORD_LOGGING_WITH_ERROR), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ON_HOLD_WITH_ERROR}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(RESUME_RECORD_LOGGING), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_ACTIVE, RECORD_AWAITING_LOGGING}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(START_LOGGING_RECORD), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGING_IN_PROGRESS}));
persister.persist(m, "xxx");
m = loadStateMachine(factory, persister, "xxx");
assertThat(m.sendEvent(LOG_RECORD), is(true));
assertThat(m.getState().getIds(), containsInAnyOrder(new String[]{RECORD_LOGGED}));
persister.persist(m, "xxx");
}
private static StateMachine<String, String> loadStateMachine(StateMachineFactory<String, String> factory,
StateMachinePersister<String, String, String> persister, final String id) throws Exception {
StateMachine<String, String> stateMachine = factory.getStateMachine();
persister.restore(stateMachine, id);
return stateMachine;
}
@Configuration
@EnableStateMachineFactory(name = "LOG_RECORD", contextEvents = false)
static class Config1 extends StateMachineConfigurerAdapter<String,String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String,String> config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<String,String> states) throws Exception {
states.withStates()
.initial(RECORD_AWAITING_CONTENT)
.state(RECORD_LOGGING_ON_HOLD_WITH_ERROR)
.state(RECORD_LOGGING_ACTIVE)
.state(RECORD_LOGGING_IN_PROGRESS)
.end(RECORD_LOGGED)
.end(RECORD_DISCARDED)
.and()
.withStates()
.parent(RECORD_LOGGING_ACTIVE)
.initial(RECORD_AWAITING_LOGGING)
.state(RECORD_LOGGING_ON_HOLD)
.history(HISTORY, StateConfigurer.History.DEEP);
}
@Override
public void configure(StateMachineTransitionConfigurer<String,String> transitions) throws Exception {
transitions
/* ****************************************************************************** */
/* FROM RECORD_AWAITING_CONTENT */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_AWAITING_CONTENT)
.event(UPLOAD_RECORD)
.target(RECORD_LOGGING_ACTIVE)
.and()
.withExternal()
.source(RECORD_AWAITING_CONTENT)
.event(SUSPEND_RECORD_LOGGING_WITH_ERROR)
.target(RECORD_LOGGING_ON_HOLD_WITH_ERROR)
.and()
.withExternal()
.source(RECORD_AWAITING_CONTENT)
.event(DISCARD_RECORD)
.target(RECORD_DISCARDED)
.and()
/* ****************************************************************************** */
/* FROM RECORD_LOGGING_ACTIVE */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_LOGGING_ACTIVE)
.event(SUSPEND_RECORD_LOGGING_WITH_ERROR)
.target(RECORD_LOGGING_ON_HOLD_WITH_ERROR)
.and()
.withExternal()
.source(RECORD_LOGGING_ACTIVE)
.event(DISCARD_RECORD)
.target(RECORD_DISCARDED)
.and()
/* ****************************************************************************** */
/* FROM RECORD_LOGGING_IN_PROGRESS */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_LOGGING_IN_PROGRESS)
.event(CANCEL_RECORD_LOGGING)
.target(HISTORY)
.and()
.withExternal()
.source(RECORD_LOGGING_IN_PROGRESS)
.event(DISCARD_RECORD)
.target(RECORD_DISCARDED)
.and()
.withExternal()
.source(RECORD_LOGGING_IN_PROGRESS)
.event(LOG_RECORD)
.target(RECORD_LOGGED)
.and()
/* ****************************************************************************** */
/* FROM RECORD_AWAITING_LOGGING */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_AWAITING_LOGGING)
.event(SUSPEND_RECORD_LOGGING)
.target(RECORD_LOGGING_ON_HOLD)
.and()
.withExternal()
.source(RECORD_AWAITING_LOGGING)
.event(START_LOGGING_RECORD)
.target(RECORD_LOGGING_IN_PROGRESS)
.and()
/* ****************************************************************************** */
/* FROM RECORD_LOGGING_ON_HOLD */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_LOGGING_ON_HOLD)
.event(START_LOGGING_RECORD)
.target(RECORD_LOGGING_IN_PROGRESS)
.and()
.withExternal()
.source(RECORD_LOGGING_ON_HOLD)
.event(RESUME_RECORD_LOGGING)
.target(RECORD_AWAITING_LOGGING)
.and()
/* ****************************************************************************** */
/* FROM RECORD_LOGGING_ON_HOLD_WITH_ERROR */
/* ****************************************************************************** */
.withExternal()
.source(RECORD_LOGGING_ON_HOLD_WITH_ERROR)
.event(RESUME_RECORD_LOGGING)
.target(HISTORY)
.and()
.withExternal()
.source(RECORD_LOGGING_ON_HOLD_WITH_ERROR)
.event(DISCARD_RECORD)
.target(RECORD_DISCARDED)
.and();
}
}
static class InMemoryStateMachinePersist implements StateMachinePersist<String, String, String> {
private final Map<Object,StateMachineContext<String, String>> contexts = new HashMap<>();
@Override
public void write(StateMachineContext<String, String> stateMachineContext, String contextOjb) throws Exception {
contexts.put(contextOjb,stateMachineContext);
}
@Override
public StateMachineContext<String, String> read(String contextOjb) throws Exception {
return contexts.get(contextOjb);
}
}
}