Videoautomat Desktop
Aus Salespoint
Gordon (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „==Die Nutzerregistrierung== Im Gegensatz zu Katalog und Bestand des Automaten benötigen wir hier keinen eigenen Identifikator, da der UserVideoStock stets nur b...“) |
(→Einen Zustandsübergang definieren) |
||
(Der Versionsvergleich bezieht 41 dazwischenliegende Versionen mit ein.) | |||
Zeile 1: | Zeile 1: | ||
- | + | __FORCETOC__ | |
- | + | ==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 sich 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. | ||
+ | |||
+ | [[Datei:Classes.gif]] | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | public class VideoShop extends Shop { | ||
+ | public VideoShop() { | ||
+ | super(); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | public class MainClass { | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | VideoShop myTutorialShop = new VideoShop(); | ||
+ | Shop.setTheShop(myTutorialShop); | ||
+ | myTutorialShop.start(); | ||
+ | } | ||
+ | }</code> | ||
+ | |||
+ | |||
+ | 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: | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | public class VideoAutomat extends SalesPoint { | ||
+ | |||
+ | public VideoAutomat() { | ||
+ | super(VideoShop.CAPTION_AUTOMAT); | ||
+ | } | ||
+ | }</code> | ||
+ | |||
+ | 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. | ||
<code java> | <code java> | ||
- | public class VideoShop extends Shop { | + | public class VideoShop extends Shop { |
+ | . | ||
+ | . | ||
+ | . | ||
+ | public static final String CAPTION_AUTOMAT = "VIDEOAUTOMAT RLD"; | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | }</code> | ||
+ | |||
+ | |||
+ | 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: | ||
+ | <code java> | ||
+ | public class MainClass { | ||
+ | public static void main(String args[]) { | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | myTutorialShop.addSalesPoint(new VideoAutomat()); | ||
+ | } | ||
+ | }</code> | ||
+ | |||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | 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 Interfaces 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. | ||
+ | <code java> | ||
+ | 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 = "Access denied!!!"; | ||
+ | }</code> | ||
+ | |||
+ | |||
+ | 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. | ||
+ | |||
+ | Dementsprechend 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: | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | public static final CatalogIdentifier<CatalogItemImpl> C_VIDEOS = | ||
+ | new CatalogIdentifier<CatalogItemImpl>("VideoCatalog"); | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Diese Konstante soll der künftige Bezeichner für den Videokatalog sein. Kataloge werden in SalesPoint (ähnlich der Java Collection API) nach deren 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: | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | |||
+ | public class VideoCatalog extends CatalogImpl<CatalogItemImpl> { | ||
+ | public VideoCatalog(CatalogIdentifier<CatalogItemImpl> id) { | ||
+ | super(id); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Der Katalog wird in der initializeData Methode wie folgt instantiiert: | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | . | ||
. | . | ||
. | . | ||
public void initializeData() { | public void initializeData() { | ||
. | . | ||
- | + | . | |
+ | . | ||
+ | addCatalog(new VideoCatalog(C_VIDEOS)); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | public static VideoCatalog getVideoCatalog() { | ||
+ | return (VideoCatalog) Shop.getTheShop().getCatalog(C_VIDEOS); | ||
} | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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: | ||
+ | <code java> | ||
+ | public class VideoShop{ | ||
. | . | ||
- | + | . | |
+ | . | ||
+ | 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 { | try { | ||
- | + | videos.add(new Video("Event Horizon", "event_horizon", 1999, c2)); | |
- | + | videos.add(new Video("H.E.A.T.", "heat", 1999, c1)); | |
- | } catch ( | + | 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 ( | + | |
- | + | for (Video video : videos) { | |
- | + | videoCatalog.add(video, null); | |
- | + | ||
- | + | ||
- | + | ||
} | } | ||
} | } | ||
- | + | } | |
- | + | </code> | |
- | </code> | + | 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 Instanzierung des Objektes aus der Datenbank (siehe [[Persistence Layer]]). 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. | ||
- | + | <code java> | |
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | Abschließend wird im Videoshop 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. | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | public void initializeData() { | ||
+ | MainClass.initializeVideos(); | ||
+ | } | ||
+ | . | ||
+ | . | ||
} | } | ||
</code> | </code> | ||
- | |||
- | === | + | ===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: | |
<code java> | <code java> | ||
- | package videoautomat | + | package videoautomat; |
- | public class | + | |
- | + | public class AutomatVideoStock extends | |
+ | CountingStockImpl<StockItemImpl, CatalogItemImpl> { | ||
+ | public AutomatVideoStock( | ||
+ | StockIdentifier<StockItemImpl, CatalogItemImpl> siId, | ||
+ | Catalog<CatalogItemImpl> ciRef) { | ||
+ | super(siId, ciRef); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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: | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | public static final StockIdentifier<StockItemImpl, CatalogItemImpl> CC_VIDEOS = | ||
+ | new StockIdentifier<StockItemImpl, CatalogItemImpl>("VideoStock"); | ||
+ | . | ||
+ | . | ||
+ | . | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Jetzt können wir den eigentlichen Stock anlegen. Dazu wird analog zum Videokatalog in die initializeData Methode von VideoShop folgende Zeile eingefügt: | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
. | . | ||
. | . | ||
. | . | ||
- | + | public void initializeData() { | |
- | + | . | |
+ | . | ||
+ | . | ||
+ | addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS))); | ||
+ | } | ||
. | . | ||
. | . | ||
. | . | ||
- | |||
} | } | ||
</code> | </code> | ||
- | + | Auch beim Videobestand lohnt es sich eine Hilfsmethode zu schreiben, die selbigen zurückliefert. | |
<code java> | <code java> | ||
- | + | public class VideoShop extends Shop { | |
- | public class | + | . |
- | + | . | |
- | + | . | |
- | + | public static AutomatVideoStock getVideoStock() { | |
- | + | return (AutomatVideoStock) Shop.getTheShop().getStock(CC_VIDEOS); | |
- | + | } | |
- | + | } | |
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | public static void initializeVideos() { | ||
+ | |||
+ | VideoCatalog videoCatalog = VideoShop.getVideoCatalog(); | ||
+ | AutomatVideoStock videoStock = VideoShop.getVideoStock(); | ||
. | . | ||
. | . | ||
- | + | . | |
- | + | ||
- | + | for (Video video : videos) { | |
- | + | . | |
- | + | videoStock.add(video.getName(), 5, null); | |
- | + | } | |
- | + | } | |
- | + | </code> | |
- | + | ||
- | + | 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. | |
- | + | <code java> | |
- | + | package videoautomat; | |
- | + | ||
+ | |||
+ | public class VideoCassette extends StockItemImpl { | ||
+ | |||
+ | @RecoveryConstructor(parameters = { "m_sName" }) | ||
+ | public VideoCassette(String key) { | ||
+ | super(key); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==StandardFormsheet== | ||
+ | |||
+ | Wie im Technischen Überblick erläutert wird, kann auf einem Display ein FormSheet und/oder ein MenuSheet angezeigt werden. | ||
+ | |||
+ | Als Begrüß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. | ||
+ | <code java> | ||
+ | protected FormSheet getDefaultFormSheet() { | ||
+ | FormSheet fs = super.getDefaultFormSheet(); | ||
+ | fs.addContentCreator(new StartFSContentCreator()); | ||
+ | |||
+ | return fs; | ||
+ | } | ||
+ | </code> | ||
+ | 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. | ||
+ | <code java> | ||
+ | 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())); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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: | ||
+ | <code java> | ||
+ | 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); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | Die Methode renderAdditionalInfoArea kriegt einen JPanel übergeben und kann dort erweiterte Informationen zum jeweiligen Benutzer hinein rendern, die in Benutzerübersichten angezeigt werden (z.B. der LogOnForm). In diesem Fall wird die Anzahl der ausgeliehenen Videos des Benutzers angezeigt. | ||
+ | |||
+ | Die dataSourceOnChange() Methode wird aufgerufen, kurz bevor sich die Datenquelle ändert. Um die Resourcen sauber freizugeben sollte der VideoStock vom PersistenceManager abgemeldet werden, damit eine Garbage Collection stattfinden kann. | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | public class AutomatUser extends User { | ||
+ | |||
+ | 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; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * free internal resources | ||
+ | * | ||
+ | */ | ||
+ | public void DatasourceOnChange() { | ||
+ | super.DatasourceOnChange(); | ||
+ | if (ss_videos != null) | ||
+ | ss_videos.unsubscribe(); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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 (aufgrund der Typisierung), eine neue Klasse UserVideoStock anzulegen, die ebendiese generischen Parameter festlegt: | ||
+ | <code java> | ||
+ | package videoautomat; | ||
+ | public class UserVideoStock extends | ||
+ | StoringStockImpl<VideoCassette, CatalogItemImpl> { | ||
+ | public UserVideoStock(String sName, CatalogImpl<CatalogItemImpl> ciRef) { | ||
+ | super(sName, ciRef); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | ==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. | ||
+ | <code java> | ||
+ | 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; | ||
+ | } | ||
+ | |||
} | } | ||
</code> | </code> | ||
- | + | 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. | ||
+ | <code java> | ||
+ | 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; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | 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; | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | ===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. | ||
<code java> | <code java> | ||
package videoautomat.contentcreator; | package videoautomat.contentcreator; | ||
- | public class | + | |
- | + | public class LogOnLOFContentCreator extends FormSheetContentCreator { | |
- | + | ||
- | + | protected void createFormSheetContent(FormSheet fs) { | |
- | + | ||
- | + | } | |
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | } | |
- | + | </code> | |
- | + | 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. | |
- | + | <code java> | |
+ | 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); | ||
+ | } | ||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
} | } | ||
</code> | </code> | ||
- | + | 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. | |
<code java> | <code java> | ||
- | + | package videoautomat.contentcreator; | |
- | + | ||
- | + | public class LogOnLOFContentCreator extends FormSheetContentCreator { | |
- | public class | + | |
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
- | + | ||
+ | protected void createFormSheetContent(FormSheet fs) { | ||
+ | fs.getButton(FormSheet.BTNID_CANCEL).setAction(new StopAction()); | ||
+ | |||
+ | } | ||
} | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | 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))); | ||
+ | |||
+ | } | ||
} | } | ||
</code> | </code> | ||
- | |||
- | + | <code java> | |
+ | package videoautomat.transition; | ||
- | == | + | 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); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
- | + | ===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: | ||
+ | <code java> | ||
+ | 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; | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
<code java> | <code java> | ||
package videoautomat.transition; | package videoautomat.transition; | ||
- | public class | + | |
- | + | public class LogOnFailTransition implements Transition { | |
+ | |||
+ | public Gate perform(SaleProcess sp, User user) { | ||
+ | SaleProcessLogOn processLogOn = (SaleProcessLogOn) sp; | ||
+ | return processLogOn.restart(); | ||
+ | } | ||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
- | |||
} | } | ||
+ | </code> | ||
+ | <code java> | ||
+ | package videoautomat.contentcreator; | ||
+ | public class LogOnMFContentCreator extends FormSheetContentCreator { | ||
+ | |||
+ | protected void createFormSheetContent(FormSheet fs) { | ||
+ | fs.getButton(FormSheet.BTNID_OK).setAction( | ||
+ | new TransitWithAction(new LogOnFailTransition())); | ||
+ | } | ||
+ | |||
+ | } | ||
</code> | </code> | ||
- | + | ||
+ | ===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. Genutzt wird dazu das ListViewFormsheet, welches mit einem String und einem Stock übergeben eine zufriedenstellende Ansicht der Videoauswahl gewährleistet. Dieses sowie sein Captionstring wird in ein DescriptiveFormsheet übergeben um eine besondere Darstellung mit zusätzlichen Informationen zum Video aufzuzeigen, wofür wir die getDescription() Methode im Video deklarierten. Das ListViewFormSheet konstruiert ein FormSheet mit einer JListView Komponente. Dies ist eine Liste die erweitere Renderingmöglichkeiten zur Verfügung stellt. | ||
<code java> | <code java> | ||
- | + | 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; | |
- | + | ||
- | + | } | |
- | + | </code> | |
- | + | Zum Schluss noch ein Gate zur Administrierung und eins zum Restarten. | |
- | + | <code java> | |
- | + | 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(); | ||
+ | } | ||
</code> | </code> | ||
- | |||
- | + | Es folgt die Deklaration des Contentcreators für unser Hauptgate, in der wir die Buttons und deren Funktion bereitstellen. Man beachte vor allem den Administrate-Button der eine ActionCapability und damit eine Action - im AutomatUser definiert - triggern, die uns zum AdministrationGate setzt. Eine andere Variante der Implementation. | |
- | + | <code java> | |
+ | 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()); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 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. | ||
+ | <code java> | ||
+ | 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); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> |
Aktuelle Version vom 19:27, 7. Nov. 2010
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 sich 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.
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 args[]) {
.
.
.
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 Interfaces 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 = "Access 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.
Dementsprechend 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 deren 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 in der initializeData Methode wie folgt instantiiert:
public class VideoShop extends Shop {
.
.
.
public void initializeData() {
.
.
.
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 VideoShop{
.
.
.
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 Instanzierung des Objektes aus der Datenbank (siehe Persistence Layer). 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 im Videoshop 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 VideoShop extends Shop {
.
.
.
public void initializeData() {
MainClass.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 die initializeData Methode von VideoShop folgende Zeile eingefügt:
public class VideoShop extends Shop {
.
.
.
public void initializeData() {
.
.
.
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 Begrüß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.
Die Methode renderAdditionalInfoArea kriegt einen JPanel übergeben und kann dort erweiterte Informationen zum jeweiligen Benutzer hinein rendern, die in Benutzerübersichten angezeigt werden (z.B. der LogOnForm). In diesem Fall wird die Anzahl der ausgeliehenen Videos des Benutzers angezeigt.
Die dataSourceOnChange() Methode wird aufgerufen, kurz bevor sich die Datenquelle ändert. Um die Resourcen sauber freizugeben sollte der VideoStock vom PersistenceManager abgemeldet werden, damit eine Garbage Collection stattfinden kann.
package videoautomat;
public class AutomatUser extends User {
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;
}
/**
* 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 (aufgrund 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 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)));
}
}
package videoautomat.transition;
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);
}
}
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. Genutzt wird dazu das ListViewFormsheet, welches mit einem String und einem Stock übergeben eine zufriedenstellende Ansicht der Videoauswahl gewährleistet. Dieses sowie sein Captionstring wird in ein DescriptiveFormsheet übergeben um eine besondere Darstellung mit zusätzlichen Informationen zum Video aufzuzeigen, wofür wir die getDescription() Methode im Video deklarierten. Das ListViewFormSheet konstruiert ein FormSheet mit einer JListView Komponente. Dies ist eine Liste die erweitere Renderingmöglichkeiten zur Verfügung stellt.
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();
}
Es folgt die Deklaration des Contentcreators für unser Hauptgate, in der wir die Buttons und deren Funktion bereitstellen. Man beachte vor allem den Administrate-Button der eine ActionCapability und damit eine Action - im AutomatUser definiert - triggern, die uns zum AdministrationGate setzt. Eine andere Variante der Implementation.
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);
}
}