Add base work for recipes

- Better state sync handling #35
- Adding first recipe for synching and
  persisting a state # 73
This commit is contained in:
Janne Valkealahti
2015-06-17 09:19:43 +01:00
parent 608183f2a3
commit 1bcdbd5ae0
22 changed files with 792 additions and 0 deletions

View File

@@ -23,3 +23,13 @@ project('spring-statemachine-samples-washer') {
project('spring-statemachine-samples-zookeeper') {
description = 'Spring State Machine Distributed Sample'
}
project('spring-statemachine-samples-persist') {
description = 'Spring State Machine Persist Sample'
dependencies {
compile project(":spring-statemachine-recipes-common")
compile ("org.hsqldb:hsqldb:2.3.1")
compile ("org.springframework:spring-jdbc:$springVersion")
}
}

View File

@@ -0,0 +1,19 @@
.gradle
bin
build
.settings
.classpath
.springBeans
.project
*.iml
*.ipr
*.iws
metastore_db
/samples/pig-scripting/src/main/resources/ml-100k.zip
/samples/pig-scripting/src/main/resources/ml-100k/u.data
/src/test/resources/s3.properties
/.idea/
.DS_Store
/out/
target
*.log

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2015 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 demo.persist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.Bootstrap;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler;
@SpringBootApplication
public class Application {
//tag::snippetA[]
@Configuration
@EnableStateMachine
static class StateMachineConfig
extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("PLACED")
.state("PROCESSING")
.state("SENT")
.state("DELIVERED");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("PLACED").target("PROCESSING")
.event("PROCESS")
.and()
.withExternal()
.source("PROCESSING").target("SENT")
.event("SEND")
.and()
.withExternal()
.source("SENT").target("DELIVERED")
.event("DELIVER");
}
}
//end::snippetA[]
@Configuration
static class PersistHandlerConfig {
@Autowired
private StateMachine<String, String> stateMachine;
@Bean
public Persist persist() {
return new Persist(persistStateMachineHandler());
}
@Bean
public PersistStateMachineHandler persistStateMachineHandler() {
return new PersistStateMachineHandler(stateMachine);
}
}
public static class Order {
int id;
String state;
public Order(int id, String state) {
this.id = id;
this.state = state;
}
@Override
public String toString() {
return "Order [id=" + id + ", state=" + state + "]";
}
}
public static void main(String[] args) throws Exception {
Bootstrap.main(args);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2015 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 demo.persist;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler;
import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler.PersistStateChangeListener;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import demo.persist.Application.Order;
public class Persist {
private final PersistStateMachineHandler handler;
@Autowired
private JdbcTemplate jdbcTemplate;
private final PersistStateChangeListener listener = new LocalPersistStateChangeListener();
public Persist(PersistStateMachineHandler handler) {
this.handler = handler;
this.handler.addPersistStateChangeListener(listener);
}
public String listDbEntries() {
List<Order> orders = jdbcTemplate.query(
"select id, state from orders",
new RowMapper<Order>() {
public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Order(rs.getInt("id"), rs.getString("state"));
}
});
StringBuilder buf = new StringBuilder();
for (Order order : orders) {
buf.append(order);
buf.append("\n");
}
return buf.toString();
}
public void change(int order, String event) {
Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?", new Object[]{order}, new RowMapper<Order>() {
public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Order(rs.getInt("id"), rs.getString("state"));
}
});
handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.state);
}
private class LocalPersistStateChangeListener implements PersistStateChangeListener {
@Override
public void onPersist(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
if (message != null && message.getHeaders().containsKey("order")) {
Integer order = message.getHeaders().get("order", Integer.class);
jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order);
}
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2015 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 demo.persist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
@Component
public class PersistCommands implements CommandMarker {
@Autowired
private Persist persist;
@CliCommand(value = "persist db", help = "List entries from db")
public String listDbEntries() {
return persist.listDbEntries();
}
@CliCommand(value = "persist process", help = "Process order")
public void process(@CliOption(key = {"", "id"}, help = "Order id") int order) {
persist.change(order, "PROCESS");
}
@CliCommand(value = "persist send", help = "Send order")
public void send(@CliOption(key = {"", "id"}, help = "Order id") int order) {
persist.change(order, "SEND");
}
@CliCommand(value = "persist deliver", help = "Deliver order")
public void deliver(@CliOption(key = {"", "id"}, help = "Order id") int order) {
persist.change(order, "DELIVER");
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2015 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 demo.persist;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
import demo.AbstractStateMachineCommands;
@Component
public class StateMachineCommands extends AbstractStateMachineCommands<String, String> {
@CliCommand(value = "sm event", help = "Sends an event to a state machine")
public String event(@CliOption(key = { "", "event" }, mandatory = true, help = "The event") final String event) {
getStateMachine().sendEvent(event);
return "Event " + event + " send";
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="demo" />
</beans>

View File

@@ -0,0 +1,4 @@
insert into orders (id, state) values (1, 'PLACED');
insert into orders (id, state) values (2, 'PROCESSING');
insert into orders (id, state) values (3, 'SENT');
insert into orders (id, state) values (4, 'DELIVERED');

View File

@@ -0,0 +1,4 @@
create table orders (
id int,
state varchar(256)
);

View File

@@ -0,0 +1,18 @@
+---------------------------------------------------------------+
| SM |
+---------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| *-->| PLACED | | PROCESSING | |
| +----------------+ PROCESS +----------------+ SEND |
| | |---------->| |-----+ |
| +----------------+ +----------------+ | |
| | |
| | |
| +----------------+ +----------------+ | |
| | SENT | | DELIVERED | | |
| +----------------+ DELIVER +----------------+ | |
| | |<----------| |<----+ |
| +----------------+ +----------------+ |
| |
+---------------------------------------------------------------+

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2015 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 demo.persist;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import demo.CommonConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
@SpringApplicationConfiguration(classes = { CommonConfiguration.class, Application.class, StateMachineCommands.class })
public class PersistTests {
@Autowired
private StateMachineCommands commands;
@Autowired
private StateMachine<String, String> machine;
@Autowired
private Persist persist;
@Test
public void testNotStarted() throws Exception {
assertThat(commands.state(), is("No state"));
}
@Test
public void testInitialState() throws Exception {
TestListener listener = new TestListener();
machine.addStateListener(listener);
machine.start();
assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true));
assertThat(listener.stateEnteredLatch.await(3, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains("PLACED"));
assertThat(listener.statesEntered.size(), is(1));
assertThat(listener.statesEntered.get(0).getId(), is("PLACED"));
assertThat(listener.statesExited.size(), is(0));
}
@Test
public void testInitialDbList() {
// dataOrder [id=1, state=PLACED]Order [id=2, state=PROCESSING]Order [id=3, state=SENT]Order [id=4, state=DELIVERED]
assertThat(persist.listDbEntries(), containsString("PLACED"));
}
@Test
public void testUpdate1() {
persist.change(1, "PROCESS");
assertThat(persist.listDbEntries(), containsString("id=1, state=PROCESSING"));
}
@Test
public void testUpdate2() {
persist.change(2, "SEND");
assertThat(persist.listDbEntries(), containsString("id=2, state=SENT"));
}
private static class TestListener extends StateMachineListenerAdapter<String, String> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile CountDownLatch stateEnteredLatch = new CountDownLatch(1);
volatile CountDownLatch stateExitedLatch = new CountDownLatch(0);
volatile CountDownLatch transitionLatch = new CountDownLatch(0);
volatile List<Transition<String, String>> transitions = new ArrayList<Transition<String, String>>();
List<State<String, String>> statesEntered = new ArrayList<State<String, String>>();
List<State<String, String>> statesExited = new ArrayList<State<String, String>>();
volatile int transitionCount = 0;
@Override
public void stateChanged(State<String, String> from, State<String, String> to) {
stateChangedLatch.countDown();
}
@Override
public void stateEntered(State<String, String> state) {
statesEntered.add(state);
stateEnteredLatch.countDown();
}
@Override
public void stateExited(State<String, String> state) {
statesExited.add(state);
stateExitedLatch.countDown();
}
@Override
public void transition(Transition<String, String> transition) {
transitions.add(transition);
transitionCount++;
transitionLatch.countDown();
}
public void reset(int c1, int c2, int c3) {
reset(c1, c2, c3, 0);
}
public void reset(int c1, int c2, int c3, int c4) {
stateChangedLatch = new CountDownLatch(c1);
stateEnteredLatch = new CountDownLatch(c2);
stateExitedLatch = new CountDownLatch(c3);
transitionLatch = new CountDownLatch(c4);
statesEntered.clear();
statesExited.clear();
transitionCount = 0;
transitions.clear();
}
}
}