Ken Webb 2010-02-01T22:48:47Z
The SwingStates open-source Java toolkit supports finite-state machines specified directly in Java to describe and implement the behavior of interactive systems. SwingStates offers various types of state machine and event objects, primarily intended for use in building user interfaces. A small core of classes (none of which depend on Java Swing) are all that's required to build a simple executable state machine, implementing for example the behavior of a coffee maker. The toolkit provides StateMachine, State, Transition, and Event classes, that can be composed into a nested hierarchy of Java anonymous inner classes. SwingStates is released under the LGPL open-source license (the same as Xholon), and is available for download at SourceForge.
Xholon can easily integrate the SwingStates classes into its framework, allowing each instance of an anonymous inner class to participate natively in the larger Xholon tree structure. This provides a simpler Java-only alternative to the full UML state machines already available in Xholon.
The following code is taken from a board game constructed using Xholon. This partial SwingStates state machine enforces the rules concerning what actions each player may take during his/her/its turn. This is the code for the Human player, which is somewhat different from the code for a Computer player.
/** * Action state in the game of Risk. * This subclass of State adds several actions that are valid no matter which state the state machine is in. */ class RiskActionState extends State { Transition showCards = new Event("showCards") { public void action() { getController().sendSyncMessage(IRiskSignal.SIG_SHOW_CARDS, null, HumanPlayer.this); } }; Transition showTerritories = new Event("showTerritories") { public void action() { getController().sendSyncMessage(IRiskSignal.SIG_SHOW_TERRITORIES, null, HumanPlayer.this); } }; Transition showContinents = new Event("showContinents") { public void action() { getController().sendSyncMessage(IRiskSignal.SIG_SHOW_CONTINENTS, null, HumanPlayer.this); } }; } /** * Create and start the actions state machine. */ public void createStateMachine() { println("Starting createStateMachine ..."); actionStateMachine = new StateMachine() { /************************ State settingUpGame ******************** * * A new game is being set up. */ public State settingUpGame = new RiskActionState() { public void enter() { println("entering state: settingUpGame"); } Transition setUpDone = new Event("setUpDone", ">> waiting") {}; public void leave() { println("leaving state: settingUpGame"); } }; /************************ State waiting ******************** * * This player is waiting for the game to begin, or for other players to finish their turns. */ public State waiting = new RiskActionState() { public void enter() { println("entering state: waiting"); enableDisableAllActions(false); enableStandardActions(); enableDisableAction(startTurnAction, true); } Transition startTurn = new Event("startTurn", ">> hasUnplacedArmies") { public void action() { getController().sendMessage(IRiskSignal.SIG_START_TURN, null, HumanPlayer.this); }; }; public void leave() { println("leaving state: waiting"); } }; /************************ State hasUnplacedArmies ******************** * * This player has armies that have not yet been placed on owned territories. */ public State hasUnplacedArmies = new RiskActionState() { public void enter() { println("entering state: hasUnplacedArmies"); enableDisableAllActions(false); enableStandardActions(); if (hasCards()) {enableDisableAction(useCardsAction, true);} enableDisableAction(placeArmiesAction, true); } Transition placeArmiesOnly = new Event("placeArmies", ">> hasUnplacedArmies") { // this simulates an unconditional transition to a choice point // it returns false to force the state machine to also try other transitions public boolean guard() { getController().sendSyncMessage(IRiskSignal.SIG_PLACE_ARMIES, null, HumanPlayer.this); return false; } }; Transition placeArmies = new Event("placeArmies", ">> mayAttack") { public boolean guard() { if (hasFivePlusCards()) {return false;} if (hasUnplacedArmies()) {return false;} return true; } public void action() { System.out.println("placeArmies action"); } }; Transition useCards = new Event("useCards", ">> hasUnplacedArmies") { public void action() { getController().sendMessage(IRiskSignal.SIG_USE_CARDS, null, HumanPlayer.this); } }; public void leave() { println("leaving state: hasUnplacedArmies"); } }; ...
The following diagram shows the SwingStates anonymous inner class state-hierarchy integrated into the overall Xholon tree at runtime.
The runtime attributes of any of these SwingState objects can be viewed in Xholon.
A runtime sequence diagram can be captured by Xholon and displayed using one of several third-party tools, including the Quick Sequence Diagram Editor. The following diagram was generated from the interaction between the Player state machine and the overall game controller.
Xholon can also generate a graphical view of the state machine itself, using its JUNG based viewer and animation tool. The detailed integration of this with SwingStates has not yet been done, so this example diagram includes many more nodes than should be visible. The State nodes are shown as small rounded rectangles. The currently active state is displayed bolded in red.
The SwingStates classes, as used in Xholon, extend the XholonAnonymous class.
The Java compiler (Sun's Java 1.6 implementation) generates a synthetic object as part of each anonymous inner class, that references the containing class. Xholon interprets this as a parentNode, which is all it needs to make the inner class part of the tree structure.
The concept of using anonymous inner classes could be used in Xholon for more than just state machines. It's a possible way of specifying other parts of the composite structure, as an alternative to the use of XML.
SwingStates offers many more features than have been mentioned in this article, some of which could be usefully integrated into Xholon.
SwingStates is currently used in an ad-hoc manner within Xholon. It should be made into a more formal Xholon mechanism. This would make it very easy to create a new state machine whenever a developer discovers a need for complex behavior that can react to events. Alternatively, it might make sense to integrate SwingStates into the currently available UML state machine mechanism. The SwingStates classes would be an alternative implementation.
SwingStates itself is missing some useful objects found in the UML state machine specification, such as Choice, Initial state, and Final state. Note that in some cases, SwingStates uses an alternative approach to providing some of the UML functionality.