Videoautomat Web
Aus Salespoint
Inhaltsverzeichnis |
Einleitung
Frameworks erleichtern die Programmierarbeit in vielerlei Hinsicht. Sie können Datenstrukturen und Prozesse eines bestimmten Anwendungsgebietes vordefinieren und darüber hinaus einen sauberen Entwurf erzwingen. Dennoch bedeutet ihre Verwendung zunächst einen erhöhten Einarbeitungsaufwand für den Programmierer. Um diesen zu minimieren wurde die folgende Abhandlung geschrieben. Grundlage für das Verständnis dieses Tutorials ist der Technische Überblick über das Framework SalesPoint. Wer diesen bisher noch nicht gelesen hat, wird gebeten diese Abkürzung zu nehmen.
Spring Basics
Zusätzlich kommt als Webframework Spring zum Einsatz. Dieses Framework stellt ein MVC-Implementierung bereit, die Anfragen an den Webserver annimmt, an den richtigen Controller (das Salespoint-Web-Äquivalent zum Salespoint) weiterleitet, der auf dem Model (Catalogs, Stocks, Users) arbeitet und bestimmt welche View (JSP-Datei also HTML-Templates) als Antwort zum Browser zurückgesendet wird. Spring ist ein sehr umfangreiches Framework und hat viele weitere Einsatzgebiete als nur Webapplikationen. Es wird kein allumfassendes Verständnis darüber verlangt, bei auftretenden MVC-Problemen sowie Fragen zur Erweiterung hier angeführter Möglichkeiten sei die Spring-Dokumentation der Version 3 allerdings erste Anlaufstelle. Im Folgenden wird ein ganz grober MVC-Überblick von Spring geliefert. Es bestehen sehr viele andere Konfigurationsmöglichkeiten. Wir verwenden eine annotationenbasierte Konfiguration, die in der SalespointWebBlank.war hinterlegt ist und als Ausgangsbasis für eine neues Projekt benutzt werden kann.
Servlet Engine Konfiguration
In Javabasierten Webprojekten ist eine gewisse Verzeichnisstruktur vorgegeben. Wichtig hierbei ist, dass die Datei WebContent/WEB-INF/web.xml existiert, welche die grundlegende Konfiguration der Webapplikation darstellt. Neben dem Namen und einer Beschreibung der Applikation wird anhand von URL-Pattern festgelegt, welche Anfragen auf welche Servlets (Javaklassen, die ein gewisses Interface Implementieren) abgebildet werden. Da dies nur wenig Abstraktionsmöglichkeiten zulässt, definieren wir neben einem default-Servlet, das nur statische Dateien wie Bilder, CSS-Dateien, etc. ausliefert, nur einen großen FrontController, der alle Anfragen annimmt und delegieren somit das Abbildungsproblem an diesen. Im Falle von Spring ist dies der DisplatchServlet.
WebContent/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<!--Basicsettings-->
<display-name>sp2010_videoautomat_web</display-name>
<description>SalesPoint2010-BlankWebapplication</description>
<!--Mapping of static resources-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>
<!--DispatcherConfig-->
<servlet>
<description>Spring MVC Dispatcher Servlet</description>
<servlet-name>dispatch</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatch</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Als Servlet Engine/Container wird Tomcat in der Version 6.x empfohlen.
Spring Konfiguration
Die Grafik stammt aus der Spring Dokumentation und zeigt Spring's MVC-Prinzip. Der Frontcontroller entspricht, wie oben erwähnt, dem DispatchServlet. Dieser wird in der WebContent/WEB-INF/dispatch-servlet.xml näher konfiguriert.
Bevor darauf näher eingegangen werden kann, gilt es Spring's Dependency Injection zu verstehen. Die Idee dabei ist, Teile der Application möglichst lose miteinander zu koppeln - gemeinsame Abhängigkeiten nicht zwischeneinander ständig hinundherzureichen, sondern von außen zu injizieren. Man lässt somit Spring XML-konfiguriert Instanzen von Klassen erzeugen und jeweils untereinander injizieren.
WebContent/WEB-INF/dispatch-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- messages for i18n -->
1 <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
1.1 <property name="basename" value="messages" />
</bean>
<!-- use the interceptor-enabled annotation based handler mapping -->
2 <bean class="org.salespointframework.web.spring.annotations.SalespointAnnotationHandlerMapping">
<property name="messageSource" ref="messageSource" />
</bean>
<!-- scan this package for annotated controllers -->
3 <context:component-scan base-package="org.salespointframework.web.spring" />
<!-- very standard viewresolver -->
4 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
- Erzeugt eine Instanz der angegebenen Klasse welche später per definierter id referenzierbar ist. In diesem Fall wird eine MessageSource-Instanz erzeugt, die für die Internationalisierung verwendet wird.
- Nach der Instanzierung wird per Setter das Attribut "basename" mit dem String "messages" gesetzt, was bedeutet dass im classpath die Datei messages.properties(sowie für weitere Sprachen z.b. messages_de.properties, messages_en.properties) erwartet wird, in der unter gewissen Codes die richtige Sprachversion des Textes abgelegt wird.
- Instanziiert ein Salespointspezifisches HandlerMapping, welches anhand von annotatierten Klassen ein URL-auf-Controller-Mapping bereitstellt.
- injeziert die MessageSource-Instanz
- Gibt ein Package an, in dem nach annotierten Klassen gesucht werden soll. Dieser Tag kann mehrmals einsetzt werden um mehrere Packages durchsuchen zu lassen.
- Erzeugt ein ViewResolver, der vom Controller zurückgegebene ViewNames auf einen Pfad zur JSP abbildet, z.B "index" => "/jsp/index.jsp"
Zusammenfassend bedeutet diese Konfiguration, dass das Package org.salespointframework.web.spring nach annotierten Klassen durchsucht wird, die selbst per Annotation bestimmen, auf welche URLs sie reagieren, und Strings zurückgeben, die vom viewResolver auf den Pfad zur JSP-Datei gemappt wird.
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.
@Component
public class VideoShop extends Shop {
public VideoShop() {
super();
Shop.setTheShop(this);
}
}
Der Konstruktor von VideoShop ruft den Konstruktor der Oberklasse durch den Befehl super() auf und setzt sich selbst über die statische Methode Shop.setTheShop(). 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. Die Annotation @Component verrät Spring, dass es sich um die Instanziierung dieser Klasse beim Start der Webapplikation zu kümmern hat.
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 im Konstruktor von VideoShop wie folgt instantiiert:
public class VideoShop extends Shop {
.
.
.
public VideoShop() {
.
.
.
addCatalog(new VideoCatalog(C_VIDEOS));
}
}
Der Videokatalog ist durch die Aufnahme in die Katalogsammlung des Ladens von jeder Klasse der Anwendung aus erreichbar, jedoch ist der Aufruf, um an den Katalog zu gelangen unangenehm lang und wird vermutlich mehr als einmal verwendet. Zur Erleichterung wird eine statische Hilfsmethode in der Klasse VideoShop geschaffen, die den Videokatalog zurückgibt.
public class VideoShop extends Shop {
.
.
.
public static VideoCatalog getVideoCatalog() {
return (VideoCatalog) Shop.getTheShop().getCatalog(C_VIDEOS);
}
}
Nun existiert zwar ein Katalog, jedoch ohne Einträge. Damit im weiteren Verlauf des Programmierens und Testens einige Daten zur Verfügung stehen, wird die MainClass um folgende Methode ergänzt:
public class MainClass {
.
.
.
public static void initializeVideos() {
VideoCatalog videoCatalog = VideoShop.getVideoCatalog();
Category c1 = new Category("Action");
Category c2 = new Category("Science Fiction");
List<Video> videos = new ArrayList<Video>();
try {
videos.add(new Video("Event Horizon", "event_horizon", 1999, c2));
videos.add(new Video("H.E.A.T.", "heat", 1999, c1));
videos.add(new Video("Matrix", "matrix", 1499, c2));
videos.add(new Video("Sin City", "sin_city", 2199, null));
videos.add(new Video("Taken (Blue-Ray)", "taken", 3199, c1));
videos.add(new Video("Terminator", "terminator", 999, c1));
videos.add(new Video("Terminator 2", "terminator2", 999, c1));
videos.add(new Video("Terminator 3", "terminator3", 1299, c1));
videos.add(new Video("True Lies", "true_lies", 999, c1));
videos.add(new Video("The X-Files", "xfiles", 999, c2));
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
for (Video video : videos) {
videoCatalog.add(video, null);
}
}
}
Was hier passiert ist relativ leicht ersichtlich. Wir holen uns zuerst den VideoKatalog aus dem Shop mit der vorhin erstellten Methode getVideoCatalog(), erstellen daraufhin 2 Kategorien in unserem Anwendungsfall Genres, nach denen wir später die Videos besser sortieren können und fügen dann nach und nach einzelne Videos mit Titel, Bildtitel, Preiswert in Cent und Genrekategorie in eine zuvor erstellte Arraylist ein, aus der wir in der For-Schleife zum Schluss alles in unseren Katalog schieben. Was uns dazu fehlt ist natürlich die VideoKlasse die von CatalogItemImpl erbt, um in unseren VideoCatalog zu passen und Descriptive und Categorizable implementiert, um das später implementierte Descriptive-Formsheet zu ermöglichen und die Categories nutzbar zu machen. Dem Konstruktor von CatalogItemImpl muss mindestens ein String und ein Value übergeben werden. Value ist ein Interface und es existieren zwei Implementationen dieser Schnittstelle im Framework. Zum Einen NumberValue, welches einen numerischen Wert kapselt und QuoteValue, das ein Paar von Werten repräsentiert, z.B. einen Ein- und Verkaufswert. In diesem Fall wird dem Konstruktor unter anderem eine Instanz von QuoteValue übergeben, welche wiederum mit zwei IntegerValue erzeugt wird. IntegerValue ist lediglich eine Spezialisierung von NumberValue, welche einen int-Wert kapselt, der hierbei der Wert in Cent ist. Der RecoveryConstructor ist nötig für die 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 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 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() {
initializeVideos();
}
.
.
}
Der Videobestand
Nach der Fertigstellung des Katalogs kann im Folgenden der Bestand aufgebaut werden. Wie bereits im Abschnitt Der Videokatalog erwähnt, sollen die Videos des Automaten in einem CountingStock gespeichert werden. Auch für dieses Interface existiert eine vordefinierte Klasse mit dem gewohnten Impl am Ende des Namens. Ein jeder Bestand bezieht sich auf einen Katalog, so dass dieser konsequenterweise neben dem String-Bezeichner dem Konstruktor von CountingStockImpl übergeben werden muss. Ähnlich den Katalogen sind auch die Bestände nach ihren Einträgen getypt, zusätzlich aber auch noch mit den Eintragstypen des zugehörigen Katalogs. Allein aus diesem Grund lohnt es sich, eine eigene Klasse hierfür zu definieren:
package videoautomat;
public class AutomatVideoStock extends
CountingStockImpl<StockItemImpl, CatalogItemImpl> {
public AutomatVideoStock(
StockIdentifier<StockItemImpl, CatalogItemImpl> siId,
Catalog<CatalogItemImpl> ciRef) {
super(siId, ciRef);
}
}
StockItemImpl ist dabei die Standardimplementation eines Bestandseintrages. Wir werden später noch einmal etwas genauer darauf zurückkommen. Am Anfang der Shop-Klasse muss der Identifikator für den neuen Bestand deklariert werden:
public class VideoShop extends Shop {
.
.
.
public static final StockIdentifier<StockItemImpl, CatalogItemImpl> CC_VIDEOS =
new StockIdentifier<StockItemImpl, CatalogItemImpl>("VideoStock");
.
.
.
}
Jetzt können wir den eigentlichen Stock anlegen. Dazu wird analog zum Videokatalog in den Konstruktor von VideoShop folgende Zeile eingefügt:
public class VideoShop extends Shop {
.
.
.
public VideoShop() {
.
.
.
addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS)));
}
.
.
.
}
Auch beim Videobestand lohnt es sich eine Hilfsmethode zu schreiben, die selbigen zurückliefert.
public class VideoShop extends Shop {
.
.
.
public static AutomatVideoStock getVideoStock() {
return (AutomatVideoStock) Shop.getTheShop().getStock(CC_VIDEOS);
}
}
Der neue Bestand ist wiederum leer. Durch das Hinzufügen einer Zeile in die Initialisierungsmethode der Videos in MainClass können dem Videobestand die benötigten Testdaten zugefügt werden.
public static void initializeVideos() {
VideoCatalog videoCatalog = VideoShop.getVideoCatalog();
AutomatVideoStock videoStock = VideoShop.getVideoStock();
.
.
.
for (Video video : videos) {
.
videoStock.add(video.getName(), 5, null);
}
}
Der Aufruf add(String id, int count, DataBasket db) bewirkt, dass von dem Katalogeintrag mit der Bezeichnung id insgesamt count-Stück in den Bestand aufgenommen werden. Der DataBasket, der zum Schluss übergeben wird, hat etwas mit der Sichtbarkeit der vollführten Aktion zu tun. Vorerst reicht es zu wissen, dass hier durch die Übergabe von null das Hinzufügen unmittelbar wirksam wird.
Zum Schluss noch die Klasse, die StockItemImpl erbt und es uns dadurch möglich macht später auf unserem VideoStock zu arbeiten.
package videoautomat;
public class VideoCassette extends StockItemImpl {
@RecoveryConstructor(parameters = { "m_sName" })
public VideoCassette(String key) {
super(key);
}
}