diff --git a/settings.gradle b/settings.gradle index 0ebfe7cb..a614a696 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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') { diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 9877be75..2b990e8d 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -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' +} + diff --git a/spring-statemachine-samples/cdplayer/.gitignore b/spring-statemachine-samples/cdplayer/.gitignore new file mode 100644 index 00000000..70e6e4b8 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/.gitignore @@ -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 diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java new file mode 100644 index 00000000..012e354b --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java @@ -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 { + + @Override + public void configure(StateMachineStateConfigurer 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 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 loadAction() { + return new Action() { + @Override + public void execute(StateContext context) { + Object cd = context.getMessageHeader(Variables.CD); + context.getExtendedState().getVariables().put(Variables.CD, cd); + } + }; + } + + @Bean + public Action playAction() { + return new Action() { + @Override + public void execute(StateContext context) { + context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l); + } + }; + } + + @Bean + public Guard playGuard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext 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); + } + +} diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Cd.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Cd.java new file mode 100644 index 00000000..d063402c --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Cd.java @@ -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; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java new file mode 100644 index 00000000..c5b614c2 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java @@ -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 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)); + } + + } + +} diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java new file mode 100644 index 00000000..532af4eb --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java @@ -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(); + } + +} diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Library.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Library.java new file mode 100644 index 00000000..f152bc33 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Library.java @@ -0,0 +1,28 @@ +package demo.cdplayer; + +import java.util.Arrays; +import java.util.List; + +public class Library { + + private final List collection; + + public Library(Cd[] collection) { + this.collection = Arrays.asList(collection); + } + + public List 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}); + } + +} diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java new file mode 100644 index 00000000..5be949d1 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java @@ -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 { + + @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"; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Track.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Track.java new file mode 100644 index 00000000..af3dfe5c --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Track.java @@ -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; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..1fc09f1f --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/cdplayer/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..82d2fb61 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/resources/statechartmodel.txt @@ -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| | | | +| | | | | | | | | | | +| | | | | | | +->| | | +| | | | | | | | | | +| | +------------+ | | +------------+ | | +| | | | | | +| +---------------------------+ +---------------------------+ | +| | ++-------------------------------------------------------------------------------+ + diff --git a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java new file mode 100644 index 00000000..100dfb7c --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java @@ -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 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}); + } + + } + +} diff --git a/spring-statemachine-samples/cdplayer/src/test/resources/log4j.properties b/spring-statemachine-samples/cdplayer/src/test/resources/log4j.properties new file mode 100644 index 00000000..4589b1e9 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/test/resources/log4j.properties @@ -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 + diff --git a/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java b/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java index 74020486..f4a1ec17 100644 --- a/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java +++ b/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java @@ -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();