Der nächste Schritt der Vervollständigung des Programms besteht darin, dem Manager zu ermöglichen, die Zeit weiterzuschalten. Es wird das "Manager"-Menü des Büros erstellt. Es wird ein Menüpunkt benötigt, der eine Aktion auslöst, die das vom Shop verwendete Timer-Objekt ermittelt und es weiterschaltet. Um den Menüpunkt als ersten des Menüs einzubauen, wird in der Methode getDefaultMenuSheet zuerst eine neues Menü für die Wartung angelegt und dort ein neuer Eintrag eingefügt. In die Klasse Office wird folgender Code geschrieben:

Menü des Managers


  public MenuSheet getDefaultMenuSheet()
  {
    MenuSheet msSubMenu = new MenuSheet("Maintenance");

    msSubMenu.add(new MenuSheetItem("Advanced time",
      new sale.Action()
      {
        public void doAction(SaleProcess p, SalesPoint sp)
        {
          Shop.getTheShop().getTimer().goAhead();
        }
      }));

    MenuSheet msMenu = new MenuSheet("Office menu");
    msMenu.add(msSubMenu);
    return msMenu; 
  }
        

Zeit weiterschalten

Damit kann die Zeit weitergestellt werden. Leider ist damit noch keine der am Tagesanfang anstehenden Aktionen ausgeführt. Dazu gehört das Aktualisieren des Standard-FormSheets.

Es ist zu beachten, daß die vom FormSheet angezeigten Daten ungültig werden könnten. Dies kann geschehen, wenn entweder die Zeit weitergeschaltet wird oder sich der Geldbestand im Münzschacht ändert.

Die Zeit kann nur vom Manager weitergeschaltet werden. Es muß also daran gedacht werden, danach das FormSheet neu aufzubauen. Dies kann entweder direkt beim Weiterschalten geschehen oder indirekt durch den Einsatz von Listenern.

Ein Listener ist Teil eines erweiterten Observer-Patterns. Zwei weitere Bestandteile sind die Quelle des Ereignisses (Observable) und des Ereignis (die Erweiterung vom Observer-Pattern) selbst. Unter einem Ereignis ist jedoch nicht der Vorfall im üblichen Sinne zu verstehen, sondern vielmehr die Beschreibung des Vorfalls. Diese Beschreibung wird von der Quelle erzeugt und an alle Listener gesendet. Das geschieht, in dem an den Listenern eine festgelegte Methode aufgerufen wird. Die Besonderheit an dem Konzept ist, daß die Anzahl von Listenern vorher nicht festgelegt ist. Vielmehr kann sich während des Programmablaufs eine, meist beliebige, Anzahl von Listenern an der Quelle an- und wieder abmelden. Ein weiteres Merkmal ist die Kapselung der Daten. So werden nicht mehr Parameter an die zuständige Methode des Listeners übergeben, sondern lediglich das Ereignis, das dann Methoden zur Verfügung stellt, um die enthaltenen Daten abzufragen.Die Methoden, die ein Listener implementieren muß, um bestimmte Ereignisse empfangen zu können, sind in einem zum Ereignismodell gehörenden Listener-Interface beschrieben.

Listener

Um dies unabhängig von der Betätigung des Menüpunktes auszuführen, werden die Aktionen nicht direkt in die durch den Menüpunkt ausgelöste Aktion eingefügt. Vielmehr wird am Timer ein Listener angemeldet, der die anfallende Arbeit übernimmt. Damit werden die Anweisungen relativ unabhängig von weiteren Änderungen am Programm ausgeführt. Es ist so z.B. möglich, den Timer auf andere Weise weiterzuschalten, als mit dem eingefügten Menüpunkt, ohne das das einen Effekt auf den Listener und die in ihm enthaltenen Anweisungen hätte.

Ein Listener wird am Timer mit addTimerListener angemeldet. Er muß das TimerListener-Interface implementieren. Eine Klasse, die das bereits erledigt und deren Objekte somit als Listener verwendet werden können, ist TimerAdapter. Diese Klasse enthält nur leere Methoden, die nach Bedarf angepaßt werden können.

TimerListener und TimerAdapter

Um den Listener nur einmal, und zwar bei der Initialisierung des Büros, anzumelden, wird der nötige Code als letzte Anweisung in den Konstruktor von Office eingefügt. Der Listener wird als anonyme Unterklasse von TimeAdapter implementiert:


  Shop.getTheShop().getTimer().addTimerListener(
    new TimerAdapter()
    {
    });
        

In dem so angemeldeten Listener muß nun die Methode onGoneAhead angepaßt werden, die vom Timer aufgerufen wird, wenn die Zeit weitergeschaltet wurde. Es wird also folgendes in die anonyme Klasse eingefügt:


  public void onGoneAhead(TimerEvent trEvt)
  {
  }
        

Die verwendeten Klassen bzw. Interfaces TimerListener, TimerAdapter und TimerEvent sind in sale.events enthalten. Das Paket wird daher importiert:


  import sale.events.*;
        

In der onGoneAhead-Methode wird das FormSheet neu dargestellt, wenn die Zeit weitergeschaltet wurde. Das kann nur geschehen, wenn der SalesPoint ein benutzbares Display zur Verfügung stellt. Das sollte hier der Fall sein, da der Manager im Büro ist und somit sein Display zur Verfügung stellt.

Weiterhin ist es theoretisch möglich, daß die zu verwendende Methode setFormSheet eine InterruptedException erzeugt. Diese Exception tritt hier jedoch nicht auf, da die Methode getDefaultFormSheet ein FormSheet zurückgibt, das die Methode setFormSheet nicht blockiert. Es ist jedoch nur einer blockierten setFormSheet-Methode möglich, die Exception zu erzeugen. Im catch-Block müssen daher keine Maßnahmen der Fehlerbehandlung durchgeführt werden.

Daher wird eine if-Anweisung eingefügt, in deren positivem Zweig ein try-Block enthalten ist:


  if (hasUseableDisplay(null)) {
    try {
      setFormSheet(null, getDefaultFormSheet());
    }
    catch(InterruptedException ie) {
    }
  }
        

Der Listener für die Weiterschaltung der Zeit ist fertig implementiert. Jetzt werden noch Listener benötigt, die bei Veränderung des Geldbestandes im Münzschacht die Oberfläche neu darstellen. In diesem Fall ist offensichtlich der Stock mit dem Namen "coin slot" Quelle der Ereignisse. Dieser Stock wurde als Instanz der Klasse MoneyBagImpl angelegt, die das ListenableStock-Interface implementiert. An Stocks, die dieses Interface implementieren, kann sich ein StockChangeListener mit addStockChangeListener an- und mit removeStockChangeListener wieder abmelden. Eine Implementierung dieses Listeners ist mit StockChangeAdapter bereits im Framework vorhanden. Ein Objekt dieser Klasse kann sich an- und abmelden und implementiert alle vom Interface geforderten Funktionen. Diese Funktionen sind jedoch nicht leer und können in einer Unterklasse angepaßt werden. Auf diese Weise ist es möglich, in einer eigenen Listenerklasse nur die Funktionen zu implementieren, die auf das gewünschte Ereignis reagieren und alle anderen vom Interface geforderten Methode vom Adapter zu erben.

StockChangeListener und StockChangeAdapter

Um StockChangeAdapter verwenden zu können, wird das Paket data.events importiert, für MoneyBagImpl wird data.ooimpl benötigt:


  import data.events.*;
  import data.ooimpl.*;
        

Es wird im Konstruktor von Office ein Listener am Münzschacht angemeldet, der von StockChangeAdapter abgeleitet ist:


  ((MoneyBagImpl)Shop.getTheShop().getStock(
    "coin slot")).addStockChangeListener(new StockChangeAdapter()
    {
    });
        

In der anonymen Klasse muß die Methode überschrieben werden, die auf das endgültige Hinzufügen von Einträgen in den Stock reagiert. Es handelt sich um die Methode commitAddStockItems, der von der Quelle das Ereignis übergeben wird:


  public void commitAddStockItems(StockChangeEvent sce)
  {
  }
        

In dieser Methode muß sichergestellt werden, daß nicht gerade ein Prozeß abläuft. Dieser stellt seine eigenen FormSheets bereit, die nicht einfach durch ein neues Standard-FormSheet überschrieben werden dürfen. Sollte ein Prozeß laufen, wird bei dessen Ende in jedem Fall das Standard-FormSheet gesetzt, so daß in diesem Fall keine weiteren Schritte zu ergreifen sind. Den gerade ablaufenden Prozeß erhält man über die Methode getCurrentProcess. Liefert sie null, läuft kein Prozeß.

Eine weitere Bedingung, die es zu beachten gilt, ist, daß das Büro überhaupt ein gültiges Display hat. Das ist nur dann der Fall, wenn ein Benutzer angemeldet ist. Tritt das abzufangende Ereignis ein, wenn gerade niemand angemeldet ist, so wird die entsprechende Methode im Listener trotzdem aufgerufen und darf nicht versuchen, ein FormSheet zu setzen. Die Überprüfung, ob ein benutzbares Display vorhanden ist, erfolgt mit der Methode hasUsableDisplay. Ihr wird der anfragende Prozeß übergeben. Da die Anfrage aus keinem Prozeß heraus stattfindet, ist der Parameter in diesem Fall null.

Wenn kein Prozeß läuft, aber ein Display vorhanden ist, so kann über setFormSheet das neue FormSheet gesetzt werden. Dazu sind der Prozeß, hier also null, und das FormSheet selbst zu übergeben. Diese Anweisung wird in einen try-Block geschrieben, da sie eine InterruptedException erzeugen könnte. Da dieser Fall normalerweise aber nicht eintreten wird, wird im catch-Block lediglich eine Fehlermeldung auf die Standardfehlerausgabe geschrieben. In die Methode wird folgender Code eingefügt:


  if (getCurrentProcess()==null && hasUseableDisplay(null))
    try {
      setFormSheet(null, getDefaultFormSheet());
    }
    catch(InterruptedException iexc) {
      System.err.println("Update interrupted");
    }
        

Analog dazu wird die Methode commitRemoveStockItems in die anonyme Klasse eingefügt. Diese reagiert, wenn vom Geldbestand des Münzschachtes Beträge abgezogen werden.


  public void commitRemoveStockItems(StockChangeEvent sce) 
  {
    if (getCurrentProcess()==null && hasUseableDisplay(null))
      try {
        setFormSheet(null, getDefaultFormSheet());
      }
      catch(InterruptedException iexc) {
        System.err.println("Update interrupted");
      }
  }
        

Damit sind die Reaktionen auf eine Änderung des Geldbestandes adäquat umgesetzt.

Im folgenden sollen im RentProcess und im GiveBackProcess die Vorgänge mitprotokolliert werden. Am Beispiel des RentProcess werden die Schritte erklärt, die das Mitloggen von Aktivitäten der Kunden realisieren. Im GiveBackProcess werden diese Schritte analog angewendet.

Mitloggen

Um Klassen aus dem Paket log verwenden zu können, muß die Klasse RentProcess um eine import-Anweisung erweitert werden:


  import log.*;
        

Am Ende der Datei RentProcess.java wird die Klasse MyLogEntry implementiert, die einen selbstdefinierten Log-Eintrag beschreibt.

LogEntry


  class MyLogEntry extends LogEntry
  {
  }
        

Es soll mitgeloggt werden, welcher Kunde welches Video zu welcher Zeit ausgeliehen hat. Es müssen entsprechende Variablen dafür deklariert werden:


  String name;
  String customerID;
  Object date;
        

Im Konstruktor werden die Variablen initialisert:


  public MyLogEntry(String name, String customerID, Object date)
  { 
    super(); 
    this.name       = name;
    this.customerID = customerID;
    this.date       = date;
  }        
        

Das Aussehen eines Log-Eintrags wird durch die folgende toString-Methode bestimmt:


  public String toString()
  {
    return name + 
      " rent by customer " + customerID + 
      " (ID) at turn " + date;
  }
        

Die Klasse MyLogEntry ist fertiggestellt.

Nach der Klasse RentProcess wird in der Datei RentProcess.java die Klasse MyLoggable implementiert. Sie definiert die Schnittstelle zum Mitloggen. Es wird vom Interface Loggable geerbt, das ein Objekt repräsentiert, das geloggt werden kann. Bestandteil dieser Klasse ist der Konstruktor. In ihm werden die für die Erstellung des Log-Eintrags wichtigen Variablen übergeben. Diese müssen zuerst deklariert werden:

Loggable


  class MyLoggable implements Loggable 
  {
    String name;
    String customerID;
    Object date;
  }  
        

Um die Variablen zu initialisieren, wird der eben erwähnte Konstruktor erstellt:


  public MyLoggable(String name, String customerID, Object date)
  { 
    super();
    this.name       = name;
    this.customerID = customerID;
    this.date       = date;
  }
        

Der erste Konstruktor erwartet Parameter, die schon in der entsprechenden Form übergeben werden. Der zweite Konstruktor läßt als ersten Parameter auch einen Eintrag aus dem DataBasket des Kunden zu, aus dem dann im Konstruktor die relevanten Daten geholt werden.

Die im Interface Loggable definierte Methode getLogData muß zur Vervollständigung der Klasse überschrieben werden. Sie holt sich den Log-Eintrag mit Hilfe der Klasse MyLogEntry:


  public LogEntry getLogData()
  {
    return new MyLogEntry(name, customerID, date);
  }
        

Um den Ausleihvorgang in die Log-Datei einzutragen, wird in der toPayingTransition der Klasse RentProcess, vor der endgültigen Übernahme der Videokassetten in das Kundenkonto des Kunden (nach Beginn der for-Schleife: for (; number-- > 0;)), folgender Code eingefügt:

Mitloggen im RentProcess


  try {
    Log.getGlobalLog().log(new MyLoggable(
      cassetteItem.getSecondaryKey(),
      customer.getCustomerID(),
      date));
  }
  catch (LogNoOutputStreamException lnose) {
  }
  catch (IOException ioe) {
  }
        

log erzeugt zwei verschiedene Exceptions, die hier jeweils explizit abgefangen werden (daher die zwei catch-Blöcke).

Die IOexception benötigt noch das Paket java.io:


  import java.io.*;
        

Am Ende der Klasse RentProcess muß die Methode getLogGate implementiert werden, die das LogGate übergibt. Da beim Beenden des Prozesses jedoch kein Log-Eintrag geschrieben werden soll, wird das StopGate zurückgeliefert.

LogGate


  public Gate getLogGate()
  {
    return getStopGate();
  }
        

Um die Log-Datei einsehen zu können, wird ein weiterer Menüpunkt in der Methode getDefaultMenuSheet der Klasse Office hinzugefügt:


  msSubMenu.add(new MenuSheetItem("See log file", new sale.Action()
  {
    public void doAction(SaleProcess p, SalesPoint sp)
    {
    }
  }));
        

Zum Anzeigen von Log-Dateien wird vom Framework bereits ein FormSheet zur Verfügung gestellt: LogTableForm. Der Konstrutor dieses FormSheets benötigt als Parameter mindestens einen Titel und einen LogInputStream Um einen LogInputStream zu erzeugen, wird wiederum ein FileInputStream benötigt. Es wird mit Hilfe des Dateinamens der Log-Datei ein FileInputStream, mit dessen Hilfe ein LogInputStream und mit diesem ein LogTableForm erstellt. Danach wird der nicht benötigte "Cancel"-Button entfernt -- die Log-Tabelle kann lediglich zur Kenntnis genommen werden -- und das FormSheet kann mittels setFormSheet angezeigt werden. Ein Anpassen des "Ok"-Buttons ist nicht notwendig, da die standardmäßig ausgeführte Aktion bereits aus einem einfachen Schließen des FormSheets besteht.

LogTableForm und LogInputStream

Die Befehlssequenz wird in einen try-Block geschrieben, da einige der verwendeten Methoden Ausnahmen auslösen können. In die doAction-Methode wird folgendes eingefügt:


  try {
    FileInputStream fis = new FileInputStream("machine.log");
    LogInputStream  lis = new LogInputStream(fis);
    LogTableForm    ltf = new LogTableForm("View log file", lis);

    ltf.removeButton(FormSheet.BTNID_CANCEL);
    setFormSheet(null, ltf);
  }
        

Es müssen für die verwendeten Klassen die benötigten Pakete importiert werden:


  import log.*;
  import log.stdforms.*;
  import java.io.*;
        

Außerdem müssen die Ausnahmen abgefangen werden. Der Konstruktor des FileInputStreams könnte eine FileNotFoundException auslösen, der Konstruktor von LogInputStream eine IOException und setFormSheet eine InterruptedException. In der Praxis dieses Programms wird das FormSheet jedoch nicht unterbrochen, da keine externen Abläufe auf das Office zugreifen.

Es werden drei catch-Blöcke an den try-Block angehangen:


  catch (FileNotFoundException fnfexc) {
    try {
      setFormSheet(null, new MsgForm("Error", "Log file not found."));
    }
    catch (InterruptedException inner_iexc) {
    }
  }
  catch (IOException ioexc) {
    try {
      setFormSheet(null, new MsgForm("Error", 
        "Log file corrupt. It might be empty."));
    }
    catch (InterruptedException inner_iexc) {
    }
  }
  catch (InterruptedException iexc) {
    try {
      setFormSheet(null, new MsgForm("Error", iexc.toString()));
    }
    catch (InterruptedException inner_iexc) {
    }
  }
        

Damit hat der Manager die Möglichkeit des Log-File einzusehen.

Hier der Quelltext der in diesem Kapitel geänderten Klassen:

Der Manager Bestand einsehen und editieren

last modified on 24.09.2001
by kk15 and ch17