Add base work for cdplayer sample

This commit is contained in:
Janne Valkealahti
2015-03-15 19:39:43 +00:00
parent 1396856bcf
commit 74af052b72
15 changed files with 656 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ include 'spring-statemachine-core'
include 'spring-statemachine-samples'
include 'spring-statemachine-samples:turnstile'
include 'spring-statemachine-samples:showcase'
include 'spring-statemachine-samples:cdplayer'
rootProject.children.find {
if (it.name == 'spring-statemachine-samples') {

View File

@@ -7,3 +7,8 @@ project('spring-statemachine-samples-turnstile') {
project('spring-statemachine-samples-showcase') {
description = 'Spring State Machine Showcase Sample'
}
project('spring-statemachine-samples-cdplayer') {
description = 'Spring State Machine CD Player Sample'
}

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,158 @@
package demo.cdplayer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.Bootstrap;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
@Configuration
public class Application {
//tag::snippetA[]
@Configuration
@EnableStateMachine
static class StateMachineConfig
extends EnumStateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.IDLE)
.state(States.IDLE)
.and()
.withStates()
.parent(States.IDLE)
.initial(States.CLOSED)
.state(States.CLOSED)
.state(States.OPEN)
.and()
.withStates()
.state(States.BUSY)
.and()
.withStates()
.parent(States.BUSY)
.initial(States.PLAYING)
.state(States.PLAYING)
.state(States.PAUSED);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.CLOSED).target(States.OPEN).event(Events.EJECT)
.and()
.withExternal()
.source(States.OPEN).target(States.CLOSED).event(Events.EJECT)
.and()
.withExternal()
.source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE)
.and()
.withInternal()
.source(States.PLAYING)
.timer(1000)
.and()
.withExternal()
.source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE)
.and()
.withExternal()
.source(States.BUSY).target(States.IDLE).event(Events.STOP)
.and()
.withExternal()
.source(States.IDLE).target(States.BUSY).event(Events.PLAY)
.action(playAction())
.guard(playGuard())
.and()
.withInternal()
.source(States.OPEN).event(Events.LOAD).action(loadAction());
}
@Bean
public Action<States, Events> loadAction() {
return new Action<States, Events>() {
@Override
public void execute(StateContext<States, Events> context) {
Object cd = context.getMessageHeader(Variables.CD);
context.getExtendedState().getVariables().put(Variables.CD, cd);
}
};
}
@Bean
public Action<States, Events> playAction() {
return new Action<States, Events>() {
@Override
public void execute(StateContext<States, Events> context) {
context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l);
}
};
}
@Bean
public Guard<States, Events> playGuard() {
return new Guard<States, Events>() {
@Override
public boolean evaluate(StateContext<States, Events> context) {
ExtendedState extendedState = context.getExtendedState();
return extendedState.getVariables().get(Variables.CD) != null;
}
};
}
}
//end::snippetA[]
//tag::snippetB[]
public static enum States {
// super state of PLAYING and PAUSED
BUSY,
PLAYING,
PAUSED,
// super state of CLOSED and OPEN
IDLE,
CLOSED,
OPEN
}
//end::snippetB[]
//tag::snippetC[]
public static enum Events {
PLAY, STOP, PAUSE, EJECT, LOAD, FORWARD, BACK
}
//end::snippetC[]
//tag::snippetD[]
@Bean
public CdPlayer cdPlayer() {
return new CdPlayer();
}
@Bean
public Library library() {
return Library.buildSampleLibrary();
}
//end::snippetD[]
//tag::snippetE[]
public static enum Variables {
CD, TRACK, ELAPSEDTIME
}
//end::snippetE[]
public static void main(String[] args) throws Exception {
Bootstrap.main(args);
}
}

View File

@@ -0,0 +1,21 @@
package demo.cdplayer;
public class Cd {
private final String name;
private final Track[] tracks;
public Cd(String name, Track[] tracks) {
this.name = name;
this.tracks = tracks;
}
public String getName() {
return name;
}
public Track[] getTracks() {
return tracks;
}
}

View File

@@ -0,0 +1,79 @@
package demo.cdplayer;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import demo.cdplayer.Application.Events;
import demo.cdplayer.Application.States;
import demo.cdplayer.Application.Variables;
@WithStateMachine
public class CdPlayer {
@Autowired
private StateMachine<States, Events> stateMachine;
private String cdStatus = "No CD";
private String trackStatus = "";
public void load(Cd cd) {
stateMachine.sendEvent(MessageBuilder.withPayload(Events.LOAD).setHeader(Variables.CD.toString(), cd).build());
}
public void play() {
stateMachine.sendEvent(Events.PLAY);
}
public void stop() {
stateMachine.sendEvent(Events.STOP);
}
public void pause() {
stateMachine.sendEvent(Events.PAUSE);
}
public void eject() {
stateMachine.sendEvent(Events.EJECT);
}
public void forward() {
stateMachine.sendEvent(Events.FORWARD);
}
public void back() {
stateMachine.sendEvent(Events.BACK);
}
public String getLdcStatus() {
return cdStatus + " " + trackStatus;
}
@OnTransition(target = "BUSY")
public void busy(ExtendedState extendedState) {
Object cd = extendedState.getVariables().get(Variables.CD);
if (cd != null) {
cdStatus = ((Cd)cd).getName();
}
}
@OnTransition(target = "PLAYING")
public void playing(ExtendedState extendedState) {
System.out.println("playing1");
Object object = extendedState.getVariables().get(Variables.ELAPSEDTIME);
if (object instanceof Long) {
long elapsed = ((Long)object) + 1000l;
extendedState.getVariables().put(Variables.ELAPSEDTIME, elapsed);
SimpleDateFormat format = new SimpleDateFormat("mm:ss");
trackStatus = format.format(new Date(elapsed));
}
}
}

View File

@@ -0,0 +1,75 @@
package demo.cdplayer;
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 CdPlayerCommands implements CommandMarker {
@Autowired
private CdPlayer cdPlayer;
@Autowired
private Library library;
@CliCommand(value = "cd lcd", help = "Prints CD player lcd info")
public String lcd() {
return cdPlayer.getLdcStatus();
}
@CliCommand(value = "cd library", help = "List user CD library")
public String library() {
StringBuilder buf = new StringBuilder();
int index = 0;
for (Cd cd : library.getCollection()) {
buf.append(index++ + ": " + cd.getName() + "\n");
}
return buf.toString();
}
@CliCommand(value = "cd load", help = "Load CD into player")
public String load(@CliOption(key = {"", "index"}) int index) {
StringBuilder buf = new StringBuilder();
try {
cdPlayer.load(library.getCollection().get(index));
buf.append("Loading cd " + index);
} catch (Exception e) {
buf.append("Cd with index " + index + " not found");
}
return buf.toString();
}
@CliCommand(value = "cd play", help = "Press player play button")
public void play() {
cdPlayer.play();
}
@CliCommand(value = "cd stop", help = "Press player stop button")
public void stop() {
cdPlayer.stop();
}
@CliCommand(value = "cd pause", help = "Press player pause button")
public void pause() {
cdPlayer.pause();
}
@CliCommand(value = "cd eject", help = "Press player eject button")
public void eject() {
cdPlayer.eject();
}
@CliCommand(value = "cd forward", help = "Press player forward button")
public void forward() {
cdPlayer.forward();
}
@CliCommand(value = "cd back", help = "Press player back button")
public void back() {
cdPlayer.back();
}
}

View File

@@ -0,0 +1,28 @@
package demo.cdplayer;
import java.util.Arrays;
import java.util.List;
public class Library {
private final List<Cd> collection;
public Library(Cd[] collection) {
this.collection = Arrays.asList(collection);
}
public List<Cd> getCollection() {
return collection;
}
public static Library buildSampleLibrary() {
Track cd1track1 = new Track("Bohemian Rhapsody", 5*60+56);
Track cd1track2 = new Track("Another One Bites the Dust", 3*60+36);
Cd cd1 = new Cd("Greatest Hits", new Track[]{cd1track1,cd1track2});
Track cd2track1 = new Track("A Kind of Magic", 4*60+22);
Track cd2track2 = new Track("Under Pressure", 4*60+8);
Cd cd2 = new Cd("Greatest Hits II", new Track[]{cd2track1,cd2track2});
return new Library(new Cd[]{cd1,cd2});
}
}

View File

@@ -0,0 +1,20 @@
package demo.cdplayer;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
import demo.AbstractStateMachineCommands;
import demo.cdplayer.Application.Events;
import demo.cdplayer.Application.States;
@Component
public class StateMachineCommands extends AbstractStateMachineCommands<States, Events> {
@CliCommand(value = "sm event", help = "Sends an event to a state machine")
public String event(@CliOption(key = { "", "event" }, mandatory = true, help = "The event") final Events event) {
getStateMachine().sendEvent(event);
return "Event " + event + " send";
}
}

View File

@@ -0,0 +1,21 @@
package demo.cdplayer;
public class Track {
private final String name;
private final int length;
public Track(String name, int length) {
this.name = name;
this.length = length;
}
public String getName() {
return name;
}
public int getLength() {
return length;
}
}

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,46 @@
+-------------------------------------------------------------------------------+
| SM |
+-------------------------------------------------------------------------------+
| |
| +---------------------------+ +---------------------------+ |
| | BUSY | *-->| IDLE | |
| +---------------------------+ +---------------------------+ |
| | entry/ | | entry/ | |
| | exit/ | | exit/ | |
| | +------------+ | | +------------+ | |
| | *-->| PLAYING | | | *-->| CLOSED | | |
| | +------------+ | STOP | +------------+ | |
| | | entry/ | |---------->| | entry/ | | |
| | | exit/ | | | | exit/ | | |
| | | | | | | | | |
| | | +--| | | | | | |
| | | | | | | | | | |
| | | timer/1s| | | | | | | |
| | | | | | | | | | |
| | | +->| | | | | | |
| | | | | | | | | |
| | +--| |<-+ | | +--| |<-+ | |
| | | +------------+ | | | | +------------+ | | |
| | | | | PLAY | | | | |
| | |PAUSE | |<----------| |EJECT | | |
| | | | | | | | | |
| | | PAUSE| | | | EJECT| | |
| | | | | | | | | |
| | | +------------+ | | | | +------------+ | | |
| | | | PAUSED | | | | | | OPEN | | | |
| | | +------------+ | | | | +------------+ | | |
| | | | entry/ | | | | | | entry/ | | | |
| | +->| exit/ |--+ | | +->| exit/ |--+ | |
| | | | | | | | | |
| | | | | | | +--| | |
| | | | | | | | | | |
| | | | | | | LOAD| | | |
| | | | | | | | | | |
| | | | | | | +->| | |
| | | | | | | | | |
| | +------------+ | | +------------+ | |
| | | | | |
| +---------------------------+ +---------------------------+ |
| |
+-------------------------------------------------------------------------------+

View File

@@ -0,0 +1,160 @@
package demo.cdplayer;
import static org.hamcrest.Matchers.startsWith;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.statemachine.EnumStateMachine;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import demo.CommonConfiguration;
import demo.cdplayer.Application.Events;
import demo.cdplayer.Application.States;
public class CdPlayerTests {
private AnnotationConfigApplicationContext context;
private StateMachine<States,Events> machine;
private CdPlayer player;
private Library library;
@Test
public void testInitialState() throws InterruptedException {
assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED));
assertLcdStatusStartsWith("No CD");
}
@Test
public void testEjectTwice() {
player.eject();
assertThat(machine.getState().getIds(), contains(States.IDLE, States.OPEN));
player.eject();
assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED));
}
@Test
public void testPlayWithCdLoaded() {
player.eject();
player.load(library.getCollection().get(0));
player.eject();
player.play();
assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING));
assertLcdStatusContains("cd1");
}
@Test
public void testPlayWithNoCdLoaded() {
player.play();
assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED));
assertLcdStatusStartsWith("No CD");
}
@Test
public void testPlayLcdTimeChanges() throws Exception {
player.eject();
player.load(library.getCollection().get(0));
player.eject();
player.play();
assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING));
assertLcdStatusContains("cd1");
Thread.sleep(1000);
assertLcdStatusContains("00:01");
Thread.sleep(1000);
assertLcdStatusContains("00:02");
Thread.sleep(1000);
assertLcdStatusContains("00:03");
}
@Test
public void testPlayPause() throws Exception {
player.eject();
player.load(library.getCollection().get(0));
player.eject();
player.play();
assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING));
assertLcdStatusContains("cd1");
Thread.sleep(1000);
assertLcdStatusIs("cd1 00:01");
Thread.sleep(1000);
assertLcdStatusContains("00:02");
player.pause();
Thread.sleep(2000);
assertLcdStatusContains("00:02");
player.pause();
assertLcdStatusContains("00:03");
Thread.sleep(1000);
assertLcdStatusContains("00:04");
}
@Test
public void testPlayStop() throws Exception {
player.eject();
player.load(library.getCollection().get(0));
player.eject();
player.play();
assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING));
player.stop();
assertLcdStatusIs("cd1 ");
}
private void assertLcdStatusIs(String text) {
assertThat(player.getLdcStatus(), is(text));
}
private void assertLcdStatusStartsWith(String text) {
assertThat(player.getLdcStatus(), startsWith(text));
}
private void assertLcdStatusContains(String text) {
assertThat(player.getLdcStatus(), containsString(text));
}
@SuppressWarnings("unchecked")
@Before
public void setup() {
context = new AnnotationConfigApplicationContext();
context.register(CommonConfiguration.class, Application.class, TestConfig.class);
context.refresh();
machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
player = context.getBean(CdPlayer.class);
library = context.getBean(Library.class);
machine.start();
}
@After
public void clean() {
context.close();
context = null;
machine = null;
player = null;
library = null;
}
static class TestConfig {
@Bean
public Library library() {
// override library to make it easier to test
Track cd1track1 = new Track("cd1track1", 3);
Track cd1track2 = new Track("cd1track2", 3);
Cd cd1 = new Cd("cd1", new Track[]{cd1track1,cd1track2});
Track cd2track1 = new Track("cd2track1", 3);
Track cd2track2 = new Track("cd2track2", 3);
Cd cd2 = new Cd("cd2", new Track[]{cd2track1,cd2track2});
return new Library(new Cd[]{cd1,cd2});
}
}
}

View File

@@ -0,0 +1,8 @@
log4j.rootCategory=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} [%t] - %m%n
log4j.category.org.springframework.statemachine=TRACE

View File

@@ -13,6 +13,8 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.statemachine.event.OnStateChangedEvent;
import org.springframework.statemachine.event.OnTransitionEvent;
import org.springframework.statemachine.event.StateMachineEvent;
@@ -32,6 +34,11 @@ public class CommonConfiguration {
return new SyncTaskExecutor();
}
@Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler();
}
@Bean
public TestEventListener testEventListener() {
return new TestEventListener();