Videoautomat - Der Ausleihprozess

Im folgenden Kapitel soll der eigentliche Ausleihvorgang beschrieben werden.

Ausgehend vom Anmeldeprozess gelangt der Kunde zum Startzustand des Ausleihprozesses, in welchem die gewünschten Videos ausgewählt werden können. Wurde eine Auswahl getroffen und bestätigt, gelangt man in den Bezahlzustand, wo die entsprechende Geldsumme beglichen werden muss. Sind ausreichend Münzen und/oder Scheine eingezahlt worden, kann zum Folgezustand gewechselt werden. In diesem werden die gewählten Videos, sowie etwaiges Wechselgeld angezeigt. Wurde die Anzeige bestätigt, werden die Änderungen durch Wechsel zum Commit-Gate persistent gemacht. Befindet sich der Prozess im Bezahlzustand, kann zum vorherigen Ausgangszustand zurückgewechselt werden. Die bereits eingeworfenen Münzen werden in diesem Fall wieder ausgegeben. Im Startzustand kann der Ausleihzustand durch Wechsel zum Rollback-Gate abgebrochen werden. Abbildung 10.1 verdeutlicht den Ablauf des Prozesses anhand eines Zustandsdiagramms.



Der Ausleihprozess als Zustandsdiagramm
Abbildung 10.1: Der Ausleihprozess als Zustandsdiagramm

Die Gates Rollback, Commit, Log und Stop sowie die Übergänge zwischen diesen sind in der Klasse SaleProcess vordefiniert (siehe Technischer Überblick).


Definition der Zustände

Als erstes wird analog zum Anmeldeprozess eine neue SaleProcess-Spezialisierung geschaffen mit entsprechenden Rückgabemethoden für die benötigten Zustände. Natürlich betrifft dies nur die drei selbstdefinierten Zustände und nicht die von der Oberklasse bereits implementierten. Insbesondere ist wieder darauf zu achten, dass der Startzustand über die Methode getInitialGate() zurückgeliefert wird.

package videoautomat;
public class SaleProcessRent extends SaleProcess {
    private UIGate uig_offer = new UIGate(null, null);
    private UIGate uig_pay = new UIGate(null, null);
    private UIGate uig_confirm = new UIGate(null, null);
    public SaleProcessRent() {
        super("SaleProcessRent");
    }
    protected Gate getInitialGate() {
        return uig_offer;
    }
    private Gate getPayGate() {
        return uig_pay;
    }
    private Gate getConfirmGate() {
        return uig_confirm;
    }
}
		

Die Initialisierung des Ausleihprozesses wird im Hauptzustand der Klasse SaleProcessLogOn vorgenommen. Der Ausleihbutton im Formular des Hauptzustands wird mit einer Aktion verknüpft, die auf dem aktuellen SalesPoint (dem Videoautomaten) mittels runProcess(SaleProcess p, DataBasket db) einen neuen Prozess startet.

Bemerkenswert ist dabei, dass die Klasse SalesPoint das komplette Prozessmanagement übernimmt. Intern wird durch diesen Aufruf der bis dato laufende Anmeldeprozess gestoppt und auf einem Kellerspeicher abgelegt, solange bis der neugestartete Prozess terminiert. Im Anschluss wird der Anmeldeprozess wieder aufgeweckt und fährt an der Stelle fort, wo er unterbrochen wurde, nämlich am Main-Gate. Für den Programmierer laufen diese Vorgänge jedoch völlig transparent ab.

public class SaleProcessLogOn extends SaleProcess {
		.
		.
		.
    private Gate getMainGate() {
        FormSheet fs_main =
            LogOn.getMainFormSheet(
                VideoShop.getVideoStock(),
                uig_main,
                false,
                new TEDVideoStock());
        fs_main.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(FormSheet fs) {
                fs.getButton(LogOn.FB_RENT).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        sp.runProcess(new SaleProcessRent(), new DataBasketImpl());
                    }
                });
                fs.getButton(LogOn.FB_LOGOUT).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        sp.detachUser();
                        uig_main.setNextTransition(
                            GateChangeTransition.CHANGE_TO_STOP_GATE);
                    }
                });
            }
        });
        uig_main.setFormSheet(fs_main);
        return uig_main;
    }
		.
		.
		.
}
		

Außerdem ist darauf zu achten, dass hier der Methode runProcess(SaleProcess p, DataBasket db) eine Instanz der Klasse DataBasketImpl übergeben wird. Dieses Objekt wird automatisch an den betreffenden Prozess gekoppelt und für Transaktionen während des Prozessablaufs genutzt. Der Datenkorb lässt sich über die Methode getBasket() vom Prozess zurückgeben.


Transaktionen mittels einer Tabelle

Nachdem die Möglichkeit geschaffen wurde den Ausleihprozess zu starten, soll die Interaktionsmöglichkeit für den Startzustand implementiert werden, d.h. konkret der Kunde soll in die Lage versetzt werden, Videos des Sortiments auszuwählen. Das Framework bietet hierfür eine besondere Form eines Tabellenformulars an, die Klasse TwoTableFormSheet.

Ein solches Formular enthält eine linke und rechte Tabelle, jeweilig die Repräsentation einer frameworkinternen Datenstruktur. Zum Beispiel könnte die linke Seite einen Bestand B1 und die rechte Seite einen anderen Bestand B2 darstellen. Aufgrund der vorhandenen Verschiebebuttons in einem derartigen Tabellenformular ist der Anwender in der Lage, Einträge zwischen B1 und B2 hin- und herzuschieben. Das Verschieben wird gänzlich vom Formular und den beteiligten Datenstrukturen geleistet. Der Programmierer braucht sich um nichts weiter zu kümmern.

Analog zum SingleTableFormSheet gibt es in der Klasse TwoTableFormSheet zahlreiche create-Methoden, die nahezu alle Kombinationen der unterschiedlichen Datenstrukturen berücksichtigen und ein entsprechendes 2-Tabellen-Formular erzeugen. Optional kann bei der Erzeugung dieser, auf Transaktionen ausgerichteten Formulare ein Datenkorb übergeben werden.

An dieser Stelle soll kurz auf die Funktionsweise eines Datenkorbs eingegangen werden. Angenommen der Kunde wählt verschiedene Videos aus. Die gewählten Videos werden in den Leihbestand des Kunden aufgenommen. Einen Moment später überlegt er sich die Sache noch einmal und kommt zu dem Schluss, dass er das Geld doch lieber für etwas Sinnvolleres ausgeben oder seine Zeit anders nutzen will. Der Leihvorgang wird abgebrochen. Die Videos müssen nun wieder aus dem virtuellen Bestand des Kunden entfernt und in den Bestand des Automaten aufgenommen werden, aber welche? Möglicherweise existieren bereits entliehene Videos im Bestand des Kunden, die nicht entfernt werden sollen.

Ein Datenkorb hilft hierbei als eine Art Puffer. Wird beim Entfernen oder Hinzufügen von Einträgen einer Datenstruktur ein Datenkorb übergeben, zeichnet der Datenkorb die Aktion auf. Später kann ein rollback ausgeführt werden, welches sämtliche protokollierte Aktionen rückgängig macht, oder es erfolgt ein commit um die Änderungen dauerhaft zu machen. Wird beim Hinzufügen oder Entfernen statt des Datenkorbs null übergeben, werden die gemachten Änderungen sofort dauerhaft gemacht.

In einer neuen Klasse Rent des Paketes videoautomat.gui, welche für die Oberflächenprogrammierung des Ausleihprozesses gedacht ist, wird eine statische Methode definiert, die das für den Startzustand erforderliche Formular zurückgeben soll. Über eine der create-Methoden der Klasse TwoTableFormSheet wird ein Formular erzeugt mit einer Tabelle für einen CountingStock auf der linken Seite und einer Tabelle für einen DataBasket auf der rechten Seite. Die Videos sollen vorläufig nur dem Datenkorb und nicht dem Bestand des Kunden zugefügt werden. Der Methode muss darüberhinaus die UIGate-Instanz übergeben werden, mit der das Formular verknüpft werden soll. Die Komparatoren dienen der jeweiligen Ordnung der Einträge der rechten und linken Tabelle. Ebenso können TableEntryDescriptor-Objekte für beide Seiten übergeben werden. Wird an Stelle eines TEDs null übergeben, wird der Standard-TED der betreffenden Datenstruktur benutzt. Die boolesche Variable bestimmt ob nicht vorhandene Elemente des CountingStock angezeigt werden sollen.

package videoautomat.gui;
public class Rent {
    public static final int FB_RENT = 1;
    public static final int FB_CANCEL = 3;
    public static TwoTableFormSheet getRentFormSheet(
        CountingStock cs_source,
        DataBasket db,
        UIGate uigGate,
        java.util.Comparator cmp_source,
        java.util.Comparator cmp_dest,
        boolean show_zeros,
        TableEntryDescriptor ted_source,
        TableEntryDescriptor ted_dest,
        CSDBStrategy csdbs) {

        TwoTableFormSheet ttfs =
            TwoTableFormSheet.create(
                "Choose your videos!",
                cs_source,
                db,
                uigGate,
                cmp_source,
                cmp_dest,
                show_zeros,
                ted_source,
                ted_dest,
                csdbs);

        ttfs.addContentCreator(new FormSheetContentCreator() {
            public void createFormSheetContent(FormSheet fs) {
                fs.removeAllButtons();
                fs.addButton("Rent", FB_RENT, null);
                fs.addButton("Cancel", FB_CANCEL, null);
            }
        });

        return ttfs;
    }
}
		

Beim letzten Parameter handelt es sich um eine Spezialisierung der Klasse MoveStrategy, welche wie der Name schon sagt für das Verhalten beim Verschieben von Elementen zwischen den beiden Datenstrukturen verantwortlich ist. Entsprechend gibt es verschiedene Spezialisierungen von MoveStrategy, je nachdem welche Datenstruktur als Quelle und welche als Ziel dient. In diesem Fall bedarf es einer Instanz der Klasse CSDBStrategy.

Das Formular wird in der getInitialGate()-Methode des Ausleihprozesses initialisiert. Außerdem wird gleich die Aktion des Buttons zum Abbrechen des Prozesses implementiert. Diese soll einen Zustandsübergang zum Rollback-Gate einleiten, damit bereits ausgewählte Videos automatisch durch ein auf dem Datenkorb des Prozesses ausgeführtes rollback zurückgesetzt werden. Ausgehend vom Rollback-Gate wird der Prozess automatisch zum Log-Gate und von dort zum Stop-Gate geleitet, wo der Prozess terminiert.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    protected Gate getInitialGate() {
        CSDBStrategy csdbs = new CSDBStrategy();
        TwoTableFormSheet ttfs_rent =
            Rent.getRentFormSheet(
                VideoShop.getVideoStock(),
                getBasket(),
                uig_offer,
                null,
                null,
                false,
                new TEDVideoStock(),
                null,
                csdbs);

        ttfs_rent.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(FormSheet fs) {
                fs.getButton(Rent.FB_CANCEL).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        uig_offer.setNextTransition(
                            GateChangeTransition.CHANGE_TO_ROLLBACK_GATE);
                    }
                });

            }
        });
        return uig_offer;
    }
		.
		.
		.
}
		

Der Methode zum Erzeugen des Formulars wird der Videobestand sowie der Datenkorb des Prozesses übergeben. Für die Komparatoren wird null übergeben, ebenso für den TableEntryDescriptor der Datenkorbtabelle. Für die Bestandstabelle wird eine Instanz von TEDVideoStock erzeugt und übergeben. Der letzte Parameter ist ein neu erzeugtes Objekt der Klasse CSDBStrategy.

Nach erfolgter Übersetzung und Start der Anwendung kann man einen Kunden oder den Administrator anmelden und durch Betätigung des rent-Button einen Verleihprozess starten. Es öffnet sich das neue Tabellenformular, worin man Einträge zwischen rechter und linker Seite hin- und her verschieben kann. Wählt man anschließend den cancel-Button soll der Prozess eigentlich terminieren, doch stattdessen erscheint eine Fehlermeldung. Der Grund dafür ist der Wechsel zum Log-Gate in welchem versucht wird, den Prozessnamen und die aktuelle Zeit in die globale Logdatei zu schreiben. Es wurde jedoch noch gar keine Logdatei definiert. Daher wird bei dem Schreibversuch eine Exception ausgelöst. Die Definition der globalen Logdatei lässt sich schnell nachholen.

Objekte der Klasse Log des Frameworks repräsentieren Protokolldateien. Es können verschiedene Logdateien für diverse Zwecke angelegt werden. Darüberhinaus lässt sich eine globale Logdatei über entsprechende statische Methoden in der Klasse Log setzen und wiedergeben. Im Konstruktor von VideoShop wird die globale Protokolldatei der Anwendung definiert.

public class VideoShop extends Shop {
		.
		.
		.
    public static final String FILENAME = "automat.log";
		.
		.
		.
    public VideoShop() {
		.
		.
		.
        try {
            Log.setGlobalOutputStream(new FileOutputStream(FILENAME, true));
        } catch (IOException ioex) {
            System.err.println("Unable to create log file.");
        }
    }
		.
		.
		.
}
		

Bei einem erneuten Start der Anwendung mit existenter Protokolldatei lässt sich der Ausleihprozess ohne Probleme abbrechen. Werden während des Ausleihprozesses Videos aus dem Bestand des Automaten entfernt, so ist nach einem Abbruch und Neustart des Prozesses dank der Nutzung des Datenkorbs und des Rollback-Gates alles wie zuvor.

Dennoch lauert ein Schönheitsfehler im Verborgenen. Wählt man für die Anzahl der zu verschiebenden Elemente aus dem Bestand mehr als vorhanden sind, kommt es zu einem Fehler. Die CSDBStrategy des Formulars erkennt den Fehler und zeigt eine Meldung an. Leider terminiert der Prozess nach Bestätigung der Meldung. Um dies zu umgehen, lässt sich die Fehlerbehandlung der verwendeten Strategie ändern. In der Methode getInitialGate() des Ausleihprozesses muss der Fehlerbehandler des CSDBStrategy-Objekts neu gesetzt werden, bevor das Objekt zur Formularerzeugung genutzt wird. Sinnvollerweise existiert eine ErrorHandler-Konstante in der Klasse FormSheetStrategy, die nur eine pop-up Nachricht anzeigt und sonst keine Änderungen vornimmt. Insbesondere wird der Ausleihprozess nicht einfach beendet. Diese Konstante soll hier verwendet werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    protected Gate getInitialGate() {
        CSDBStrategy csdbs = new CSDBStrategy();
        csdbs.setErrorHandler(FormSheetStrategy.MSG_POPUP_ERROR_HANDLER);
		.
		.
		.
        return uig_offer;
    }
		.
		.
		.
}
		

Die Bezahlung anzeigen

Die Auswahl der Videos kann jetzt getroffen werden. Im nächsten Schritt müssen die Verkaufskosten der gewählten Videos aufsummiert werden. Die erforderliche Summe soll dann im Bezahlzustand durch den Kunden beglichen werden.

Da für die Anwendung kein realer Automat und damit auch kein Erkennungsmechanismus für eingeworfene Münzen zur Verfügung steht, muss bei der Bezahlung ein klein wenig getrickst werden. Der Einwurf des Geldes wird durch ein TwoTableFormSheet mit dem Währungskatalog als Quelle und einem Geldbeutel als Ziel simuliert.

In der Klasse Rent wird eine neue Rückgabemethode für das erforderliche Formular definiert. Der Aufbau der Methode unterscheidet sich nur geringfügig von der für die Konstruktion des Startformulars vorgesehenen. Zum Einen unterscheiden sich Quelle und Ziel und damit auch die benutzte MoveStrategy, was jedoch für den Programmierer kaum einen Unterschied macht. Zum Anderen muss, abgesehen von den Tabellen, der zu zahlende Preis angezeigt werden. Außerdem ist darauf zu achten, dass wiederum der Datenkorb als Puffer zwischen Quelle und Ziel übergeben wird.

public class Rent {
		.
		.
		.
    public static final int FB_PAY = 2;
		.
		.
		.
    public static TwoTableFormSheet getPayFormSheet(
        Catalog c_source,
        CountingStock cs_dest,
        DataBasket db,
        UIGate uig,
        java.util.Comparator cmp_source,
        java.util.Comparator cmp_dest,
        boolean show_zeros,
        TableEntryDescriptor ted_source,
        TableEntryDescriptor ted_dest,
        CCSStrategy ccss,
        final String value) {

        TwoTableFormSheet ttfs =
            TwoTableFormSheet.create(
                "Throw the money in the slot, please.",
                c_source,
                cs_dest,
                db,
                uig,
                cmp_source,
                cmp_dest,
                show_zeros,
                ted_source,
                ted_dest,
                ccss);

        ttfs.addContentCreator(new FormSheetContentCreator() {
            public void createFormSheetContent(FormSheet fs) {

                JComponent jc = new JPanel();
                jc.setLayout(new BoxLayout(jc, BoxLayout.Y_AXIS));
                jc.add(Box.createVerticalStrut(10));
                jc.add(new JLabel("You have to pay: " + value));
                jc.add(Box.createVerticalStrut(10));
                jc.add(fs.getComponent());

                fs.setComponent(jc);
                fs.removeAllButtons();
                fs.addButton("Pay", FB_PAY, null);
                fs.addButton("Cancel", FB_CANCEL, null);
            }
        });
        return ttfs;
    }
}
		

Anpassungen vorhandener Formulare sind, wie im Beispiel zu sehen ist, einfach zu realisieren. Man lässt sich die Anzeigefläche des Formulars (die Buttonleiste ist davon ausgenommen) über die Methode getComponent() zurückgeben. Diese Komponente kann man dann an beliebiger Stelle eines selbstdefinierten JComponent-Objekts, z.B innerhalb einer JPanel-Instanz, platzieren. Die neu definierte Komponente wird über setComponent zum neuen Anzeigeobjekt des Formulars.

Bevor das Formular in der entsprechenden Methode des Ausleihprozesses initialisiert wird, soll ein Komparator für die Geldeinträge geschrieben werden. Bei den Tabellenformularen werden, sofern bei der Initialisierung kein Komparator übergeben wird, die Einträge standardmäßig nach ihrem Namen sortiert. Bei den Geldeinträgen ist diese Art der Anordnung jedoch irritierend, da beispielsweise auf 1-Cent gleich 1-Euro folgt. Wünschenswert wäre eine Anordnung nach den Werten. Folgende Implementation des Interface Comparator leistet dies.

package videoautomat;
public class ComparatorCurrency implements Comparator, Serializable {
    public ComparatorCurrency() {
    }
    public int compare(Object arg0, Object arg1) {
        if (arg0 instanceof CatalogItem) {
            return ((CatalogItem) arg0).getValue().compareTo(
                ((CatalogItem) arg1).getValue());
        }
        if (arg0 instanceof CountingStockTableModel.Record) {
            Value v1 =
                ((CountingStockTableModel.Record) arg0)
                    .getDescriptor()
                    .getValue();
            Value v2 =
                ((CountingStockTableModel.Record) arg1)
                    .getDescriptor()
                    .getValue();
            return v1.compareTo(v2);
        }
        return 0;
    }
}
		

Die zu implementierende Methode compare(Object o1, Object o2) untersucht, ob es sich bei den zu vergleichenden Objekten um Tabelleneinträge eines Katalogs (Währungskatalog) oder eines zählenden Bestands (Geldbeutel) handelt, und delegiert den Vergleich an die zu untersuchenden Werte. Wichtig ist, dass der Komparator das Interface Serializable implementiert. Damit signalisiert man Speicherfähigkeit des Objekts.

Jetzt lässt sich das Formular für den Geldtransfer in der Methode getPayGate() initialisieren. Dafür wird zusätzlich eine Variable des Typs MoneyBagImpl deklariert, sowie ein NumberValue. Der deklarierte Geldbeutel wird an Stelle des Automaten-Geldbeutels verwendet, da es gewiss nicht wünschenswert ist, dass der Kunde die gesamte Barschaft im Automaten zu Gesicht bekommt. Der numerische Wert wird zur Repräsentation des zu zahlenden Betrages genutzt. Die Berechnung des Betrags erfolgt während des Übergangs zu diesem Bezahlzustand und wird später implementiert.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private MoneyBagImpl mb_temp =
        new MoneyBagImpl("mb_user", VideoShop.getCurrency());
    private NumberValue nv_sum = null;
		.
		.
		.
    private Gate getPayGate() {
        String value = VideoShop.getCurrency().toString(nv_sum);
        CCSStrategy ccss = new CCSStrategy();
        ccss.setErrorHandler(FormSheetStrategy.MSG_POPUP_ERROR_HANDLER);
        TwoTableFormSheet ttfs_pay =
            Rent.getPayFormSheet(
                VideoShop.getCurrency(),
                mb_temp,
                getBasket(),
                uig_pay,
                new ComparatorCurrency(),
                new ComparatorCurrency(),
                false,
                null,
                null,
                ccss,
                value);
        return uig_pay;
    }
		.
		.
		.
}
		

Wie zu sehen ist, wird der Währungskatalog und der neu initialisierte Geldbeutel, sowie der Datenkorb des Prozesses für die Erzeugung des Formulars übergeben. Der Betrag wird über den Währungskatalog zu einem entsprechend formatierten String gewandelt und der Erzeugermethode als letzter Parameter übergeben.


Den Datenkorb einteilen

Bevor der Zustandsübergang zum Bezahlzustand definiert und damit erst die Möglichkeit geschaffen wird, das so eben erzeugte Formular zu betrachten, soll folgende Überlegung angestellt werden:

Der Datenkorb des Ausleihprozesses wird für die Transaktionen der Videos und des Geldes genutzt. Kommt es zu einem Zustandswechsel vom Pay-Gate zurück zum Initial-Gate, müssen die Geldtransaktionen rückgängig gemacht werden, nicht aber die Videotransaktionen. Der Aufruf von rollback(), in Bezug auf den Datenkorb, bewirkt jedoch ein Zurücksetzen aller Transaktionen, die mit diesem getätigt wurden. Abhilfe schafft hier die Definition von Sub-Datenkörben. Durch die Methode setCurrentSubBasket(String s) kann man einen solchen bestimmen. Ein Rollback oder Commit lässt sich begrenzt auf einem Sub-Datenkorb ausführen.

Es werden zwei Strings zur Identifikation der Sub-Datenkörbe am Anfang der Klasse SaleProcessRent definiert und vor der Initialisierung des Formulars für den Startzustand wird der Datenkorb-Abschnitt für die Videos bestimmt.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private static final String SUB_SHOP_VIDEO = "videos_cs";
    private static final String SUB_TMP_MONEY = "money_temp";
		.
		.
		.
    protected Gate getInitialGate() {
        getBasket().setCurrentSubBasket(SUB_SHOP_VIDEO);
		.
		.
		.
        return uig_offer;
    }
		.
		.
		.
}
		

Jetzt kann der Übergang vom Start- zum Bezahlzustand in einer Methode getSumUpTransition definiert werden. Während des Zustandsübergangs wird der Inhalt des Datenkorbs aufsummiert und der resultierende Wert der für diesen Zweck definierten Variablen zugewiesen. Entspricht die Summe einem Wert von null, wird das Initial-Gate, sonst das Pay-Gate zurückgegeben.

Die Methode für das Aufsummieren des Sub-Datenkorbs erwartet den Namen desselben. Außerdem bedarf es eines DataBasketCondition-Objekts, sofern nur gewisse Einträge des Datenkorbs untersucht werden sollen. Sollen alle Einträge berücksichtigt werden, wie in diesem Fall, kann null übergeben werden. Ferner braucht die Methode ein BasketEntryValue-Objekt das über die Methode getEntryValue(DataBasketEntry dbe) den Wert der Datenkorbeinträge liefert, der aufsummiert werden soll. In diesem Fall wird die Implementatierung des Interface BasketEntryValue gleich im Methodenaufruf vorgenommen. Als letzten Parameter erwartet die Methode für das Aufsummieren des Sub-Datenkorbs einen Startwert. von dem ausgegangen werden soll. In diesem Fall ist es ein IntegerValue, das den Wert null repräsentiert.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Transition getSumUpTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
                nv_sum = (NumberValue) getBasket().sumSubBasket(SUB_SHOP_VIDEO,
                        null, new BasketEntryValue() {
                            public Value getEntryValue(DataBasketEntry dbe) {
                                try {
                                    CatalogItem ci = VideoShop
                                            .getVideoCatalog().get(
                                                    dbe.getSecondaryKey(),
                                                    null, false);
                                    int count = ((Integer) dbe.getValue())
                                            .intValue();
                                    return ((QuoteValue) ci.getValue())
                                            .getOffer().multiply(count);
                                } catch (VetoException e) {
                                    e.printStackTrace();
                                    getBasket().rollback();
                                }
                                return null;
                            }
                        }, new IntegerValue(0));
                if (nv_sum.isAddZero())
                    return getInitialGate();
                getBasket().setCurrentSubBasket(SUB_TMP_MONEY);
                return getPayGate();
            }
        };
    }
}
		

Zu beachten ist, dass bei einem erfolgreichen Wechsel zum Bezahlzustand vorher noch der Sub-Datenkorb für die Geldtransaktionen gesetzt wird.

Der Zustandsübergang wird in der Aktion des Bezahl-Buttons in der Methode getInitialGate() eingeleitet.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    protected Gate getInitialGate() {
		.
		.
		.
        ttfs_rent.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(FormSheet fs) {
                fs.getButton(Rent.FB_RENT).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        uig_offer.setNextTransition(getSumUpTransition());
                    }
                });
		.
		.
		.
            }
        });
        return uig_offer;
    }
		.
		.
		.
}
		

Der Wechsel vom Start- zum Bezahlzustand ist damit möglich. Der Übergang zurück zum Startzustand lässt sich sehr einfach implementieren. Es wird wiederum eine Methode für die Rückgabe der entsprechenden Transition definiert. Diese muss lediglich die Geldtransaktionen über ein Rollback auf dem Sub-Datenkorb rückgängig machen und anschließend das Initial-Gate zurückgeben. Das Setzen des Übergangs in der Aktion des Abbrechen-Buttons wird in der Methode getPayGate() vorgenommen.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Gate getPayGate() {
		.
		.
		.
        ttfs_pay.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(final FormSheet fs) {
                fs.getButton(Rent.FB_CANCEL).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        uig_pay.setNextTransition(getPayRollbackTransition());
                    }
                });
            }
        });
        return uig_pay;
    }
		.
		.
		.
    private Transition getPayRollbackTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
                getBasket().rollbackSubBasket(SUB_TMP_MONEY);
                return getInitialGate();
            }
        };
    }
}
		

Nach erfolgter Übersetzung und Ausführung können die neuen Features des Programms getestet werden.


Der Button bestimmt, wann es weiter geht

Der Übergang vom Bezahl- zum Bestätigungszustand soll erst dann ermöglicht werden, wenn genügend Geld "eingeworfen" wurde. Praktisch wird dies dadurch realisiert, dass der entsprechende Button solange deaktiviert bleibt, bis die Summe der eingeworfenen Münzen und Scheine dem zu zahlenden Betrag entspricht oder diesen übersteigt.

Die Überprüfung der Summe nach jeder Geldtransaktion im Bezahlzustand lässt sich am einfachsten mit Hilfe eines StockChangeListeners realisieren. Ein solcher Zuhörer ist Bestandteil eines speziellen Observer-Entwurfsmusters, dem sogenannten Event-Delegations-Muster, welches häufig in Java insbesondere bei der Oberflächenprogrammierung zum Einsatz kommt. Das Prinzip ist denkbar einfach. Einem Objekt, in diesem Fall dem Geldbeutel, können diverse Zuhörer (hier StockChangeListener), die sich für Änderungen dieses Objekts interessieren, zugeordnet werden. Die Zuhörer implementieren ein zuvor definiertes Schnittstellenverhalten. Treten Änderungen auf (z.B. Hinzufügen von Geld in den Beutel), informiert das Objekt über die von der Schnittstelle definierten Methoden seine Zuhörerschaft.

Das Interface StockChangeListener definiert verschiedenste Methoden bezogen auf die Veränderungen eines Bestandes. Hier sind nur die Methoden addedStockItems(StockChangeEvent e) und removedStockItems(StockChangeEvent e) von Interesse. Erstere soll in diesem Fall überprüfen, ob die Summe im Geldbeutel ausreicht und wenn dies der Fall ist, den Bezahl-Button aktivieren. Letztere soll prüfen, ob zuwenig Geld im Bestand steckt und ggf. den Button deaktivieren. Um nicht alle weiteren Methoden des Interface implementieren zu müssen, wird auf die Klasse StockChangeAdapter zurückgegriffen, in der alle Methoden der Schnittstelle leer vordefiniert sind. In einer neuen Methode wird die Spezialisierung von StockChangeAdapter implementiert und dem Geldbeutel, dem "zugehört" werden soll, zugefügt. Der Methode muss der Button, der de- bzw. aktiviert werden soll, übergeben werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private void setButtonStockListener(final FormButton fb) {
        StockChangeAdapter sca = new StockChangeAdapter() {
            public void addedStockItems(StockChangeEvent e) {
                if (mb_temp
                    .sumStock(getBasket(), new CatalogItemValue(), new IntegerValue(0))
                    .compareTo(nv_sum)
                    >= 0)
                    fb.setEnabled(true);
            }
            public void removedStockItems(StockChangeEvent e) {
                if (mb_temp
                    .sumStock(getBasket(), new CatalogItemValue(), new IntegerValue(0))
                    .compareTo(nv_sum)
                    < 0)
                    fb.setEnabled(false);
            }
        };
        mb_temp.addStockChangeListener(sca);
    }
}
		

Der hier angewandten Methode zum Aufsummieren des Geldbestands muss als erster Parameter der Datenkorb übergeben werden. Ohne diesen würden die bisher nur temporär hinzugefügten Einträge des Geldbeutels nicht mitgezählt werden. Der zweite Parameter, die CatalogItemValue-Instanz, hat etwas mit den Katalogeinträgen zu tun, auf die die jeweiligen Bestandseinträge referenzieren. Im Fall des Währungskatalogs kann die vorimplementierte Form genutzt werden. Sollten die Katalogeinträge jedoch Werte vom Typ QuoteValue besitzen, muss über die CatalogItemValue-Instanz entschieden werden, welcher Wert aufsummiert werden soll.

Der Aufruf der neuen Methode erfolgt in getPayGate(). Der Bezahlbutton wird zunächst deaktiviert.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Gate getPayGate() {
		.
		.
		.
        ttfs_pay.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(final FormSheet fs) {
		.
		.
		.
                fs.getButton(Rent.FB_PAY).setEnabled(false);
                setButtonStockListener(fs.getButton(Rent.FB_PAY));
            }
        });
        return uig_pay;
    }
		.
		.
		.
}
		

Die Videos und das Restgeld ausgeben

Es fehlen noch der Bestätigungszustand und der Übergang dorthin. In diesem Übergang müssen Einträge dem Bestand des Kunden hinzugefügt werden, die den ausgewählten Videos entsprechen. Außerdem muss das Geld, welches dem Geldbeutel des Ausleihprozesses zugefügt wurde, in den Geldbeutel des Automaten, sowie eventuelles Wechselgeld umgekehrt aus dem Automaten-Geldbeutel in den des Prozesses verschoben werden. Im Bestätigungszustand sollen die entliehenen Videos und das Wechselgeld angezeigt werden.

Es wird mit dem Zustandsübergang begonnen, der wie gehabt in einer Methode der Prozessklasse definiert wird. Zunächst wird lediglich das Hinzufügen der Videokassetten zum Bestand des Kunden implementiert. Dazu muss ein neuer Sub-Datenkorb definiert und gesetzt werden, der nur die Transaktionen in den Bestand des Kunden protokolliert. Anschließend iteriert man über alle Einträge des Sub-Datenkorbs, der die vorerst entfernten Videos des Automatenbestands enthält. Mit Hilfe der Namen der vom Iterator gelieferten Einträge erzeugt man entsprechende Instanzen der Klasse VideoCassette und fügt sie mit Hilfe des Datenkorbs dem Bestand des Kunden hinzu.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private static final String SUB_USER_VIDEO = "video_ss";
		.
		.
		.
    private Transition getPayConfirmTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
                getBasket().setCurrentSubBasket(SUB_USER_VIDEO);
                StoringStock ss_user =
                    ((AutomatUser) ((SalesPoint) p.getContext()).getUser())
                        .getVideoStock();
                Iterator i =
                    getBasket().subBasketIterator(
                        SUB_SHOP_VIDEO,
                        DataBasketConditionImpl.ALL_ENTRIES);
                while (i.hasNext()) {
                    VideoCassette vc =
                        new VideoCassette(
                            ((CountingStockItemDBEntry) i.next())
                                .getSecondaryKey());
                    ss_user.add(vc, getBasket());
                }
                return getConfirmGate();
            }
        };
    }
}
		

Als nächstes muss berechnet werden wieviel Wechselgeld der Kunde erhält. Anschließend kann das Geld aus dem Geldbeutel des Prozesses in den des Automaten verschoben werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Transition getPayConfirmTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
		.
		.
		.
                getBasket().setCurrentSubBasket(SUB_TMP_MONEY);
                NumberValue nv =
                    (NumberValue)
                        (
                            (NumberValue) mb_temp.sumStock(
                                getBasket(),
                                new CatalogItemValue(),
                                new IntegerValue(0))).subtract(
                        nv_sum);
                VideoShop.getMoneyBag().addStock(mb_temp, getBasket(), true);
                return getConfirmGate();
            }
        };
    }
}
		

Die Aufsummierung des eingeworfenen Geldes wird genauso wie in der Methode setButtonStockListener(final FormButton fb) getätigt. Anschließend wird der zu zahlende Betrag subtrahiert, so dass der Wert des Wechselgelds übrigbleibt. Das Verschieben der Einträge aus dem Geldbeutel des Prozesses in den des Automaten vollzieht sich durch die Methode addStock(Stock st, DataBasket db, boolean b), wobei der übergebene boolesche Parameter darüber entscheidet, ob die Einträge des übergebenen Bestandes gleichzeitig aus diesem entfernt werden sollen, so wie in diesem Fall, oder nicht.

Für den nächsten Schritt wird ein Algorithmus benötigt, der für einen gegebenen Wert die passenden Geldgrößen (das Wechselgeld) aus dem Geldbeutel des Automaten herausgibt. Dafür existiert die Methode transferMoney(MoneyBag mb, DataBasket db, NumberValue nv) in der Klasse MoneyBagImpl. Die Methode verschiebt dem übergebenen Wert entsprechend Einträge des Objekts in den übergebenen Geldbeutel mittels des Datenkorbs. Gibt es keine Kombination der vorhandenen Einträge, die dem Wert entspricht (existiert kein Wechselgeld), oder es ist nicht genügend Geld vorhanden, wird eine NotEnoughMoneyException geworfen.

Auf Basis dieser Methode lässt sich das Wechselgeld zurückgeben. Ist nicht genug bzw. kein passendes Wechselgeld verfügbar, so werden die bisher im Zustandsübergang ausgeführten Transaktionen durch Rollbacks der entsprechenden Sub-Datenkörbe rückgängig gemacht und der Prozess wechselt zurück zum Bezahlzustand.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private static final String SUB_SHOP_MONEY = "money_shop";
		.
		.
		.
    private Transition getPayConfirmTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
		.
		.
		.
                getBasket().setCurrentSubBasket(SUB_SHOP_MONEY);
                try{    
                    VideoShop.getMoneyBag().transferMoney(mb_temp, getBasket(), nv);                    
                }catch(NotEnoughMoneyException e){
                    getBasket().rollbackSubBasket(SUB_USER_VIDEO);
                    getBasket().rollbackSubBasket(SUB_TMP_MONEY);
                    getBasket().rollbackSubBasket(SUB_SHOP_MONEY);
                    getBasket().setCurrentSubBasket(SUB_TMP_MONEY);
                    return getPayGate();
                }
                return getConfirmGate();
            }
        };
    }
}
		

Es wäre jedoch wünschenswert, wenn der Kunde zumindest darüber informiert wird, dass passend gezahlt werden muss. Theoretisch ließe sich dafür ein extra Zustand definieren. Es soll aber stattdessen eine alternative Dialogmöglichkeit des Frameworks vorgestellt werden. Mittels der Klasse JDisplayDialog lässt sich ein Dialogfenster einblenden, welches ein Formular anzeigen kann.

Es wird eine neue Klasse definiert, die von JDisplayDialog erbt. Im Konstruktor dieser Klasse wird ein zuvor in der Klasse Global definiertes MsgForm mit dem Aufruf popUpFormSheet(FormSheet fs) zur Anzeige gebracht. Bei Ausführung der Aktion des Ok-Buttons soll das Formular wieder geschlossen werden.

package videoautomat;
public class DisplayMoneyStockError extends JDisplayDialog {
    public DisplayMoneyStockError() {
        super();
        FormSheet fs = Global.getNoChangeFormSheet();
        fs.addContentCreator(new FormSheetContentCreator() {
            public void createFormSheetContent(FormSheet fs) {
                fs.getButton(FormSheet.BTNID_OK).setAction(new Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        closeFormSheet();
                    }
                });
            }
        });
        try {            
            popUpFormSheet(fs);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }
}
		

Die Methode zur Erzeugung des Formulars sieht so aus:

public class Global {
		.
		.
		.
    public static FormSheet getNoChangeFormSheet() {
        return new MsgForm(
            "No change!",
            "There is not enough change in here.n"
                + "Please insert the correct amount of moneyn"
                + "or contact the hotline.");
    }
}
		

Ein Objekt der Klasse DisplayMoneyStockError muss noch in der Methode getPayConfirmTransition() initialisiert werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Transition getPayConfirmTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
		.
		.
		.
                getBasket().setCurrentSubBasket(SUB_SHOP_MONEY);
                try{    
                    VideoShop.getMoneyBag().transferMoney(mb_temp, getBasket(), nv);                    
                }catch(NotEnoughMoneyException e){
                    getBasket().rollbackSubBasket(SUB_USER_VIDEO);
                    getBasket().rollbackSubBasket(SUB_TMP_MONEY);
                    getBasket().rollbackSubBasket(SUB_SHOP_MONEY);
                    DisplayMoneyStockError dmse = new DisplayMoneyStockError();
                    getBasket().setCurrentSubBasket(SUB_TMP_MONEY);
                    return getPayGate();
                }
                return getConfirmGate();
            }
        };
    }
}
		

Die Methode für den Zustandsübergang ist damit vorläufig komplett und kann in der Aktion des Bezahl-Buttons innerhalb der Methode getPayGate() aufgerufen werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Gate getPayGate() {
		.
		.
		.
        ttfs_pay.addContentCreator(new FormSheetContentCreator() {
            protected void createFormSheetContent(final FormSheet fs) {
		.
		.
		.
                fs.getButton(Rent.FB_PAY).setAction(new sale.Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        uig_pay.setNextTransition(getPayConfirmTransition());
                    }
                });
                fs.getButton(Rent.FB_PAY).setEnabled(false);
                setButtonStockListener(fs.getButton(Rent.FB_PAY));
            }
        });
        return uig_pay;
    }
		.
		.
		.
}
		

Es fehlt nur noch die Anzeige des Bestätigungszustands. In der Klasse Rent wird eine entsprechende Methode implementiert, die ein Formular zurückgibt, das intern aus zwei SingleTableFormsheet-Instanzen erzeugt wird. Eines zeigt einen Datenkorb unter eingeschränkten Bedingungen an, die durch eine Instanz der Klasse DataBasketCondition vorgegeben werden müssen. Das Andere repräsentiert den Geldbeutel und das in ihm befindliche Wechselgeld. Die Anzeigekomponenten der beiden Instanzen werden in einem neuen JPanel-Objekt zusammengefasst. Der Cancel-Button wird entfernt, der Ok-Button beibehalten.

public class Rent {
		.
		.
		.
    public static FormSheet getConfirmFormSheet(
        final DataBasket db,
        final DataBasketCondition dbc_videos,
        final TableEntryDescriptor ted_videos,
        final CountingStock cs_money,
        final UIGate uig) {
        SingleTableFormSheet stfs_videos =
            SingleTableFormSheet.create(
                "Confirm your transaction!",
                db,
                uig,
                dbc_videos,
                ted_videos);

        stfs_videos.addContentCreator(new FormSheetContentCreator() {
            public void createFormSheetContent(FormSheet fs) {
                SingleTableFormSheet stfs_money =
                SingleTableFormSheet.create("", cs_money, uig, db);

                JComponent jc = new JPanel();
                jc.setLayout(new BoxLayout(jc, BoxLayout.Y_AXIS));
                jc.add(new JLabel("All your rented videos:"));
                jc.add(fs.getComponent());
                jc.add(new JLabel("The money you`ll get back:"));
                jc.add(stfs_money.getComponent());
                jc.add(new JLabel("Please, click Ok to confirm the transaction!"));
                fs.setComponent(jc);
                fs.removeButton(FormSheet.BTNID_CANCEL);
            }
        });

        return stfs_videos;
    }
}
		

Das neu kreierte Formular kommt in der Methode getConfirmGate() der Klasse SaleProcessRent zur Anwendung. Für die benötigte DataBasketCondition wird auf die Methode DataBasketConditionImpl.allStockItemsWithDest(Stock s) zurückgegriffen. Wie der Name schon sagt, filtert das zurückgegebene Objekt alle Einträge heraus, die als Ziel den übergebenen Bestand haben. Für den TED der Videotabelle wird die Klasse DefaultStoringStockDBETableEntryDescriptor genutzt.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Gate getConfirmGate() {
        FormSheet fs =
            Rent.getConfirmFormSheet(
                getBasket(),
                DataBasketConditionImpl.allStockItemsWithDest(
                    ((AutomatUser) ((SalesPoint) getContext()).getUser()).getVideoStock()),
                new DefaultStoringStockDBETableEntryDescriptor(),
                mb_temp,
                null);
        fs.addContentCreator(new FormSheetContentCreator() {
            public void createFormSheetContent(FormSheet fs) {
                fs.getButton(FormSheet.BTNID_OK).setAction(new Action() {
                    public void doAction(SaleProcess p, SalesPoint sp) {
                        uig_confirm.setNextTransition(
                            GateChangeTransition.CHANGE_TO_COMMIT_GATE);
                    }
                });
            }
        });

        uig_confirm.setFormSheet(fs);
        return uig_confirm;
    }
		.
		.
		.
}
		

Wie man sieht wird in der Aktion des Ok-Button der Wechsel zum Commit-Gate eingeleitet. Damit werden alle Änderungen, für die der Datenkorb benutzt wurde, auf einen Schlag persistent gemacht.

Der Ausleihprozess kann nach erfolgter Übersetzung und Ausführung komplett durchlaufen werden.

Es wird noch eine hässliche Exception geworfen, wenn der Kunde den Geldbetrag passend bezahlt und anschließend bestätigt. Die Ausnahme hat ihren Ursprung in den Untiefen des Pakets javax.swing. Irgendwie wird versucht, nicht mehr existente Tabelleneinträge zu rendern, nachdem die Transaktion zwischen dem Geldbeutel des Prozesses und dem des Automaten stattfand. Um dies zu umgehen, kann man einfach das Formular des Automaten auf null setzen, bevor die Einträge verschoben werden.

public class SaleProcessRent extends SaleProcess {
		.
		.
		.
    private Transition getPayConfirmTransition() {
        return new Transition() {
            public Gate perform(SaleProcess p, User u) {
		.
		.
		.
                try {
                    p.getContext().setFormSheet(p, null);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                VideoShop.getMoneyBag().addStock(mb_temp, getBasket(), true);
		.
		.
		.
                return getConfirmGate();
            }
        };
    }
		.
		.
		.
}
		

Der Ausleihprozess ist nun wirklich abgeschlossen. Nach einer erneuten Übersetzung und Ausführung können nach Herzenslust Videos ausgeliehen werden.


previous Die Leih-KassetteDas Protokollieren next


Valid HTML 4.01!