Videoautomat Desktop
Aus Salespoint
Inhaltsverzeichnis |
Einleitung
Frameworks erleichtern die Programmierarbeit in vielerlei Hinsicht. Sie können Datenstrukturen und Prozesse eines bestimmten Anwendungsgebietes vordefinieren und darüber hinaus einen sauberen Entwurf erzwingen. Dennoch bedeutet ihre Verwendung zunächst einen erhöhten Einarbeitungsaufwand für den Programmierer. Um diesen zu minimieren wurde die folgende Abhandlung geschrieben. Grundlage für das Verständnis dieses Tutorials ist der Technische Überblick über das Framework SalesPoint. Wer diesen bisher noch nicht gelesen hat, wird gebeten diese Abkürzung zu nehmen.
Auf Basis von SalesPoint wird exemplarisch ein Videoautomat Schritt für Schritt zusammengesetzt. Dabei wird dem Leser empfohlen die einzelnen Schritte per copy & paste selbst zu vollziehen. Darüber hinaus wird an geeigneten Stellen dazu aufgefordert, das Programm zu kompilieren und auszuführen, um den Zusammenhang von SalesPoint-Konstrukten und Anzeige zu verdeutlichen. Zunächst wird der zu entwickelnde Automat anhand der vom Automatenbetreiber formulierten Anforderung kurz umrissen.
Aufgabenstellung
Die Videothek HOMECINEMA bietet mit Hilfe eines Videoverleihautomaten einen vereinfachten 24-Stunden-Service an: Am Videoverleihautomaten erhalten registrierte, erwachsene Kunden Videobänder gegen Bezahlung. Die Rückgabe der Bänder soll ebenfalls möglich sein. Der Automat hat ein Sortiment von 10 Filmen, die in je 5 Exemplaren vorhanden sind.
Überblick
Im weiteren Verlauf, werden einige Annahmen gemacht, die kurz erläutert werden müssen. Es wird davon ausgegangen, dass die Videoautomaten von HOMECINEMA untereinander nicht vernetzt sind. Somit können die Automaten als eigenständige, vollfunktionstüchtige Verkaufs- bzw. Verleihstellen betrachtet werden und das zu entwerfende Programm repräsentiert genau einen Automaten. Ein Automat besitzt genau ein Display, vorstellbar wäre ein Touch-Screen, als Interaktionsfläche für die Kunden und den Betreiber. Eine Person meldet sich an, autorisiert durch ein Kennwort und kann entsprechend ihrer Berechtigungen Verleih-, Rückgabe- oder administrative Vorgänge vollführen. Übertragen auf die Framework-Konstrukte bedeutet das, dass es neben dem Shop genau einen SalesPoint geben wird. Auf diesem laufen die verschiedenen Vorgänge. Diese Vorgänge werden jeweils als SaleProcess implementiert und dienen der Interaktion mit dem Nutzer, dem User. Abbildung 1.1 fasst diese essentiellen Zusammenhänge noch einmal in einem UML-Klassendiagramm zusammen.
Hinweis: Der Name der Oberklasse wird in einem UML-Klassendiagramm in dem Klassenkasten rechts oben notiert. Beispielsweise sagt das untenstehende Diagramm aus, dass VideoShop von der Klasse Shop erbt.
vereinfachtes Klassendiagramm Abbildung 1.1: vereinfachtes Klassendiagramm
Außerdem sind in dem Diagramm noch diverse Datenstrukturen als Attribute von VideoShop und AutomatUser zu sehen. Zum Einen dient ein MoneyBag, welches eine Spezialisierung von Stock ist, dem Aufbewahren der verschiedenen Geldscheine und -stücke, zum Anderen wird ein CountingStock benötigt, der die verfügbaren Videos des Automaten verwaltet. Beide Bestände beziehen die verfügbaren Elemente von jeweils einem Catalog. Die Klasse EUROCurrencyImpl stellt dabei den Währungskatalog dar.
Hinweis: Die Namen der Attribute stimmen im übrigen nicht mit denen in der Implementation überein.
Die entliehenen Videos des Kunden werden im Gegensatz zum VideoShop in einem StoringStock gespeichert, da in diesem Fall die einzelnen Videos entsprechend ihres Ausleihdatums unterschieden werden müssen. Wer mit der Unterscheidung zwischen Storing- und CountingStock oder von Katalogen und Beständen Probleme hat, sollte noch einmal den Technischen Überblick aufsuchen.
Details zu den obigen und den weiteren Entwurfsentscheidungen werden im Verlauf des Tutorials näher erläutert. Teilweise werden bewusst für dieses Beispiel nicht unbedingt notwendige Konstruktionen gewählt, an anderer Stelle wird vereinfacht, um einen kompakten und möglichst umfassenden Einblick in SalesPoint zu gewährleisten.
Hinweise
Auf den folgenden Seiten wird nahezu jeder einzelne Programmierschritt erläutert,wobei auf import-Anweisungen verzichtet wird, um die Übersichtlichkeit zu erhöhen. Es sei an dieser Stelle jedoch noch einmal ausdrücklich darauf hingewiesen, dass sofern eine Klasse oder Methode vom Compiler als unbekannt zurückgewiesen wird, möglicherweise lediglich ein import vergessen wurde. Sofern keine moderne Integrated Development Environment benutzt wird, die entsprechende import-Anweisungen automatisch ergänzen kann, wird empfohlen, einen Blick in das Application Programming Interface (API) von SalesPoint zu werfen, um die Paketzugehörigkeiten festzustellen.
In den Codebeispielen sind Abschnitte, die bereits erläutert und daher weggelassen wurden, durch drei Punkte angedeutet.
Trotz mehrmaligem Korrektur-Lesen lässt es sich nicht vermeiden, dass die vorliegende Dokumentation noch Rechtschreibfehler sowie Fehler in Codeabschnitten enthalten kann. Für entsprechende Korrekturmeldungen ist der Autor dieser Seiten dankbar.
Der Grundaufbau
Aufbau des Shops
Begonnen wird mit der zentralen Klasse einer jeden SalesPoint-Anwendung, dem Shop. Es wird eine neue Klasse VideoShop erzeugt, als Ableitung von Shop.
Der Konstruktor von VideoShop ruft den Konstruktor der Oberklasse durch den Befehl super() auf.
package videoautomat;
public class VideoShop extends Shop {
public VideoShop() {
super();
}
}
Hinweis: In diesem Beispiel des Videoautomaten wird jede Klasse in einer separaten Datei gespeichert. Die Klassen des Automaten werden zu einem Paket videoautomat zusammengefasst. Klassen eines Pakets werden normalerweise in einem Verzeichnis gespeichert, das den Namen des Pakets trägt. Darüberhinaus muss am Anfang einer Klasse eine Zeile der Form: package paketname; stehen, die aussagt, welchem Paket die Klasse angehört.
Um die Anwendung ausführen zu können, ist eine Klasse erforderlich, die die von der Java Virtual Machine zur Ausführung benötigte main-Methode implementiert. Zu diesem Zweck wird eine neue Klasse MainClass angelegt. In der main-Methode wird eine Instanz von VideoShop erzeugt, welche an die statische Methode Shop.setTheShop(Shop s) übergeben wird. Dieser Aufruf bewirkt, dass die übergebene Instanz zur einzigen und global erreichbaren erhoben wird. Auf diese globale Instanz kann über die ebenfalls statische Methode Shop.getTheShop() von überall aus zugegriffen werden. Das hier angewandte Entwurfsmuster Singleton ist insofern zweckmäßig, da über dieses einzelne Shopobjekt nahezu alle global benötigten Daten gekapselt werden können.
Hinweis: Statisch bedeutet, die Variable oder die Methode ist keine Eigenschaft der Instanz sondern eine Eigenschaft der Klasse selbst und ist somit unabhängig vom Zustand des Objektes. Shop.getTheShop() ist statisch und kann auf der Klasse Shop aufgerufen werden. Eine statische Variable wird von allen Instanzen einer Klasse geteilt und kann z.B. genutzt werden, um die Anzahl der erzeugten Instanzen zu zählen oder Informationen zwischen ihnen auszutauschen. D.h. alle Instanzen haben Zugriff auf ein und die selbe Variable und können diese ungewollt überschreiben. Der Einsatz von static muss wohl bedacht sein, sonst können z.B. zwei unabhängige SalesPoints sich gegenseitig den aktuellen Bargeldbestand überschreiben, wenn dieser auf statische Weise gespeichert ist. Statische Variablen bergen also eine gewisse Gefahr und werden üblicher Weise für Konstanten verwendet, die für alle Instanzen gleich bleiben.
Zuletzt wird noch ein Aufruf ergänzt, der die Instanz von VideoShop zur Ausführung bringt.
package videoautomat;
public class MainClass {
public static void main(String[] args) {
VideoShop myTutorialShop = new VideoShop();
Shop.setTheShop(myTutorialShop);
myTutorialShop.start();
}
}
Nun existiert bereits eine lauffähige Anwendung. Nach der erfolgreichen Übersetzung des Programms und der Ausführung von MainClass öffnet sich das Shopfenster, so wie es durch das Framework vordefiniert ist. Machen Sie sich ein wenig damit vertraut. Einstellungen wie Auflösung, Fensterposition und Windowmode werden nach Beenden der Anwendung automatisch in die erstellte salespoint.config gespeichert, welche nach einem Refresh auftauchen sollte.
Der Videoautomat
Die eigentliche Interaktion mit der Anwendung findet aber nicht über das Shopfenster statt, sondern über die Klasse SalesPoint. In dieser Anwendung soll VideoAutomat diese Klasse implementieren:
package videoautomat;
public class VideoAutomat extends SalesPoint {
public VideoAutomat() {
super(VideoShop.CAPTION_AUTOMAT);
}
}
Der Konstruktor der Klasse SalesPoint erwartet einen String, welcher als Identifikationsmerkmal dient und unter Anderem im Fensterrahmen angezeigt wird. Entsprechend muss auch beim Aufruf von super() ein String übergeben werden. Um die darzustellenden Strings leichter und an einer Stelle bearbeiten zu können, wird eine String-Konstante im VideoShop zur Verfügung gestellt. Wie man erkennen kann, wird diese Variable statisch verwendet und durch final vor weiteren Änderungen geschützt.
public class VideoShop extends Shop {
.
.
.
public static final String CAPTION_AUTOMAT = "VIDEOAUTOMAT RLD";
.
.
.
}
Damit der Automat auch sichtbar ist, muss die Klasse VideoAutomat instantiiert und beim Shop angemeldet werden. Dazu wird folgende Zeile der main-Methode in MainClass angefügt:
public class MainClass {
public static void main(String arqs[]) {
.
.
.
myTutorialShop.addSalesPoint(new VideoAutomat());
}
}
Bei erneuter Übersetzung und Ausführung erscheint nun, zusätzlich zu dem Shopframe, der Videoautomat mit dessen Standard-FormSheet.
Das Menü des Ladens
Damit nach dem Schließen des Videoautomaten die Anwendung nicht jedesmal neu gestartet werden muss, um diesen zu rekonstruieren, wird als nächster Schritt das Menü des Shopfensters erweitert.
Derzeit besteht das Menü des Videoladens aus den Untermenüs Shop, mit der Möglichkeit einen der möglicherweise mehreren Salespoints auszuwählen sowie dem Database-Manager mit dem man das Speichersystem modifizieren kann und MultiWindow, in welchem gewählt werden kann, ob die verschiedenen Frames der Anwendung in separaten Fenstern oder in Registerkarten innerhalb des Shopfensters angezeigt werden sollen. Das Menü soll im Folgenden um einen Eintrag ergänzt werden, dessen Aktivierung eine neue Instanz der Klasse VideoAutomat erzeugt und dem Videoladen hinzufügt.
Dafür muss die Methode createShopMenuSheet() der Klasse VideoShop überschrieben werden. Sie liefert wie der Name schon sagt das MenuSheet des Ladens. Die Klasse MenuSheet kapselt den Namen des Menüs, wie z.B. MultiWindow. Ein Menü kann darüberhinaus weitere MenuSheet-Instanzen beinhalten - die Untermenüs. Außerdem kann eine MenuSheet-Instanz Objekte der Klasse MenuSheetItem beinhalten, diese sind ähnlich wie Buttons direkt mit einer Aktion verknüpft.
Im Beispiel wird zunächst das Standard-MenuSheet des Ladens über den Aufruf super.createShopMenuSheet() zurückgegeben. Diesem wird ein neues Untermenü zugeordnet, welchem zuvor eine neue Instanz von MenuSheetItem zugefügt worden ist. Dem Konstruktor des Menüeintrags muss eine Implementation des Interface Action übergeben werden.
public class VideoShop extends Shop {
.
.
.
protected MenuSheet createShopMenuSheet() {
MenuSheet ms_default = super.createShopMenuSheet();
MenuSheet ms_new = new MenuSheet(MS_NEW);
MenuSheetItem msi_automat =
new MenuSheetItem(MSI_AUTOMAT, new Action() {
public void doAction(SaleProcess p, SalesPoint sp)
throws Throwable {
addSalesPoint(new VideoAutomat());
}
});
ms_new.add(msi_automat);
ms_default.add(ms_new);
return ms_default;
}
}
Hinweis: Das Interface Action gehört dem Package sale des Frameworks an und darf nicht mit javax.swing.Action verwechselt werden.
Die zu implementierende Methode doAction(SaleProcess process, SalesPoint point) des Interface definiert, was innerhalb der Aktion geschieht. In diesem Fall erfolgt die Implementierung des Interfaces durch eine sogenannte anonyme Klasse. Alternativ kann eine neue Klasse erstellt werden, die eine doAction Methode enthält. Die Verwendung anonymer Klassen erschwert die Lesbarkeit und Wiederverwendbarkeit des Quelltextes. Sie sollten nur verwendet werden, wenn ihr Inhalt nur einmal vorkommt oder der Zugriff auf Methoden und Ressourcen schwierig ist, wenn sie als eigene Klasse implementiert sind. Später wird gezeigt, wie Action-Klassen als eigene wiederverwendbare Klassen genutzt werden können. Im konkreten Fall hier wird in der doAction-Methode einfach eine Instanz von VideoAutomat erzeugt und mittels addSalesPoint(SalesPoint sp) beim Laden angemeldet. Der Name des Untermenüs sowie der des Menüeintrags werden in der Klasse VideoShop definiert.
public class VideoShop extends Shop {
.
.
.
public static final String MS_NEW = "Videoautomat";
public static final String MSI_AUTOMAT = "Start automat";
public static final String MSG_ACCESS = "Acces denied!!!";
}
Nach erneuter Übersetzung und Ausführung kann ein neuer Videoautomat über den entsprechenden Eintrag im Menü des Shopfensters gestartet werden.
Der Videokatalog
Die Videos eines Automaten zeichnen sich durch einen Titel und die jeweilige Anzahl, sowie den Einkaufspreis für den Betreiber und den Verkaufspreis für den Kunden aus. Entsprechend bietet sich zu ihrer Datenhaltung ein CountingStock an. Ein solcher Bestand referenziert auf die Einträge des ihm zugeordneten Katalogs und speichert deren verfügbare Anzahl. Die Katalogeinträge wiederum besitzen die Attribute Bezeichnung und Preis.
Dem entsprechend wird zunächst ein Catalog benötigt, der die Videonamen und -preise enthält. Da es sich dabei um ein Interface handelt, bedarf es einer Klasse, die dieses Schnittstellenverhalten implementiert. Im Framework existiert bereits eine vordefinierte Klasse namens CatalogImpl, die für die meisten Zwecke ausreichen dürfte. Der Konstruktor dieser Klasse verlangt einen Bezeichner, der den Katalog eindeutig von anderen unterscheidet. Es wird zunächst folgende Zeile der Klasse VideoShop hinzugefügt:
public class VideoShop extends Shop {
public static final CatalogIdentifier<CatalogItemImpl> C_VIDEOS =
new CatalogIdentifier<CatalogItemImpl>("VideoCatalog");
.
.
.
}
Diese Konstante soll der künftige Bezeichner für den Videokatalog sein. Kataloge werden in SalesPoint (ähnlich der Java Collection API) nach den ihren Einträgen getypt. Dasselbe gilt für ihre Bezeichner. Um nicht immer die generischen Parameter mit angeben zu müssen ist es zweckmäßig eine eigene Klasse dafür anzulegen:
package videoautomat;
public class VideoCatalog extends CatalogImpl<CatalogItemImpl> {
public VideoCatalog(CatalogIdentifier<CatalogItemImpl> id) {
super(id);
}
}
Der Katalog wird im Konstruktor von VideoShop wie folgt instantiiert:
public class VideoShop extends Shop {
.
.
.
public VideoShop() {
.
.
.
addCatalog(new VideoCatalog(C_VIDEOS));
}
}
Der Videokatalog ist durch die Aufnahme in die Katalogsammlung des Ladens von jeder Klasse der Anwendung aus erreichbar, jedoch ist der Aufruf, um an den Katalog zu gelangen unangenehm lang und wird vermutlich mehr als einmal verwendet. Zur Erleichterung wird eine statische Hilfsmethode in der Klasse VideoShop geschaffen, die den Videokatalog zurückgibt.
public class VideoShop extends Shop {
.
.
.
public static VideoCatalog getVideoCatalog() {
return (VideoCatalog) Shop.getTheShop().getCatalog(C_VIDEOS);
}
}
Nun existiert zwar ein Katalog, jedoch ohne Einträge. Damit im weiteren Verlauf des Programmierens und Testens einige Daten zur Verfügung stehen, wird die MainClass um folgende Methode ergänzt:
public class MainClass {
.
.
.
public static void initializeVideos() {
VideoCatalog videoCatalog = VideoShop.getVideoCatalog();
Category c1 = new Category("Action");
Category c2 = new Category("Science Fiction");
List<Video> videos = new ArrayList<Video>();
try {
videos.add(new Video("Event Horizon", "event_horizon", 1999, c2));
videos.add(new Video("H.E.A.T.", "heat", 1999, c1));
videos.add(new Video("Matrix", "matrix", 1499, c2));
videos.add(new Video("Sin City", "sin_city", 2199, null));
videos.add(new Video("Taken (Blue-Ray)", "taken", 3199, c1));
videos.add(new Video("Terminator", "terminator", 999, c1));
videos.add(new Video("Terminator 2", "terminator2", 999, c1));
videos.add(new Video("Terminator 3", "terminator3", 1299, c1));
videos.add(new Video("True Lies", "true_lies", 999, c1));
videos.add(new Video("The X-Files", "xfiles", 999, c2));
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
for (Video video : videos) {
videoCatalog.add(video, null);
}
}
}
Was hier passiert ist relativ leicht ersichtlich. Wir holen uns zuerst den VideoKatalog aus dem Shop mit der vorhin erstellten Methode getVideoCatalog(), erstellen daraufhin 2 Kategorien in unserem Anwendungsfall Genres, nach denen wir später die Videos besser sortieren können und fügen dann nach und nach einzelne Videos mit Titel,Bildtitel, Preiswert in Cent und Genrekategorie in eine zuvor erstellte Arraylist ein, aus der wir in der For-Schleife zum Schluss alles in unseren Katalog schieben. Was uns dazu fehlt ist natürlich die VideoKlasse die von CatalogItemImpl erbt, um in unseren VideoCatalog zu passen und Descriptive und Categorizable implementiert, um das später implementierte Descriptive-Formsheet zu ermöglichen und die Categories nutzbar zu machen. Dem Konstruktor von CatalogItemImpl muss mindestens ein String und ein Value übergeben werden. Value ist ein Interface und es existieren zwei Implementationen dieser Schnittstelle im Framework. Zum Einen NumberValue, welches einen numerischen Wert kapselt und QuoteValue, das ein Paar von Werten repräsentiert, z.B. einen Ein- und Verkaufswert. In diesem Fall wird dem Konstruktor unter anderem eine Instanz von QuoteValue übergeben, welche wiederum mit zwei IntegerValue erzeugt wird. IntegerValue ist lediglich eine Spezialisierung von NumberValue, welche einen int-Wert kapselt, der hierbei der Wert in Cent ist. Der RecoveryConstructor ist nötig für die DB-Speicherung. HINWEIS FÜR DEN FREEKY!!!!!!!!!!!!!!!! Der ResourceManager dient hier primär zum Beschaffen von binären Formaten wie vor allem Bildern. Hier ruft er über den Typ PNG, im Projektordner res die einzelnen Bilder ab. Diese entweder dem downloadbaren Sourceverzeichnis entnehmen oder einfach den Aufruf durch null ersetzen.
package videoautomat;
public class Video extends CatalogItemImpl implements Descriptive, Categorizable {
private Category category = null;
@RecoveryConstructor(parameters = { "m_sName" })
public Video(String name) {
super(name);
}
public Video(String name, String image, int price, Category category)
throws URISyntaxException, NullPointerException {
super(name, new QuoteValue(new IntegerValue(250), new IntegerValue(
price)), ResourceManager.getInstance().getResource(
ResourceManager.RESOURCE_PNG, "videos." + image).toURI());
this.category = category;
}
protected CatalogItemImpl getShallowClone() {
return null;
}
public StyledDocument getDescription() {
StyledDocument styledDocument = new DefaultStyledDocument();
SimpleAttributeSet attributes = new SimpleAttributeSet();
try {
styledDocument.insertString(0, "This is the Description for ", attributes);
ColorConstants.setForeground(attributes, Color.GREEN);
FontConstants.setFontSize(attributes, 16);
styledDocument.insertString(styledDocument.getLength(), getName(), attributes);
} catch (BadLocationException e) {
e.printStackTrace();
}
return styledDocument;
}
public Category getCategory() {
return category;
}
}
Abschließend wird in der main-Methode der MainClass die neue Methode zur Ausführung gebracht, damit die Änderungen wirksam werden. Wir nutzen dazu die Methode initializeData zur Kapselung, da diese später aus dem DatabaseManager jederzeit über den gleichnamigen Button dort aufgerufen werden kann, was die Dinge durchaus erleichtert.
public class MainClass {
public static void main(String arqs[]) {
.
.
.
public void initializeData() {
initializeVideos();
}
}
.
.
.
}
Der Videobestand
Nach der Fertigstellung des Katalogs kann im Folgenden der Bestand aufgebaut werden. Wie bereits im Abschnitt Der Videokatalog erwähnt, sollen die Videos des Automaten in einem CountingStock gespeichert werden. Auch für dieses Interface existiert eine vordefinierte Klasse mit dem gewohnten Impl am Ende des Namens. Ein jeder Bestand bezieht sich auf einen Katalog, so dass dieser konsequenterweise neben dem String-Bezeichner dem Konstruktor von CountingStockImpl übergeben werden muss. Ähnlich den Katalogen sind auch die Bestände nach ihren Einträgen getypt, zusätzlich aber auch noch mit den Eintragstypen des zugehörigen Katalogs. Allein aus diesem Grund lohnt es sich, eine eigene Klasse hierfür zu definieren:
package videoautomat;
public class AutomatVideoStock extends
CountingStockImpl<StockItemImpl, CatalogItemImpl> {
public AutomatVideoStock(
StockIdentifier<StockItemImpl, CatalogItemImpl> siId,
Catalog<CatalogItemImpl> ciRef) {
super(siId, ciRef);
}
}
StockItemImpl ist dabei die Standardimplementation eines Bestandseintrages. Wir werden später noch einmal etwas genauer darauf zurückkommen. Am Anfang der Shop-Klasse muss der Identifikator für den neuen Bestand deklariert werden:
public class VideoShop extends Shop {
.
.
.
public static final StockIdentifier<StockItemImpl, CatalogItemImpl> CC_VIDEOS =
new StockIdentifier<StockItemImpl, CatalogItemImpl>("VideoStock");
.
.
.
}
Jetzt können wir den eigentlichen Stock anlegen. Dazu wird analog zum Videokatalog in den Konstruktor von VideoShop folgende Zeile eingefügt:
public class VideoShop extends Shop {
.
.
.
public VideoShop() {
.
.
.
addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS)));
}
.
.
.
}
Auch beim Videobestand lohnt es sich eine Hilfsmethode zu schreiben, die selbigen zurückliefert.
public class VideoShop extends Shop {
.
.
.
public static AutomatVideoStock getVideoStock() {
return (AutomatVideoStock) Shop.getTheShop().getStock(CC_VIDEOS);
}
}
Der neue Bestand ist wiederum leer. Durch das Hinzufügen einer Zeile in die Initialisierungsmethode der Videos in MainClass können dem Videobestand die benötigten Testdaten zugefügt werden.
public static void initializeVideos() {
VideoCatalog videoCatalog = VideoShop.getVideoCatalog();
AutomatVideoStock videoStock = VideoShop.getVideoStock();
.
.
.
for (Video video : videos) {
.
videoStock.add(video.getName(), 5, null);
}
}
Der Aufruf add(String id, int count, DataBasket db) bewirkt, dass von dem Katalogeintrag mit der Bezeichnung id insgesamt count-Stück in den Bestand aufgenommen werden. Der DataBasket, der zum Schluss übergeben wird, hat etwas mit der Sichtbarkeit der vollführten Aktion zu tun. Vorerst reicht es zu wissen, dass hier durch die Übergabe von null das Hinzufügen unmittelbar wirksam wird.
Zum Schluss noch die Klasse, die StockItemImpl erbt und es uns dadurch möglich macht später auf unserem VideoStock zu arbeiten.
package videoautomat;
public class VideoCassette extends StockItemImpl {
@RecoveryConstructor(parameters = { "m_sName" }) public VideoCassette(String key) { super(key); }
}
StandardFormsheet
Wie im Technischen Überblick erläutert wird, kann auf einem Display ein FormSheet und/oder ein MenuSheet angezeigt werden.
Als Bgegrüßungsschirm reicht uns unser schönes Standardformsheet vollkommen, jedoch müssen neue Buttons eingefügt werden. Das Anpassen des StandardFormsheets eines SalesPoints geschieht durch das Überschreiben der Methode getDefaultFormSheet().So zum Beispiel in der Klasse VideoAutomat.
protected FormSheet getDefaultFormSheet() {
FormSheet fs = super.getDefaultFormSheet();
fs.addContentCreator(new StartFSContentCreator());
return fs;
}
Hier holen wir uns lediglich das StandardFormsheet der Parentklasse und ergänzen es um einen Contentcreator, welcher eine Möglichkeit darstellt Buttons und Anzeigen zu kapseln, jedoch nicht erforderlich ist.
Der FormSheetContentCreator
Die Anordnung der Elemente eines Formulars kann in einem FormSheetContentCreator vorgenommen werden, der dann dem Formular zugefügt wird.
Zur Anpassung der Buttonleiste wird dementsprechend ein FormSheetContentCreator in der Erzeugermethode definiert und dem Tabellenformular hinzugefügt. FormSheetContentCreator ist eine abstrakte Klasse und fordert zur Ableitung der Klasse und Implementierung der Methode createFormSheetContent(FormSheet fs) auf. Die Implementierung könnte an dieser Stelle als anonyme Klasse erfolgen, jedoch bietet eine eigene Klasse einige Vorteile. Es wird die Trennung zwischen Anwendungslogik und Oberfläche verbessert, was später besonders bei den Prozesses deutlich wird. Außerdem kann dieser FormSheetContentCreator so für mehrere FormSheets wiederverwendet werden.
package videoautomat.contentcreator;
public class StartFSContentCreator extends FormSheetContentCreator {
public static final int FB_LOGON = 1;
public static final int FB_REGISTER = 2;
protected void createFormSheetContent(FormSheet fs) {
fs.removeAllButtons();
fs.addButton("Login", FB_LOGON, new RunProcessAction(
new SaleProcessLogOn()));
fs.addButton("Register", FB_REGISTER, new RunProcessAction(
new SaleProcessRegister()));
}
}
Die Methode removeAllButtons() tut das, was der Name verspricht, während addButton(String name, int id, sale.Action a) einen Formbutton erzeugt und dem Formular hinzufügt. Formbutton ist die Framework-Version eines Buttons und besitzt eine Beschriftung, eine ID zur eindeutigen Unterscheidung und eine mit diesem Knopf assoziierte Aktion. Die ID kann unter anderem dazu verwendet werden, einen Button außerhalb der Deklaration des FormSheet referenzieren zu können. Dies ermöglicht z.B. nachträglich die Aktion des Buttons zu verändern. Es sollten jedoch alle Änderungen der Darstellung an einer Stelle erfolgen, um die Übersicht behalten zu können und den Quelltext les- und wartbarer zu halten, besonders wenn mehrere Teammitglieder am gleichen Programm arbeiten. Hier wird beispielsweise die RunProcessAction genutzt, um den Code zum Starten eines Prozesses wiederzuverwenden. Dies ist keine Framework-Klasse und sieht folgendermaßen aus:
package videoautomat.contentcreator.stdactions;
public class RunProcessAction implements Action {
private SaleProcess process;
private DataBasket basket;
public RunProcessAction(SaleProcess process) {
this.process = process;
}
public RunProcessAction(SaleProcess process, DataBasket basket) {
this.process = process;
this.basket = basket;
}
public void doAction(SaleProcess saleProcess, SalesPoint salePoint)
throws Throwable {
if (basket != null)
salePoint.runProcess(process, basket);
else
salePoint.runProcess(process);
}
}
Sie ist im Package videoautomat.contentcreator.stdactions; abgelegt und implementiert das Interface Action. Im ersten Konstruktor wird die Instanz des Prozesses übergeben, der durch diese Aktion gestartet werden soll. Im zweiten Konstruktor wird zusätzlich ein DataBasket übergeben, der beim Starten des Prozesses ihm zugewiesen wird. Das Starten erfolgt in der doAction Methode, die als Parameter den verwendeten SaleProcess und den SalesPoint übergeben bekommt. Somit kann auf diesem SalesPoint die runProcess Methode ausgeführt werden. Aufgrund dieser Parameterisierbarkeit, kann diese Klasse immer genutzt werden, wenn ein Prozess über eine Aktion gestartet werden soll. Man spart sich also jedesmal als anonyme Klasse das Action-Interface zu implementieren und die Anweisungen zum Starten jedesmal erneut zu schreiben.
Hinweis: Die Anordnung der Buttons ist durch die Reihenfolge ihres Hinzufügens bestimmt.
SaleProcessLogon und SaleProcessRegister sind SalesProcesses, von denen jeweils einer auf einem Salespoint aktiv laufen darf. Sie werden über die jeweiligen Buttons aufgerufen, wie oben dargestellt, aber erst später implementiert.
Der Usermanager
Bevor mit dem ersten Prozess der Anwendung, der Nutzerregistrierung, begonnen werden kann, muss die Nutzerverwaltung angelegt werden. Zur Erinnerung: es können lediglich registrierte Kunden am Automaten Filme entleihen. Entsprechend braucht die Anwendung eine Datenstruktur, anhand derer der Automat erkennen kann, wer Kunde ist und wer nicht.
SalesPoint bietet dafür die Klassen User und UserManager an.
Der Nutzermanager ist eine Art Containerklasse, in der alle Nutzer gespeichert werden. Ebenso wie beim Shop wurde bei der Klasse UserManager auf das Entwurfsmuster Singleton zurückgegriffen, d.h. es gibt genau eine Instanz des Nutzermanagers, die über UserManager.getGlobalUM() referenziert werden kann.
Der Automatennutzer
Anwender des Programms können durch die Klasse User dargestellt werden. Ein User besitzt einen Namen, über den er eindeutig identifiziert werden kann. Darüber hinaus besteht die Möglichkeit ein Passwort zu setzen, sowie Rechte auf mögliche Aktionen zu vergeben.
Damit die entliehenen Videos eines Kunden in der ihn repräsentierenden Instanz gekapselt werden können, muss eine neue Klasse von User abgeleitet werden. Abgesehen vom Kunden gibt es die Nutzergruppe der Betreiber bzw. Administratoren. Da diese sich im Prinzip nur darin unterscheiden, dass sie über mehr Rechte verfügen, genügt es, eine einzige Klasse für Kunden und Administratoren zu definieren. Die Unterscheidung bezüglich der verschiedenen Rechte erfolgt über einen Parameter im Konstruktor der neuen Klasse.
package videoautomat;
public class AutomatUser extends User implements Recoverable {
public static final String CAPABILITY_ADMIN = "admin";
@PersistenceProperty(follow = false)
private UserVideoStock ss_videos = null;
@RecoveryConstructor(parameters = { "m_sName" })
public AutomatUser(String user_ID) {
super(user_ID);
}
public AutomatUser(String user_ID, String passWd, boolean admin) {
super(user_ID);
setPassWd(garblePassWD(passWd));
ActionCapability ac = new ActionCapability(CAPABILITY_ADMIN,
VideoShop.MSG_ACCESS, new Action() {
@Override
public void doAction(SaleProcess p, SalesPoint sp)
throws Throwable {
UIGate gate = (UIGate) p.getCurrentGate();
gate.setNextTransition(new Transition() {
public Gate perform(SaleProcess owner, User usr) {
return ((SaleProcessLogOn) owner)
.getAdministrationGate();
}
});
}
}, admin);
setCapability(ac);
ss_videos = new UserVideoStock(user_ID, VideoShop.getVideoCatalog());
}
public void renderAdditionalInfoArea(JPanel panel, boolean isSelected) {
JLabel videos = new JLabel(ss_videos.size(null) + " videos rented");
videos.setForeground(isSelected ? Color.white : Color.gray);
videos.setFont(new Font(null, Font.PLAIN, 14));
panel.add(videos);
}
public UserVideoStock getVideoStock() {
return ss_videos;
}
public void recover(Map<String, Object> data, Object recoveryContext,
boolean reInit) {
if (!reInit)
ss_videos = new UserVideoStock(getName(), VideoShop
.getVideoCatalog());
}
/**
* free internal resources
*
*/
public void DatasourceOnChange() {
super.DatasourceOnChange();
if (ss_videos != null)
ss_videos.unsubscribe();
}
}
Im Gegensatz zum VideoShop werden hier die Videos in einem StoringStockImpl verwaltet. In einem solchen wird nicht nur die Anzahl gewisser Katalogeinträge gespeichert, sondern es wird jedes einzelne StockItem separat behandelt. Die Bestandseinträge der Videos werden über die Klasse VideoCassette definiert.
Ähnlich dem Videokatalog und dem Videobestand des Automaten ist es auch hier zweckmäßig (wegen der Typisierung), eine neue Klasse UserVideoStock anzulegen, die ebendiese generischen Parameter festlegt:
package videoautomat;
public class UserVideoStock extends
StoringStockImpl<VideoCassette, CatalogItemImpl> {
public UserVideoStock(String sName, CatalogImpl<CatalogItemImpl> ciRef) {
super(sName, ciRef);
}
}
Die Nutzerregistrierung
Im Gegensatz zu Katalog und Bestand des Automaten benötigen wir hier keinen eigenen Identifikator, da der UserVideoStock stets nur beim Nutzer und nie im Shop selbst registriert wird. Deswegen überladen wir auch den String - Konstruktor anstelle des Identifikator - Konstruktors.
Weiterhin ist zu erkennen, dass beim Setzen des Passworts nicht das Passwort selbst, sondern der Rückgabewert der Methode garblePassWD(passWd) übergeben wird. Dabei handelt es sich um eine Sicherheitsmaßnahme. Damit das Passwort nicht im Klartext abgespeichert wird und ausgelesen werden kann, wird es vorher umschlüsselt. Welcher Algorithmus dabei verwendet wird, kann ebenfalls über die Klasse User gesetzt werden. Der voreingestellte Algorithmus für die Umschlüsselung ist einfach umkehrbar und daher für sicherheitsrelevante Kontexte nicht zu empfehlen. Wird das Passwort mit der Methode isPassWd(String s) überprüft, ist darauf zu achten, dass der übergebene String vorher ebenfalls umschlüsselt wird.
Die boolesche Variable wird erst zu einem späteren Zeitpunkt für die Rechtevergabe benötigt.
Zum Schluss sollen einige Testkunden und ein Administrator der Nutzerverwaltung zugefügt werden. Als Passwort wird der vereinfachten Testbarkeit halber jeweils eine leere Zeichenkette übergeben.
public class VideoShop extends Shop {
.
.
public void initializeData() {
.
initializeUsers();
}
.
public static void initializeUsers() {
try {
UserManager.getInstance().addUser(
new AutomatUser("Administrator", "", true));
} catch (DuplicateUserException e) {
}
for (int i = 0; i < 10; i++) {
try {
UserManager.getInstance().addUser(
new AutomatUser("Customer" + i, "", false));
} catch (DuplicateUserException e) {
}
}
}
}
An dieser Stelle sind nun die Nutzerverwaltung und einige Testnutzer angelegt. Es sollen sich jedoch auch weitere Kunden registrieren können, die sich in geeigneter Weise anmelden müssen. Hierzu wird der Prozess SaleProcessRegister erzeugt, der nun im Weiteren erläutert wird.
Der Prozess fordert zur Eingabe des Nutzernamens, Passwort und Passwortwiederholung auf. Sind alle Angaben korrekt und der Nutzer existiert noch nicht, so wird er beim Klick auf OK angelegt und der Prozess endet erfolgreich. Tritt ein Fehler auf, so springt man zur Eingabemaske zurück und bekommt eine Fehlermeldung. Durch Abbrechen kann der Prozess zu jeder Zeit beendet werden, ohne dass dabei eine Änderung an den internen Daten erfolgt.
Deklaration des Prozesses
Wie im Technischen Überblick erläutert wird, ist ein SaleProcess eine Folge von Zuständen und Zustandsübergängen. Die Zustände werden dabei durch das Interface Gate, die Zustandsübergänge durch das Interface Transition beschrieben. Während eine Transition atomar behandelt, d.h. nicht unterbrochen wird, kann an einem Gate der Kontrollfluss unterbrochen werden und somit auch Interaktion mit dem Anwender stattfinden. Eine Implementation von Gate, die die Kommunikation über ein FormSheet und/oder MenuSheet ermöglicht, ist die Klasse UIGate.
Im Folgenden wird die Klasse SaleProcessRegister als Spezialisierung von SaleProcess implementiert. Dabei muss die abstrakte Methode getInitialGate() implementiert werden, welche das Start-Gate zurückliefert. Würde der Prozess weitere Gates haben, wären analog dazu weitere Methoden hinzuzufügen. Das ist sinnvoll, um die Definition und Erstellung eines Gates möglichst zentral zu haben. Alternativ wäre es auch möglich dies am Ende einer Transition auszuführen, jedoch würde das u.U. erneut die Übersichtlichkeit mindern. Bei den weiteren Prozessen im Tutorial mit mehreren Gates findet immer die erste Alternative ihre Anwendung. Etwas abweichend davon wird in diesem Prozess der ContentCreator des initialen FormSheets bei einem Zustandsübergang wiederverwendet und nicht jedesmal durch die getInitialGate()-Methode neu erzeugt. Doch mehr dazu im letzten Unterkapitel.
package videoautomat;
public class SaleProcessRegister extends SaleProcess {
public SaleProcessRegister() {
super("Register Proecess");
}
protected Gate getInitialGate() {
FormSheet register = new FormSheet("Register",
new RegisterContentCreator("Please type in your data!"), false);
return new UIGate(register, null);
}
}
Ähnlich wie bei der Anzeige des Videobestandes wird ein FormSheet erstellt. Ihm wird im Konstruktor der Titel und ein FormSheetContentCreator übergeben. Das FormSheet selbst würde nur ein leeres Fenster darstellen. Der Inhalt wird durch die Klasse RegisterContentCreator erzeugt. Würde man nun die Anwendung übersetzen, bekäme man einen Fehler oder, falls sie doch zu starten geht, spätestens bei der Betätigung des Register-Buttons. Dies passiert, weil die Klasse RegisterContentCreator noch nicht erstellt wurde. Dies geschieht nun im nächsten Abschnitt.
Die Klasse RegisterContentCreator
Wie schon beim SingleTableFormSheet des Videobestandes, erweitert auch die Klasse RegisterContentCreator die Klasse FormSheetContentCreator. Jedoch kann diesmal ein String übergeben werden, der als Text über den Eingabefeldern erscheint.
package videoautomat.contentcreator;
public class RegisterContentCreator extends FormSheetContentCreator {
public RegisterContentCreator(String message){
.
.
.
}
protected void createFormSheetContent(FormSheet fs) {
.
.
.
}
}
Diese Klasse ist also konfigurierbar und man kann ihre Darstellung von außen verändern. Damit bei einer Falscheingabe der Nutzername nicht erneut eingegeben werden muss und die Werte ausgelesen werden können, bietet diese Klasse zahlreiche get- und set-Methoden. Diese stehen nur für die Daten, die gesetzt oder ausgelesen werden können. Nach außen hin sieht man also nur diese Methoden ohne eigentlich zu wissen, wie die Darstellung der Daten erfolgt. Dies ist ein wichtiger Beitrag die Oberfläche von der Anwendungslogik zu trennen. Die Klassen, die die Daten weiterverarbeiten, sind nur an diesen Methoden interessiert und nicht an ihrer visuellen Darstellung. So wird auch die Änderungsfreundlichkeit erhöht, da so leicht Oberflächenkomponenten ausgetauscht werden können ohne andere Stellen im Programm anzupassen. Die Klasse bietet folgende Methoden, deren Benutzung später gezeigt wird.
package videoautomat.contentcreator;
public class RegisterContentCreator extends FormSheetContentCreator {
private JTextArea errorMessage;
private JLabel message;
private JTextField userName;
private JPasswordField password;
private JPasswordField confirmedPassword;
private JFileChooser fcImage = null;
private ImagePanel ImageView;
.
.
.
public String getUserName() {
return userName.getText();
}
public String getPassword() {
return String.valueOf(password.getPassword());
}
public String getConfirmedPassword() {
return String.valueOf(confirmedPassword.getPassword());
}
public URI getUserImage() {
if (fcImage.getSelectedFile() == null)
return null;
return fcImage.getSelectedFile().toURI();
}
public void setUserName(String userName) {
this.userName.setText(userName);
}
public void setErrorMessage(String message) {
errorMessage.setText(message);
}
}
Die createFormSheetContent Methode erzeugt wie bisher den darzustellenden Inhalt. Diesmal wird aber mit zusätzlichen Swing-Komponenten gearbeitet, um eine eigene Eingabemaske zu erzeugen. Sie verwendet nun die Daten, die mit den set- Methoden gesetzt wurden, um die Daten entsprechend zu visualisieren. Die get-Methoden lesen die Daten wieder aus den Swing-Komponenten aus. Die Verwendung von Swing soll an dieser Stelle nicht weiter erläutert werden, dazu finden sich zahlreiche Tutorials im Internet. Das Interessante liegt hier bei der Zuweisung der Buttons.
package videoautomat.contentcreator;
public class RegisterContentCreator extends FormSheetContentCreator {
.
.
.
public RegisterContentCreator(String message) {
errorMessage = new JTextArea();
this.message = new JLabel();
userName = new JTextField();
password = new JPasswordField();
confirmedPassword = new JPasswordField();
this.message.setText(message);
errorMessage.setForeground(Color.RED);
errorMessage.setBackground(this.message.getBackground());
errorMessage.setEditable(false);
ImageView = new ImagePanel();
fcImage = new JFileChooser();
fcImage.setFileFilter(new FileFilter() {
public boolean accept(File f) {
if (f.isDirectory())
return true;
String[] xts = new String[] { "jpg", "jpeg", "png", "gif",
"bmp" };
for (String xt : xts)
if (f.getPath().toLowerCase().endsWith("." + xt))
return true;
return false;
}
@Override
public String getDescription() {
return "Images";
}
});
}
protected void createFormSheetContent(FormSheet fs) {
JPanel superpanel = new JPanel(new BorderLayout());
JOptionPanel panel = new JOptionPanel(null, 16, 4, JOptionPanel.CENTER,
JOptionPanel.CENTER);
userName.setPreferredSize(new Dimension(150, 27));
panel.addOption("User Name", userName);
password.setEchoChar('\u2022');
password.setPreferredSize(new Dimension(150, 27));
panel.addOption("Password", password);
confirmedPassword.setEchoChar('\u2022');
confirmedPassword.setPreferredSize(new Dimension(150, 27));
panel.addOption("Confirm Password", confirmedPassword);
errorMessage.setPreferredSize(new Dimension(300, 27));
errorMessage.setBackground(new Color(0, 0, 0, 0));
errorMessage.setOpaque(false);
errorMessage.setBorder(BorderFactory.createEmptyBorder());
panel.addOption("", errorMessage);
JButton btnImage = new JButton("Choose Image...");
final JTextField ImageField = new JTextField();
ImageField.setPreferredSize(new Dimension(150, 27));
final JFileChooser fFcImage = fcImage;
btnImage.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (fFcImage.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
try {
ImageField.setText(fFcImage.getSelectedFile()
.getCanonicalFile().toURI().toString());
ImageView.setImage(ImageIO.read(fFcImage
.getSelectedFile().getCanonicalFile()), true);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
});
ImageView.setPreferredSize(new Dimension(150, 150));
try {
ImageView.setImage(ImageIO.read(new URL(User.defaultUserImage)),
true);
} catch (MalformedURLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
panel.addOption("", btnImage);
panel.addOption("", ImageField);
JPanel imageContainer = new JPanel(new GridBagLayout());
GridBagConstraints ctr = new GridBagConstraints();
ctr.insets = new Insets(25, 25, 0, 25);
ctr.anchor = GridBagConstraints.NORTH;
ctr.weighty = 1f;
imageContainer.setBackground(new Color(51, 84, 111, 150));
imageContainer.add(ImageView, ctr);
panel.setOpaque(false);
superpanel.add(panel, BorderLayout.CENTER);
superpanel.add(imageContainer, BorderLayout.EAST);
fs.setComponent(superpanel);
fs.removeAllButtons();
fs.addButton("OK", 1, new TransitWithAction(new RegisterOKTransition(
this)));
fs.addButton("Cancel", 2, new RollBackAction());
}
}
Wie bereits erwähnt, werden den Buttons wieder Action Klassen übergeben. Auch diesmal sind es wieder Hilfsklassen, die sich wiederverwenden lassen. In diesem Fall lösen diese Aktionen einen Zustandsübergang aus. Die beiden Klassen sind wieder selbst implementiert und sehen wie folgt aus:
package contentcreator.stdactions;
public class TransitWithAction implements Action {
private Transition transition;
public TransitWithAction(Transition transition) {
this.transition = transition;
}
public void doAction(SaleProcess saleProcess, SalesPoint salePoint)
throws Throwable {
UIGate currentGate = (UIGate) saleProcess.getCurrentGate();
currentGate.setNextTransition(transition);
}
}
package contentcreator.stdactions;
public class RollBackAction implements Action {
public void doAction(SaleProcess saleProcess, SalesPoint salePoint)
throws Throwable {
UIGate currentGate = (UIGate) saleProcess.getCurrentGate();
currentGate
.setNextTransition(GateChangeTransition.CHANGE_TO_ROLLBACK_GATE);
}
}
Im beiden Fällen wird auf dem aktuellen Gate die Methode setNextTransition ausgeführt, um den Zustandsübergang mit einer entsprechenden Transition durchzuführen.
Im Falle des OK-Buttons wurde der Action eine spezielle Transition übergeben und dieser zusätzlich die this Referenz des RegisterContentCreator mitgegeben. Warum dies gemacht wurde und wie die Transition aussieht, wird im nächsten Abschitt erklärt.
Einen Zustandsübergang definieren
Der Technischen Überblick beschreibt, dass die internen Bearbeitungsvorgänge in einer Transition durchzuführen sind. Für diesen Fall ist das die RegisterOKTransition, die prüft, ob der Nutzer schon vorhanden ist und die beiden Passwörter übereinstimmen. Die Klasse liegt im Package transition und implementiert das Interface Transition des Frameworks. Das Interface gibt vor, dass die Methode perform(SaleProcess sp, User user) zu implementieren ist.
package videoautomat.transition;
public class RegisterOKTransition implements Transition {
private RegisterContentCreator creator;
public RegisterOKTransition(RegisterContentCreator creator) {
this.creator = creator;
}
public Gate perform(SaleProcess sp, User user) {
.
.
.
}
}
Wie zu erkennen ist, wird im Konstruktor die Referenz des RegisterContentCreator übergeben. Dies ist die this-Referenz aus dem vorherigen Abschnitt. Da die Transition die Daten aus der Eingabe verarbeitet, braucht sie Zugriff auf dieses Objekt. Durch die Übergabe im Konstruktor kann die perform(SaleProcess sp, User user)-Methode nun auf die Daten zugreifen und diese verarbeiten.
public Gate perform(SaleProcess sp, User user) {
StringBuffer errors = new StringBuffer("");
if ("".equals(creator.getUserName())) {
errors.append("You have to choose a user name!n");
}
if (!creator.getPassword().equals(creator.getConfirmedPassword())) {
errors.append("The passwords are different!n");
}
if (UserManager.getInstance().getUserNames().contains(
creator.getUserName())) {
errors.append("User already exists!n");
}
if (errors.length() != 0) {
creator.setErrorMessage(errors.toString());
FormSheet register = new FormSheet("Register", creator, false);
return new UIGate(register, null);
}
AutomatUser nUser = new AutomatUser(creator.getUserName(), creator
.getPassword(), false);
nUser.setUserImage(creator.getUserImage());
UserManager.getInstance().addUser(nUser);
return sp.getCommitGate();
}
Zuerst wird geprüft, ob die Daten korrekt eingegeben wurden. Der erste Vergleich prüft, ob überhaupt ein Nutzername eingeben wurde. Fehlt der Nutzername, so wird eine Fehlermeldung vorbereitet. Der zweite Vergleich prüft die beiden Passwörter auf Gleichheit und der dritte, ob der Nutzer schon existiert. Zum Auslesen der Daten werden die jeweiligen Methoden genutzt, die der RegisterContentCreator dafür zur Verfügung stellt.
Der vierte Vergleich prüft nun, ob ein Fehler auftrat und bestimmt dann die Verzweigung. Man hätte natürlich beim ersten Auftreten eines Fehlers darauf reagieren können. So werden aber gleich alle Fehler erkannt und dem Nutzer mitgeteilt. So spart er sich u.U. mehrere Durchläufe, bis alles korrekt eingegeben ist.
Laut Zustandsübergangsdiagramm geht der Prozess zum ersten Gate zurück, wenn ein Fehler auftritt. Dies passiert im Rumpf des vierten Vergleiches. In diesem Fall wird der RegisterContentCreator wiederverwendet und nur der Fehlertext gesetzt. D.h. alle bisherigen Eingaben bleiben bestehen. Alternative hätte man auch eine neue Instanz erstellen können und über die Methoden die Eingaben gesetzt. Je nach Anwendungsfall kann man sich für den einen oder anderen Weg entscheiden.
Nach dem Setzen der Fehlernachricht wird ein neues Gate erstellt und mit dem alten RegisterContentCreator verknüpft. Anschließen wird das Gate zurückgegeben und somit als nächstes angezeigt. Anstatt das Gate hier zu erstellen, könnte man auch eine selbst definierte getGate-Methode im Prozess nutzen, und dann dieses Gate zurückgeben. Die Methode könnte auch einige Paramter haben, um, wie in diesem Beispiel, Daten an das FormSheet übermitteln zu können. Dieses Verfahren wird an späterer Stelle im Tutorial gezeigt.
Trat kein Fehler auf, so wird der Nutzer, unter Verwendung der eingegebenen und geprüften Daten, erstellt und der Prozess wechselt zum CommitGate, womit er erfolgreich beendet wäre.
Die Anmeldung
Es sind nun alle Voraussetzungen erfüllt, um den Anmeldeprozess zu implementieren. Bei diesem initialen Prozess muss der Anwender sich zunächst durch Namen und Passwort identifizieren. Nach erfolgreicher Anmeldung werden die weiteren möglichen Aktivitäten angezeigt. Wurde kein Nutzername ausgewählt oder ein falsches Passwort eingetippt, erscheint eine Fehlermeldung. Meldet man sich ab, terminiert der Prozess. Wählt man eine der möglichen Aktivitäten, wie z.B. Video leihen, so wird ein neuer, auf die Aktivität ausgerichteter Prozess gestartet. Der Anmeldeprozess "schläft" dann solange, bis der neugestartete Prozess terminiert. Abbildung 7.1 verdeutlicht den Ablauf des Prozesses anhand eines Zustandsdiagramms.
Deklaration eines SaleProcess
Im Folgenden wird die Klasse SaleProcessLogOn als Spezialisierung von SaleProcess implementiert. Es werden zunächst die in Abbildung 7.1 aufgeführten Zustände deklariert, mit Ausnahme des Stop-Gate, welches bereits in der Klasse SaleProcess selbst definiert ist. Für die Repräsentation der Zustände werden UIGate-Instanzen benutzt, selbige werden innerhalb des jeweiligen Gates definiert und ihrem Konstruktor wird, sowohl für das erwartete FormSheet als auch für das MenuSheet, vorläufig null übergeben. Außerdem muss die abstrakte Methode getInitialGate() implementiert werden, welche das Gate zurückliefert, an das zu Prozessbeginn gesprungen wird. Die anderen Methoden dienen der Rückgabe der restlichen UIGate-Instanzen.
package videoautomat;
public class SaleProcessLogOn extends SaleProcess {
public SaleProcessLogOn() {
super("SaleProcessLogOn");
}
protected Gate getInitialGate() {
UIGate uig_log_on = new UIGate(null, null);
return uig_log_on;
}
public Gate getFaultGate() {
UIGate uig_fault = new UIGate(null, null);
return uig_fault;
}
public Gate getMainGate() {
UIGate uig_main = new UIGate(null, null);
return uig_main;
}
}
Der Prozess wird durch die Aktion, wie im Abschnitt Der FormSheetContentCreator beschrieben, gestartet. Wird die Anwendung zum jetzigen Zeitpunkt übersetzt und ausgeführt und der betreffende Button betätigt, so startet der Prozess, jedoch mit leerer Anzeigefläche. Darüberhinaus kann das Fenster vom Videoautomaten nicht wie gewohnt geschlossen werden. Das liegt daran, dass ein SalesPoint mit darauf laufendem Prozess nicht ohne weiteres geschlossen werden kann.
Anmeldung durch das LogOnForm
Im initialen Zustand des Anmeldeprozesses soll der Anwender zwecks Authentifikation den Namen und das Kennwort eintragen. Das Framework bietet dafür eine eigene FormSheet-Ableitung, die Klasse LogOnForm. Dieses spezielle Formular zeigt anhand einer übergebenen Instanz von UserManager eine Auswahlliste aller registrierten Namen an, sowie ein Textfeld zur Eingabe des Kennworts. Optional kann mit Hilfe einer Implementation des Interface UserFilter die Menge der dargestellten Nutzer eingeschränkt werden.
Innerhalb der Methode getInitialGate(), dem Anfangszustand des Prozesses, wird eine neue Instanz von LogOnForm erstellt, die die Oberfläche des Anmeldeprozesses darstellt. Die Standard-Buttons Ok und Cancel werden dabei beibehalten.
protected Gate getInitialGate() {
UIGate uig_log_on = new UIGate(null, null);
LogOnForm lof_initial = new LogOnForm("Are you a registered user?",
"Select your user name", "Enter your passphrase", true,
UserManager.getInstance(), null, null);
lof_initial.addContentCreator(new LogOnLOFContentCreator());
return uig_log_on;
}
Der boolesche Parameter im Konstruktoraufruf entscheidet darüber, ob eine Passwortabfrage erfolgt oder nicht. Der übergebene java.util.Comparator bestimmt, wie die Nutzernamen in der Liste sortiert werden. In diesem Falle ist eine Sortierung nicht nötig, weswegen dafür der Wert null übergeben wird.
Wo die einzelnen String-Objekte, die ebenfalls dem Konstruktor von LogOnForm übergeben werden, in dem Formular auftauchen, kann zur Ausführungszeit betrachtet werden. Dazu muss zuvor das Formular dem initialen UIGate des Anmeldeprozesses hinzugefügt werden. Das geschieht, indem das Formular durch den Aufruf setFormSheet(FormSheet fs) an den Startzustand gebunden wird.
protected Gate getInitialGate() {
UIGate uig_log_on = new UIGate(null, null);
LogOnForm lof_initial = new LogOnForm("Are you a registered user?",
"Select your user name", "Enter your passphrase", true,
UserManager.getInstance(), null, null);
lof_initial.addContentCreator(new LogOnLOFContentCreator());
uig_log_on.setFormSheet(lof_initial);
return uig_log_on;
}
Einen Zustandsübergang definieren
Als Nächstes müssen die vorhandenen Buttons mit Leben gefüllt werden. Im Fall des Cancel-Button soll der Prozess terminieren, während beim Ok-Button die Anmeldung vollführt werden soll. In beiden Fällen werden Zustandsübergänge, also Implementationen von Transition benötigt, die zu den jeweiligen Folgezuständen führen.
Zunächst wird eine eigene FormSheetContentCreator-Klasse für das LogOnForm definiert. Warum das sinnvoll ist, wurde bereits im Kapitel Der FormSheetContentCreator erläutert. Diese Klasse wird dann dem LogOnForm hinzugefügt.
package videoautomat.contentcreator;
public class LogOnLOFContentCreator extends FormSheetContentCreator {
protected void createFormSheetContent(FormSheet fs) {
}
}
Im folgendenen werden nun die Buttons des LogOnForms mit Aktionen verknüpft. Der Cancel-Button soll lediglich vom initialen Zustand zum Stop-Gate wechseln. Für den einfachen Übergang von einem Zustand zum anderen gibt es die Klasse GateChangeTransition, deren Konstruktor der Zielzustand übergeben werden muss, zu dem der Übergang führen soll. Darüberhinaus existieren in dieser Klasse Transition-Konstanten. Diese vorimplementierten Zustandsübergänge führen zu den in der Klasse SaleProcess bereits definierten Zuständen, wie z.B. Stop-Gate. Zur Realisierung dieses Überganges wird eine eigene Action-Klasse definiert, in deren doAction- Methode das aktuelle Gate des übergebenen Prozesses geholt wird und selbigem die nächste Transition zugewiesen wird. Diese Action-Klasse kann nun auch von anderen FormSheetContentCreator-Klassen genutzt werden.
package videoautomat.contentcreator.stdactions;
public class StopAction implements Action {
public void doAction(SaleProcess saleProcess, SalesPoint salePoint)
throws Throwable {
UIGate currentGate = (UIGate) saleProcess.getCurrentGate();
currentGate.setNextTransition(GateChangeTransition.CHANGE_TO_STOP_GATE);
}
}
Die vordefinierten Buttons der Klasse FormSheet erhält man über die Methode getButton(int i) mit Hilfe der Integer-Konstanten FormSheet.BTNID_OK und FormSheet.BTNID_CANCEL. Damit der Zustandsübergang beim Klick auf den Cancel-Button auch vollzogen wird, muss ihm die Action noch über die Methode setAction(Action)zugewiesen werden.
package videoautomat.contentcreator;
public class LogOnLOFContentCreator extends FormSheetContentCreator {
protected void createFormSheetContent(FormSheet fs) {
fs.getButton(FormSheet.BTNID_CANCEL).setAction(new StopAction());
}
}
Schlussendlich muss nun noch der Übergang zum Haupt-Gate implementiert werden. Hierbei kann nicht einfach von Gate zu Gate gewechselt werden, zwischen den Gates müssen noch die Anmeldedaten auf Korrektheit geprüft werden. Abhängig davon wird der Prozess zum Haupt- oder Fehler Gate umgeleitet. Um nicht einfach zwischen den Gates zu wechseln sondern zwischen beiden noch Daten auszuwerten etc., bedarf es der Klasse Transition, die zwischen zwei Gates ausführbar ist. Im Package videoautomat.transition wird dazu eine eigene Transitions-Klasse definiert. Selbige muss die Methode perform(SaleProcess sp, User user) enthalten, welche ein Gate zurückgibt, das den nächsten Zustandsübergang definiert. Es wird außerdem ein Konstruktor definiert, dem eine Instanz von LogOnForm übergeben wird, damit die Anmeldedaten überhaupt auswertbar sind. Durch den Aufruf der ok()-Methode der LogOnForm-Instanz wird geprüft, ob ein Name aus der Liste gewählt wurde und wenn ja, ob das Passwort stimmt. Ist beides der Fall, liefert getResult() die dem Namen entsprechende User-Instanz, andernfalls null. Bei korrekter Anmeldung wird die User-Instanz dem Videoautomaten durch den Aufruf attach(User u) zugeordnet, von wo sie in weiterführenden Prozessen jederzeit abrufbar ist, und es wird zum Haupt-Gate weitergeleitet. Wurde kein Name oder ein inkorrektes Passwort gewählt, wechselt der Prozess zum Fehler-Gate.
package videoautomat.transition;
public class LogOnTransition implements Transition {
private LogOnForm lof;
public LogOnTransition(LogOnForm lof) {
this.lof = lof;
}
public Gate perform(SaleProcess sp, User user) {
SaleProcessLogOn processLogOn = (SaleProcessLogOn) sp;
lof.ok();
User user_current = lof.getResult();
if (user_current != null) {
((SalesPoint) processLogOn.getContext()).attach(user_current);
return processLogOn.getMainGate();
}
return processLogOn.getFaultGate();
}
}
Die neu erstellte Transition muss noch mit dem Ok-Button verbunden werden, wobei die Hilfsklasse TransitWithAction, welche dem aktuellen Gate die nächste Transition zuweist, verwendet wird.
package videoautomat.contentcreator;
public class LogOnLOFContentCreator extends FormSheetContentCreator {
protected void createFormSheetContent(FormSheet fs) {
fs.getButton(FormSheet.BTNID_CANCEL).setAction(new StopAction());
fs.getButton(FormSheet.BTNID_OK).setAction(
new TransitWithAction(new LogOnTransition((LogOnForm) fs)));
}
}
Informieren über ein MsgForm
Am Fehler-Gate soll über die fehlgeschlagene Anmeldung informiert werden und nach erfolgter Bestätigung der Prozess zum Startzustand zurückkehren. Eine für diesen Zweck geeignete Spezialisierung der Klasse FormSheet ist MsgForm. Dabei handelt es sich um ein Formular, das eine Information darstellen kann und einen OK-Button enthält.
Das benötigte Formular wird entsprechend im Fehler-Gate getFaultGate definiert und selbigem zugewiesen:
public Gate getFaultGate() {
UIGate uig_fault = new UIGate(null, null);
MessageFormSheet mf_fault = new MessageFormSheet("Log on failed!", "You didn`t choose a user name or the passphrase didn`t match!", MessageFormIcon.ERROR);
mf_fault.addContentCreator(new LogOnMFContentCreator());
uig_fault.setFormSheet(mf_fault);
return uig_fault;
}
Anschließend muss dem einzigen Button des MsgForms noch eine Aktion zugewiesen werden, die zum Start- Gate des Anmeldeprozesses wechselt. Dies geschieht durch Definition einer Transitions-Klasse, welche den Gate-Wechsel durchführt. Da das Start-Gate getInitialGate() im Prozess geschützt ist, folglich von anderen Klassen nicht darauf zugegriffen werden kann, wird ein Hilfs-Gate restart() definiert, was einfach das Start-Gate zurückgibt.
package videoautomat.transition;
public class LogOnFailTransition implements Transition {
public Gate perform(SaleProcess sp, User user) {
SaleProcessLogOn processLogOn = (SaleProcessLogOn) sp;
return processLogOn.restart();
}
}
package videoautomat.contentcreator;
public class LogOnMFContentCreator extends FormSheetContentCreator {
protected void createFormSheetContent(FormSheet fs) {
fs.getButton(FormSheet.BTNID_OK).setAction(
new TransitWithAction(new LogOnFailTransition()));
}
}
Das Haupt-Gate
Um die Implementation des Anmeldeprozesses abzuschließen, muss noch das Haupt-Gate vervollständigt werden, wo die weiteren Aktivitäten gewählt werden können. Diese Aktivitäten sind im Einzelnen: die Ausleihe, die Rückgabe, die Administration und die Abmeldung. Mit Ausnahme des Abmeldens soll für jede der einzelnen Aktionen ein eigener Prozess gestartet werden. Im Grunde muss das hierfür benötigte Formular lediglich vier Knöpfe für die unterschiedlichen Aktivitäten bieten. Um den Anreiz des Ausleihens zu steigern und die Anzeige nicht völlig leer aussehen zu lassen, wird zusätzlich die Video-Kollektion präsentiert. Der Videobestand wird wie im Abschnitt Eine Angebotstabelle erstellen durch ein Tabellenformular angezeigt. Die Standard-Buttons werden entfernt, die neu benötigten hinzugefügt und das Formular dem Gate zugeordnet. Die Buttons sind bereits mit Actions belegt, welche aber erst in den Folgeprozessen erklärt werden.
public Gate getMainGate() {
ListViewFormSheet stfs_main = ListViewFormSheet.create("Select an action", VideoShop.getVideoStock());
FormSheet fs = new DescriptiveFormSheet(stfs_main, stfs_main.getCaption());
UIGate uig_main = new UIGate(fs, null);
stfs_main.setCellConstraints(.1f, .5f, .165f, 1f);
fs.addContentCreator(new LogOnSTFSContentCreator(this));
return uig_main;
}
Zum Schluss noch ein Gate zur Administrierung und eins zum Restarten.
public Gate getAdministrationGate() {
ListViewFormSheet lvfs = ListViewFormSheet.create("System Users");
lvfs.addContentCreator(new AdministrationContentCreator());
UIGate gate = new UIGate(lvfs, null);
return gate;
}
public Gate restart() {
return getInitialGate();
}
public class LogOnSTFSContentCreator extends FormSheetContentCreator {
private User user;
public LogOnSTFSContentCreator(SaleProcessLogOn process) {
user = (User) process.getContext().getCurrentUser(process);
}
protected void createFormSheetContent(FormSheet fs) {
fs.removeAllButtons();
fs.addButton("Rent", 1, new RunProcessAction(new SaleProcessRent(),
new DataBasketImpl()));
fs.addButton("Hand back", 2, new RunProcessAction(new SaleProcessHandBack(),
new DataBasketImpl()));
fs.addButton("Administrate", 3, (ActionCapability) user
.getCapability(AutomatUser.CAPABILITY_ADMIN));
fs.addButton("Logout", 4, new LogOutAction());
}
}
Für den Abmeldebutton wird eine neue Action definiert, wobei der Nutzer über die Methode detachUser() vom VideoAutomaten abgekoppelt und ein Zustandsübergang zum Stop-Gate durchgeführt wird. Diese Action wird dem Button dann noch angehangen.
package videoautomat.contentcreator.stdactions;
public class LogOutAction implements Action {
public void doAction(SaleProcess saleProcess, SalesPoint sp)
throws Throwable {
sp.detachUser();
UIGate currentGate = (UIGate) saleProcess.getCurrentGate();
currentGate.setNextTransition(GateChangeTransition.CHANGE_TO_STOP_GATE);
}
}