Heiner Kücker

State Transition Engine

Home

Java-Seite

   ASM Improved

   heterogene
   Map, HMap

   Constraint
   Code Generator

   JSP WorkFlow
   PageFlow FlowControl
   Page Flow Engine
   Web Flow Engine
   Control_and_Command

   JSP_Spreadsheet

   Code-Generator
   für Option-Either-Stil
   in Java

   verbesserter
   Comparator

   Fluent-Interface
   Code-Generator
   auf Basis
   einer Grammatik

   Visitor mit Multidispatch

   for-Schleife mit
   yield-return

   Kognitions-Maschine
   semantisches Netz

   Domain Parser

   Codegenerator_für
   hierarchische
   Datenstrukturen

   Expression_Engine
   Formula_Parser

   Thread Preprocessor

   State Transition Engine

   AspectJ

   Java_Explorer

   DBF_Library

   Kalender_Applet

   SetGetGen

   BeanSetGet

   CheckPackage

   LineNumbers

   GradDms

   Excel-Export

   StringTokenizer

   JspDoc

   JspCheck

   JSP-Schulung
   Java Server Pages
   Struts

   Ascii-Tabellen-
   Layouter

   Ascii-Baum-
   Layouter

   Ascii-Art-Fluss-
   Diagramm-
   Parser

   AsciiArt
   AssignmentMatrix
   Layouter

   StringSerial

   Silbentrennung

   JDBC_Schlüssel-
   Generierung

   bidirektional/
   unidirektional
   gelinkte Liste

   Java_Sitemap
   Generator

   XmlBuilder

   RangeMap

   StringFormatter

   VersionSafe
   XCopy

   JTextField

   CommandLine-
   ParamReader

   Bitmap-Grafik

   MultiMarkable-
   Buffered-
   InputStream

   JavaCache

   JdomUtil

   CollectionUtil

   XML Really
   Pull Parser

   Log-Filter

   Remote-Protokoll

   Sudoku-Generator

   Delegation statt
   Mehrfachvererbung

   Disjunct
   Interval Set

   WebCam_Demo

   Weiterentwicklung_Java

Alaska-XBase++-Seite

Projekte

Philosophien
Techniken


Konzepte

Sudoku

Kontakt /
Impressum


Links

SiteMap





Letzte Aktualisierung:
25.08.2003

Anleitung StateTransitionEngine

Die StateTransitionEngine (Zustands-Übergangs-Maschine) stellt eine Lösung zur Realisierung von Workflows (Fluss-Steuerungen) oder von zustandsbehafteten Systemen dar. Entsprechende grafische Modelle sind Aktivitätsdiagramme oder Petri-Netze.
       +-----------+            +-----------+
    +->| Zustand 1 |----------->| Zustand 2 |--+
    |  +-----------+            +-----------+  |
    |                                          |
    |  +-----------+            +-----------+  |
    +--| Zustand 4 |<-----------| Zustand 3 |<-+
       +-----------+            +-----------+
Die Struktur eines Zustandsübergangsdiagrammes wird in eine Tabellendarstellung transformiert.

Die Steuertabelle hat folgenden Aufbau:
aktueller Status
SourceState
Ereignis
StateTransitEvent
PosUser-Rolle
isUserPermitted()
Bedingung
beforeStateChanged()
Aktion vor Wechsel
beforeStateChanged()
Ziel-Status
DestinationState
Aktion nach Wechsel
afterStateChanged()
Startnext0User  Laufi = 0
Laufnext0Useri < 3 Laufi++
1User  Stop 
Stop 0User    
Obiges Beispiel zeigt drei mögliche Zustände, Start, Lauf und Stop, wobei der Zustand Lauf dreimal wiederholt erreicht wird.

Das heisst also, dass zu jedem Status eine Tabelle mit möglichen Ereignissen und zu jedem Ereignis eine Tabelle mit Bedingungen, entsprechenden Ziel-Zuständen und Aktionen (Methoden-Aufrufe) vor und nach dem Wechsel exisitiert. Jeder Zielzustand mit den dazugehörigen Bedingungen hat eine bestimmte Position in der Reihenfolge der Zielzustände je Ereignis, um die Reihenfolge beim Bestimmen des passenden Zielzustandes eindeutig festzulegen.

In der Implementierung wird die Status-Tabelle auf einen Baum mit folgender Struktur abgebildet.
 StateTransitionEngine
   |
   +--> SourceState (Map)
          |
          +--> StateTransitEvent (Map)
                 |
                 +--> DestinationState (List) , isUserPermitted() , isTransitConditionTrue()
Eine wichtige Regel beim Entwurf der Zustands-Übergangs-Tabelle ist, dass StateTransitEvents stets Imperative sein sollten:

Gib in Bearbeitung!
Gib frei!
Setze auf Status "Geprüft"!
Weise zurück!
Lösche!

während ein Status immer einen Zustand beschreibt:

Ist in Bearbeitung.
Ist Freigegeben.
Wurde Geprüft.
Ist Zurückgewiesen.
Ist Gelöscht.


Prinzipiell könnte man die StateTransitEvents auch StateTransitActions nennen. Ich habe mich jetzt erst mal auf den Namen Event festgelegt. Wahrscheinlich spielt es keine Rolle. Beim Entwurf der StateTransitionMap sollte das einem aber bewusst sein.

Wenn man für die States (Source und Destiantion) und die Events kryptische (technische) Namen verwendet, bietet es sich an, vor der Anzeige auf der GUI für Statusinformation und menüartige Event-Auswahl (Selectbox, Links, Buttons) eine Internationalisierungs-Logik (i18n) mit RessourceBundles dazwischenzuschalten. Dadurch erhält man neben der Internationalisierung noch eine Übersetzung von technischer zu fachlicher Notation.

Das Definieren/Erzeugen der Zustands-Tabelle

Zuerst wird eine StateTransitionEngine erzeugt:
  // create engine
  StateTransitionEngine stateTransEng = new StateTransitionEngine();
Dieser StateTransitionEngine können beliebig viele Zustände zugeordnet werden. Da diese Zustände Anfangspunkte von Zustandsübergängen sind, nennen sie sich SourceState.
  SourceState stateStart = new SourceState("Start");
  stateTransEng.add(stateStart);
Jedem SourceState können beliebig viele Ereignisse (StateTransitEvent) zum Zustandsübergang zugeordnet werden. Unterschiedliche Ereignisse können dabei unterschiedliche Zielzustände spezifizieren.
+-------------+
| SourceState |
+-------------+
   |                             +--------------------+
   +--- StateTransitEvent 1 ---->| DestinationState 1 |
   |                             +--------------------+
   |
   |                             +--------------------+
   +--- StateTransitEvent 2 ---->| DestinationState 2 |
                                 +--------------------+
  // create from state start event next
  StateTransitEvent nextFromStateStartEvent = new StateTransitEvent("next");
  stateStart.add(nextFromStateStartEvent);
Jedem Zustands-Übergangs-Ereignis (StateTransitEvent) kann mindestens ein Zielzustand zugeordnet werden.
  // create destination state start to second
  DestinationState secondDestinationState = new DestinationState("Second");
  nextFromStateStartEvent.add(secondDestinationState);
Alternativ ist es möglich, jedem Zustands-Übergangs-Ereignis (StateTransitEvent) beliebig viele Zielzustände zuzuordnen, die sich durch ihre Übergangs- Bedingungen (Methode boolean isTransitConditionTrue() ) unterscheiden sollten.

Dazu kann eine eigene Klasse von der Klasse DestinationState abgeleitet werden, um die Methoden isTransitConditionTrue, transitConditionDescription, beforeStateChanged und afterStateChanged zu überschreiben.
  // create destination state start to second
  // Die eigene (Anwender-)Klasse SecondDestinationState
  // erbt von DestinationState
  DestinationState secondDestinationState = new SecondDestinationState();
  nextFromStateStartEvent.add(secondDestinationState);
Die mit einenm StateTransitEvent verbundenen DestinationState´s werden in der Reihenfolge, in welcher sie aufaddiert wurden (Methode add oder Reihenfolge in der XML-Datei) nach dem Erfüllen der Umschalt-Bedingung (isTransitConditionTrue()-Methode) abgesucht. Der erste passende DestinationState wird angesprungen.

Dabei werden die Aktions-Methoden beforeStateChanged vor und afterStateChanged nach dem Umschalten des Status ausgeführt.

Durch den Einbau der User-Rollen-basierten Bedingungsabfrage werden diese in die Prüfung, ob der Zielzustand passt, mit einbezogen.

Nach dem kompletten Aufbau der Zustands-Übergangs-Tabelle sollte ihre innere Struktur geprüft werden. Dabei wird geprüft, ob zu jedem DestinationState ein passender SourceState existiert. Der Einbau weiterer Prüfungen ist vorgesehen, zum Beispiel, ob jeder SourceState, ausser initial, über einen DestinationState erreicht werden kann. Beim Einlesen aus einer XML-Datei ist der check()-Aufruf nicht unbedingt nötig, er wird automatisch erledigt.
  // check validity of state map
  if (!stateTransEng.check()) {
    System.out.println("state map is invalid");
    System.out.println("" + stateTransEng.getCheckMessages());

    System.exit(0);
oder
    throw new StateTransitionEngineIsInvalidException();

  }
  else {
    System.out.println("state map is valid");
  }

Anschliessend wird mindestens ein State-Objekt mit Bezug auf die jetzt fertige StateTransitionEngine erzeugt.
  // create application state
  ApplicationState appState = new ApplicationState(stateTransEng) ;
Durch das Abschicken der Events kann der Status weitergeschaltet werden.
  appState.perform("next", ctx);//ohne User-Rolle
  appState.perform("next", ctx, strUserRole);//mit User-Rolle
Als Kontext kann eine beliebiges Objekt, zum Beispiel auch this übergeben werden. Dieses Objekt wird in die anwendungsorientierte Prüfung der Umschaltbedingungen einbezogen.

Hier der entsprechende Code aus SecondSecondDestinationState:
  /**
   * check condition with overgiven context method to implements by the user of the StateTransitionEngine
   */
  public boolean isTransitConditionTrue(Object paObjStateContext) {
    Context contxt = (Context) paObjStateContext;

    if (contxt == null) {
      System.out.println("overgiven context is null");
      return true;
    }

    // perform if Context.i <= 3
    return contxt.i < 3;

  }// end method



Generieren aus XML-File

Eine XML-Datei kann als InputStream mit der Methode readXmlInputStream aus dem gleichen Verzeichnis wie die Source-Klassen, bzw. aus einem jar-File eingelesen werden.
  InputStream xmlInpStream = stateTransEng.getClass().getResourceAsStream(
          "/de/cnc/statetransitiondemo/DemoStateTransitionMap.xml");
  stateTransEng.readXmlInputStream(xmlInpStream);
  stateTransEng.check();//bei Einlesen aus XML nicht unbedingt nötig, wird automatisch erledigt
Als XML-Reader dient die Open-Source-Library kXml. Deshalb muss die mitgelieferte kxml.jar in den CLASSPATH eingebunden werden.

Hier ist die XML-Definitionsdatei für die oben als Beispiel angegebene Zustands-Übergangs-Tabelle, die auch der aufgebauten Zustands-Übergangs-Tabelle aus StateDemo.java entspricht.

siehe hierzu die mitgelieferte Beispieldatei /statetransitiondemo/DemoStateTransitionMap.xml

Tags

-StateTransitionMap äusseres Tag
 Attribute:
  checkUserRole (true/false), optional, schaltet checkUserRole ein/aus
darf folgendes Tag enthalten: SourceState

-SourceState darf nur im Tag StateTransitionMap erscheinen
 Attribute:
  Name Pflicht-Attribut
  initial (true/false), optional, setzt den State als Anfangszustand
darf folgendes Tag enthalten: StateTransitEvent

-StateTransitEvent darf nur im Tag SourceState erscheinen
 Attribute:
  Name Pflicht-Attribut
darf folgendes Tag enthalten: DestinationState

-DestinationState darf nur im Tag StateTransitEvent erscheinen
 Attribute:
  Remark optionaler Kommentar
  DestinationStateName Pflicht-Attribut, Name des anzusteuernden States
  AllPermitted optional, erlaubt diesen DestinationState für alle User-/User-Rollen, wenn true
(Wenn das Attribut AllPermitted auftaucht, schaltet der XML-Parser den checkUserRole für die StateTransitionEngine ein)
  UserRole optional, darf beliebig oft wiederholt werden, Angabe einer/mehrerer User-Rollen, für die der anzusteuernde Status erlaubt ist
(Wenn das Attribut UserRole auftaucht, schaltet der XML-Parser den checkUserRole für die StateTransitionEngine ein)
(eventuell mit kommaseparierter Liste arbeiten)
darf folgendes Tag enthalten: keines

Einbringen applikationsspezifischen Codes in die Zustands-Tabelle und
Einbeziehen des Applikations-Kontextes in die Prüfung der Bedingungen

Durch Erben von der Klasse DestinationState kann der Benutzer dieser StateTransitionEngine eigenen Code für die Wechsel-Bedingung (transit condition) sowie die Vor- und Nach-Wechsel-Aktionen einbringen. Die Möglichkeit zur Verwendung von User-Code schien mir flexibler als eine Realisierung mit Primitiv-Bedingungen, zu interpretierenden Ausdrücken oder Reflection-Features.

Der Methode
  void perform(String paStrEventName, Object paObjStateContext)
in der Klasse StateTransitionEngine kann der Applikations-Kontext als Object übergeben werden.

In den Methoden
  boolean isTransitConditionTrue(Object paObjStateContext)

  void beforeStateChanged(Object paObjStateContext)

  void afterStateChanged(Object paObjStateContext)
der Klasse DestinationState, beziehungsweise ihren anwenderdefinierten Ableitungen(Kind-Klassen), kann der als Object übergebene Applikations-Kontext zurück zur Originalklasse gecastet werden, um die Bedingungen gegen den Applikations-Kontext prüfen zu können sowie Aktionen (pre/after state changed) auf Applikations-Kontext ausführen zu können. So geht zwar die Compiler-Type- Prüfung verloren, aber der Applikations-Kontext steht so unmittelbar zur Verfügung. Eine bessere Lösung ist mir nicht eingefallen.

Anwendung einer Zustands-Übergangs-Tabelle auf mehrere zustandsbehaftete Objekte

Für reale Applikationen ist eine Lösung mit einem Session-, User-, Dokument- und so weiter -weit gültigen State, aber mit einer applikationsweit gültigen Zustands-Übergangs-Tabelle sinnvoll. Dafür gibt es die Klasse ApplicationState.

Um das ständige Neuaufbauen der Zustands-Übergangs-Tabelle zu vermeiden, empfehle ich, die StateTransitionEngine statisch anzulegen.
  /**
   * Beispiel für statisches Anlegen einer StateTransitionMap
   */
  static final StateTransitionEngine staticStateTransEng = initStateTransitionEngine();

  /**
   * initialize StateTransitionEngine
   */
  private static StateTransitionEngine initStateTransitionEngine() {
    StateTransitionEngine retStateTransEng = new StateTransitionEngine();
    InputStream xmlInpStream = retStateTransEng.getClass().getResourceAsStream(
          "/de/cnc/statetransitiondemo/DemoStateTransitionMap.xml");
    retStateTransEng.readXmlInputStream(xmlInpStream);
    return retStateTransEng;
  }// end method
Für den laufenden Test ist dagegen günstiger, bei jedem Request die Zustands- Übergangs-Tabelle neu aus der XML-datei zu lesen, um das Neustarten des Web- oder Application-Servers zu vermeiden.

Persistierung des Zustandes

Im Umfeld von Server- oder Host-Applikationen ist das Weitergeben des Zustandes über mehrere Requests oder Sessions notwendig.
Dies kann ganz leicht durch das Setzen und Abfragen des als String codierten aktuellen Zustandes mit den Methoden
  void setState(String paStrNewState)

  String getCurrentStateName()
erreicht werden. Der String kann in einer HTTPSession oder auf einer Datenbank gespeichert werden.

Rollenbasiertes Ändern des Zustandes

Ein Dokument könnte Zustände wie neu, angelegt, in Bearbeitung, geprüft, abgewiesen, zurückgezogen, gelöscht, veröffentlicht usw. haben. Dabei darf eine bestimmte Zustandsänderung nur abhängig von der Rolle des aktuellen Users wie Autor, Redakteur, verantwortlicher Redakteur angestossen werden. Über den Applikations-Kontext kann das in der Methode
  boolean isUserPermitted(String  paStrUserRole)
der Klasse DestinationState realisiert werden.

Die Benutzer-Rollen werden in den Ziel-Zuständen (DestinationState) der Zustands-Übergangs-Tabelle gespeichert. Dadurch ist kein Anwendercode für die rollenabhängigen Bedingungen nötig.

Für die gesamte StateTransitionEngine kann die Prüfung der Benutzer-Rollen mit der Methode
  setUserCheck(boolean paBOn)
eingeschaltet werden. Im XML-File dient dazu das Attribut checkUserRole.
Wenn im XML-File das Attribut UserRole oder AllPermitted im Tag DestinationState erscheint, schaltet der XML-Parser den UserCheck ein.

Die zulässigen Rollen können zu den jeweiligen DestinationStates mit
  addUserRole(String paStrUserRole)
hinzugefügt werden. Im XML-File dient dafür das Attribut UserRole, das beliebig oft angegeben werden kann.

Mit der Methode
  setAllPermitted(boolean paBAllPerm)
kann die Prüfung für DestinationStates, die alle User ansteuern dürfen, komplett abgeschaltet werden. Im XML-File gibt es das entsprechende Attribut AllPermitted.

Falls die StateTransitionEngine in einer Applikation mit Prüfung der User-Rollen gegen eine Datenbank, LDAP oder anderes Backend erfolgt, muss die Methode boolean isUserPermitted(String) in der Klasse DestinationState entsprechend ausgetausch werden.

Noch ein kleiner Praxistip. Ich hatte einen Anwendungsfall mit einem untergeordnetem User (Dokument Anlegen, Vorschlagen) und einem übergeordneten User (Dokument Zurückweisen,Freigeben,Löschen). Dazu kamen einige automatische Schritte wie Löschen nach Ablauf einer bestimmten Zeit und Weiterschalten des Status von Erzeugt auf Vorgeschlagen nach dem Abspeichern durch den untergeordneten User. Dafür bietet sich die Anlage eines Users "Automatic" oder "Maschine" zusätzlich zu "Autor" und "Redakteur" an. Bei der Durchführung der automatischen Statuswechsel wird der perform()-Methode dieser automatische User mitgegeben.

Abfrage der vom aktuellen Zustand aus erreichbaren Ziel-Zustände

Die hinterlegte Zustands-Übergangs-Tabelle erlaubt es, ausgehend vom aktuellen Zustand alle erreichbaren Zustände aufzulisten. Dies kann zum Erzeugen von Select-Boxen, Buttons oder Links für die jeweilige Benutzeroberfläche genutzt werden. Dabei wird der Anwendungs-Kontext und die Benutzer- Rolle mit einbezogen. Dadurch kann auf der Benutzeroberfläche ohne weitere zu erstellende Logik eine Auswahl der jeweils möglichen Zielzustände angeboten werden.

Siehe Methoden
ApplicationState.getPossibleStateToChangeNames(Object paObjStateContext, String paStrUserRole)
ApplicationState.getPossibleStateToChangeNames(Object paObjStateContext)
ApplicationState.getPossibleStateToChangeNamesAndEventNames(Object paObjStateContext, String paStrUserRole)
ApplicationState.getPossibleStateToChangeNamesAndEventNames(Object paObjStateContext)
StateTransitionEngine.getAllPossibleStateToChangeNames(ApplicationState paAppState)

Realisierung eines StateProcessors

Welcher Programmierer hätte sich es nicht schon mal gewünscht, einen eigenen Prozessor entwickeln. Ein Prozessor zur automatischen Abarbeitung der Events lässt sich durch eine einfache Schleife realisieren. Wichtig ist dafür ein fester vorgegebener StateTransitEvent-Name, hier zum Beispiel "next".
  while ( <Abbruchbedingung, z.B. ! "Stop".equals( appState.getCurrentStateName() )> ) {
    <Anwendungscode vorher>
    appState.perform("next", <Anwendungskontext> );
    <Anwendungscode nachher>
  }

Debugging

Es ist möglich, die Zustands-Übergangstabelle mit der StateTransitionEngine.toString()-Methode auszugeben.

Ausserdem kann man sich die abhängig vom Kontext und User erreichbaren Ziel- Zustände bzw. alle Ziel-Zustände für den jeweiligen Ausgangs-Zustand auflisten lassen. Siehe hierzu den entsprechenden Abschnitt.

Mögliche Verbesserungen

Alternativ zu ApplicationState(many state, one map) könnte eine Methode
  String perform( currentStateString, EventString, contextObject )
den neuen Status in einem einzigen Aufruf umschalten.

Alle Methoden müssen threadsave ausgelegt sein (ausschliesslich auf lokale Variablen und return-Werte schreiben, globale Variablen nur lesend).

Weiterhin sollten innerhalb der Engine verwendete Klassen und Methoden entsprechend dem Prinzip des smallest scope möglichst nicht public sein.

SubStateTransitionMaps: Es wäre denkbar, weitere Zustands-Übergangs-Tabellen in der Art von Unterprogrammen anzuspringen. Dabei benötigt man einen Zustands- Stack. An die SubStateTransitionMaps könnten der Applikations-Kontext, SourceStates, StateTransitEvents, DestinationStates und so weiter als Parameter übergeben werden, um sie generisch wiederverwenden zu können.

Dynamisches Verändern der StateMap zur Laufzeit? Braucht man so etwas? Dabei müsste geprüft werden, ob nicht ein noch gesetzter Status gelöscht wird und ob ein zusätzlicher Ziel-Status valid ist (als Quell-Status eingetragen).

HTML-Dump

Debug-Information mit der XML-Zeile in der sich der aktuelle oder ein bestimmter SourceState, StateTransitEvent und DestinationState befindet.

Einbau Internationalisierung. Methode zum Anmelden eines RessourceBundles (registerRessourceBundle). Natürlich auch Abfrage (isRessourceRegistred()). Methoden zur Rückgabe konvertierter oder realer Namen. Eventabarbeitung von übergebenen konvertierten Namen (Rückkonvertierung und dann perform)

Logging für Zustands-Historie



Download der Java-Quelldateien StateTransitionEngine.zip



Achtung: Erweiterungen und Fixes stelle ich ohne Historie und ohne Ankündigung hier bereit.
Deshalb am besten immer die letzte Version runterladen.

Lizenzbedingungen:

Die Programme, Quelltexte und Dokumentationen können ohne irgendwelche Bedingungen kostenlos verwendet werden.
Sie sind Freeware und Open Source. Für Fehler und Folgen wird keinerlei Haftung übernommen.

Hinweise zur Fehlerbeseitigung und Verbesserung sind mir willkommen.

Ich freue mich auch über Feedback bezüglich der erfolgreichen Verwendung meiner Sourcen.

Bei Fragen helfe ich gern mit Hinweisen oder zusätzlicher Dokumentation, falls ich dafür Zeit habe.