Videoautomat Web
Aus Salespoint
Lk (Diskussion | Beiträge) (→Der Videobestand) |
Uwe (Diskussion | Beiträge) (→Geldkatalog) |
||
(Der Versionsvergleich bezieht 67 dazwischenliegende Versionen mit ein.) | |||
Zeile 1: | Zeile 1: | ||
=Einleitung= | =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. | 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. | ||
+ | |||
+ | Des weiteren sehr hilfreich zu Installation der Entwicklungsumgebung ist dieser [http://www.youtube.com/watch?v=JNCIa4uhxq0 ScreenCast]. | ||
==Spring Basics== | ==Spring Basics== | ||
- | Zusätzlich kommt als Webframework Spring zum Einsatz. Dieses Framework stellt | + | Zusätzlich kommt als Webframework Spring zum Einsatz. Dieses Framework stellt eine 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) 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 [http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ 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 | + | 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 [http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ 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 annotationsbasierte Konfiguration, die in der salespoint-2010-blankweb.war hinterlegt ist und als Ausgangsbasis für eine neues Projekt benutzt werden kann. |
===Servlet Engine Konfiguration=== | ===Servlet Engine Konfiguration=== | ||
Zeile 120: | Zeile 122: | ||
<code java> | <code java> | ||
public class VideoShop extends Shop { | public class VideoShop extends Shop { | ||
- | public static final | + | public static final String C_VIDEOS = "VideoCatalog"; |
. | . | ||
. | . | ||
Zeile 128: | Zeile 130: | ||
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: | 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> | <code java> | ||
- | public class VideoCatalog extends CatalogImpl<CatalogItemImpl> { | + | public class VideoCatalog extends CatalogImpl<CatalogItemImpl> |
- | + | { | |
- | + | @RecoveryConstructor(parameters = {"m_sName"}) | |
- | + | public VideoCatalog(String name) { | |
+ | super(name); | ||
+ | } | ||
} | } | ||
</code> | </code> | ||
Zeile 143: | Zeile 147: | ||
. | . | ||
. | . | ||
- | addCatalog(new VideoCatalog(C_VIDEOS)); | + | try |
+ | { | ||
+ | addCatalog(new VideoCatalog(C_VIDEOS)); | ||
+ | . | ||
+ | . | ||
+ | } catch(DuplicateKeyException dke){} | ||
+ | . | ||
+ | . | ||
} | } | ||
} | } | ||
Zeile 231: | Zeile 242: | ||
. | . | ||
try { | try { | ||
+ | . | ||
+ | . | ||
initializeData(); | initializeData(); | ||
} catch(DuplicateKeyException dke){} | } catch(DuplicateKeyException dke){} | ||
Zeile 248: | Zeile 261: | ||
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: | 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> | ||
- | public class AutomatVideoStock extends CountingStockImpl<StockItemImpl, CatalogItemImpl> { | + | public class AutomatVideoStock extends CountingStockImpl<StockItemImpl, CatalogItemImpl> |
- | + | { | |
- | + | @RecoveryConstructor(parameters={"m_sName", "m_ciCatalog"}) | |
- | + | public AutomatVideoStock(String name, VideoCatalog ciRef) { | |
+ | super(name, ciRef); | ||
+ | } | ||
} | } | ||
</code> | </code> | ||
Zeile 260: | Zeile 275: | ||
. | . | ||
. | . | ||
- | public static final | + | public static final String CC_VIDEOS = "VideoStock"; |
. | . | ||
. | . | ||
Zeile 274: | Zeile 289: | ||
. | . | ||
. | . | ||
- | addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS))); | + | try |
+ | { | ||
+ | . | ||
+ | . | ||
+ | addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS))); | ||
+ | . | ||
+ | . | ||
+ | } catch(DuplicateKeyException dke){} | ||
} | } | ||
. | . | ||
Zeile 323: | Zeile 345: | ||
} | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==Geldkatalog== | ||
+ | Da wir eine Bezahlfunktion einbauen wollen, brauchen wir eine Repräsentation von gültigen Geldeinheiten respektive Münzen/Scheine. Um diesen nicht bei Gebrauch ständig neu instanziieren zu müssen, legen wir ihn einfach als Katalog im Shop an. | ||
+ | <code java> | ||
+ | public class VideoShop extends Shop { | ||
+ | . | ||
+ | . | ||
+ | public static final String C_CURRENCY = "CurrencyCatalog"; | ||
+ | . | ||
+ | . | ||
+ | public VideoShop() { | ||
+ | . | ||
+ | . | ||
+ | try | ||
+ | { | ||
+ | . | ||
+ | . | ||
+ | super.addCatalog(new EUROCurrencyImpl(C_CURRENCY)); | ||
+ | . | ||
+ | . | ||
+ | } | ||
+ | . | ||
+ | . | ||
+ | public void initializeData() { | ||
+ | . | ||
+ | . | ||
+ | initializeMoney(); | ||
+ | } | ||
+ | . | ||
+ | . | ||
+ | public void initializeMoney() { | ||
+ | getCurrency().initCurrencyItems(); | ||
+ | } | ||
+ | . | ||
+ | . | ||
+ | public static EUROCurrencyImpl getCurrency() { | ||
+ | return (EUROCurrencyImpl)Shop.getTheShop().getCatalog(C_CURRENCY); | ||
+ | } | ||
} | } | ||
</code> | </code> | ||
Zeile 340: | Zeile 402: | ||
<code java> | <code java> | ||
- | |||
public class AutomatUser extends User implements Recoverable { | public class AutomatUser extends User implements Recoverable { | ||
- | + | @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) { | |
- | + | super(user_ID); | |
- | + | setPassWd(garblePassWD(passWd)); | |
- | + | ss_videos = new UserVideoStock(user_ID, VideoShop.getVideoCatalog()); | |
- | + | } | |
- | + | public UserVideoStock getVideoStock() { | |
- | + | return ss_videos; | |
- | + | } | |
- | + | public void recover(Map<String, Object> data, Object recoveryContext, boolean reInit) { | |
- | + | if (!reInit) | |
- | + | ss_videos = new UserVideoStock(getName(), VideoShop.getVideoCatalog()); | |
- | + | } | |
- | + | ||
- | + | ||
} | } | ||
- | + | </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. | 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. | ||
Zeile 374: | Zeile 433: | ||
Ä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: | Ä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> | <code java> | ||
- | + | public class UserVideoStock extends StoringStockImpl<VideoCassette, CatalogItemImpl> | |
- | public class UserVideoStock extends | + | { |
- | + | ||
- | public UserVideoStock(String sName, | + | @RecoveryConstructor(parameters={"m_sName", "m_ciCatalog"}) |
- | super(sName, ciRef); | + | public UserVideoStock(String sName, VideoCatalog ciRef) { |
+ | super(sName, ciRef, VideoCassette.class); | ||
} | } | ||
- | } | + | } |
- | + | ||
</code> | </code> | ||
Zeile 394: | Zeile 453: | ||
Um den verschiedene Nutzergruppen zu unterscheiden, können ihnen verschiedene Capabilities zugeordnet werden. In unserm Fall eine mit dem Namen "admin". | Um den verschiedene Nutzergruppen zu unterscheiden, können ihnen verschiedene Capabilities zugeordnet werden. In unserm Fall eine mit dem Namen "admin". | ||
<code java> | <code java> | ||
- | + | public class VideoShop extends Shop { | |
- | + | . | |
- | + | . | |
- | + | public void initializeData() { | |
- | + | . | |
- | + | . | |
- | + | initializeUsers(); | |
- | + | } | |
- | + | . | |
- | + | . | |
- | + | public static void initializeUsers() { | |
- | + | try { | |
- | + | AutomatUser usr = new AutomatUser("Administrator", "")); | |
- | + | usr.setCapability(new NameCapability("admin")); | |
+ | UserManager.getInstance().addUser(usr); | ||
+ | } catch (DuplicateUserException dke) {} | ||
+ | } | ||
+ | } | ||
</code> | </code> | ||
Zeile 418: | Zeile 481: | ||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> | ||
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | ||
- | <%@ taglib uri="http:// | + | <%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %> |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | <html xmlns="http://www.w3.org/1999/xhtml"> | ||
Zeile 469: | Zeile 532: | ||
Zuerst einmal fügen wir auf der Startseite einen Link auf das Registrierformular hinzu: | Zuerst einmal fügen wir auf der Startseite einen Link auf das Registrierformular hinzu: | ||
<code xml> | <code xml> | ||
+ | <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | ||
. | . | ||
<sp:LoggedIn status="false"> | <sp:LoggedIn status="false"> | ||
Zeile 496: | Zeile 560: | ||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | ||
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> | ||
+ | <%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %> | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | <html xmlns="http://www.w3.org/1999/xhtml"> | ||
Zeile 558: | Zeile 623: | ||
if(userPasswd.isEmpty()) | if(userPasswd.isEmpty()) | ||
MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.empty", new String[]{"password"}, RequestContextUtils.getLocale(request))); | MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.empty", new String[]{"password"}, RequestContextUtils.getLocale(request))); | ||
- | if(UserManager.getInstance().getUser( | + | if(UserManager.getInstance().getUser(userName) != null) |
MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.userNameNotFree", new String[]{userName}, RequestContextUtils.getLocale(request))); | MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.userNameNotFree", new String[]{userName}, RequestContextUtils.getLocale(request))); | ||
Zeile 566: | Zeile 631: | ||
AutomatUser usr = new AutomatUser(userName, userPasswd); | AutomatUser usr = new AutomatUser(userName, userPasswd); | ||
UserManager.getInstance().addUser(usr); | UserManager.getInstance().addUser(usr); | ||
- | MessagesUtil.addFlash(mav, | + | MessagesUtil.addFlash(mav, |
- | return | + | mav.setViewName("redirect:/"); |
+ | return mav; | ||
} | } | ||
} | } | ||
Zeile 577: | Zeile 643: | ||
videoautomat.empty = input field ''{0}'' should not be empty | videoautomat.empty = input field ''{0}'' should not be empty | ||
videoautomat.userNameNotFree = user name ''{0}'' is already taken by another user | videoautomat.userNameNotFree = user name ''{0}'' is already taken by another user | ||
- | |||
</code> | </code> | ||
OK, nun der Reihe nach: | OK, nun der Reihe nach: | ||
Zeile 583: | Zeile 648: | ||
*;MessageUtil: Toolbox zum Hinzufügen(add)/Abfragen(has)/Löschen(clear) von Fehler-(Error) oder Erfolgs-(Flash)nachrichten. Diese werden in einer Liste von Strings unter den Schlüsseln "spErrors" bzw. "spFlash" in der ModelMap abgelegt und können unter diesen auch wieder abgefragt werden (siehe JSP). | *;MessageUtil: Toolbox zum Hinzufügen(add)/Abfragen(has)/Löschen(clear) von Fehler-(Error) oder Erfolgs-(Flash)nachrichten. Diese werden in einer Liste von Strings unter den Schlüsseln "spErrors" bzw. "spFlash" in der ModelMap abgelegt und können unter diesen auch wieder abgefragt werden (siehe JSP). | ||
- | Falls etwas schiefgegangen ist und Fehler dem ModelAndView hinzugefügt wurden, wird diesem der registerForm()-Methode übergeben, die auf der JSP das Formular sowie die Fehlermeldungen aus der MAV anzeigt. Falls keine Fehler aufgetreten sind, wird eine neue Instanz der Klasse AutomatUser erstellt, dem UserManager hingefügt | + | Falls etwas schiefgegangen ist und Fehler dem ModelAndView hinzugefügt wurden, wird diesem der registerForm()-Methode übergeben, die auf der JSP das Formular sowie die Fehlermeldungen aus der MAV anzeigt. Falls keine Fehler aufgetreten sind, wird eine neue Instanz der Klasse AutomatUser erstellt, dem UserManager hingefügt sowie auf die Hauptseite "geroutet". |
Diese Funktionalität sollte nun erstmal wieder getestet werden. | Diese Funktionalität sollte nun erstmal wieder getestet werden. | ||
Zeile 652: | Zeile 717: | ||
*;VideoCassetteDBESSTED: Mit einem ''TableEntryDescriptor''(TED) kann man bestimmen, wie sich die Elemente eines Catalogs/Stocks/DataBaskets in einer Visualisierung beschreiben sollen. Da wir hier einen DataBasket visualisieren wollen und dafür kein DefaultTED existiert, müssen wir einen angeben, der z.B. so aussieht: | *;VideoCassetteDBESSTED: Mit einem ''TableEntryDescriptor''(TED) kann man bestimmen, wie sich die Elemente eines Catalogs/Stocks/DataBaskets in einer Visualisierung beschreiben sollen. Da wir hier einen DataBasket visualisieren wollen und dafür kein DefaultTED existiert, müssen wir einen angeben, der z.B. so aussieht: | ||
<code java> | <code java> | ||
- | public class | + | public class VideoCassetteDBESSTED extends AbstractTableEntryDescriptor { |
private static final String[] cNames = { "Name", "Price" }; | private static final String[] cNames = { "Name", "Price" }; | ||
Zeile 687: | Zeile 752: | ||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | ||
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> | ||
- | <%@ taglib uri="http:// | + | <%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %> |
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | ||
Zeile 734: | Zeile 799: | ||
===Bezahlen=== | ===Bezahlen=== | ||
+ | Für das Ziel des "pay"-Buttons auf der Ausleihseite - die Bezahlseite - brauchen wir einen neuen Controller, der von DoubleViewController erbt und auf die URL "/pay" lauscht: | ||
+ | <code java> | ||
+ | @Controller | ||
+ | @Scope("session") | ||
+ | @RequestMapping("/pay") | ||
+ | public class PayController extends DoubleViewController<Currency<CurrencyItemImpl>, MoneyBag> { | ||
+ | |||
+ | public PayController() { | ||
+ | initialize( | ||
+ | VideoShop.getCurrency(), | ||
+ | new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true), | ||
+ | new CCSStrategy(), | ||
+ | null); | ||
+ | } | ||
+ | |||
+ | @RequestMapping("") | ||
+ | public ModelAndView index(ModelAndView mav, HttpServletRequest request, HttpServletResponse response) { | ||
+ | |||
+ | Value pricePaid = calculatePricePaid(); | ||
+ | Value priceToPay = calculatePriceToPay(); | ||
+ | |||
+ | mav.addObject("pricePayedEnough", pricePaid.compareTo(priceToPay) >= 0); | ||
+ | |||
+ | MessagesUtil.addFlash(mav, messageSource.getMessage("videoautomat.pricePayed", | ||
+ | new String[{""+pricePaid, | ||
+ | ""+priceToPay, | ||
+ | EUROCurrencyImpl.ABBREVIATION}, | ||
+ | LocaleContextHolder.getLocale())); | ||
+ | /* videoautomat.pricePayed = {0} of {1} {2} payed */ | ||
+ | |||
+ | mav.addObject("currency",new ATMBuilder<Currency<CurrencyItemImpl>>(getSource()).getATM()); | ||
+ | |||
+ | mav.addObject("moneyBag", new ATMBuilder<MoneyBag>(getDestination()) | ||
+ | .zeros(false) | ||
+ | .getATM()); | ||
+ | |||
+ | mav.addObject("currencyExtraColumns", Collections.singletonList(new EuroCurrencyImageEC("50", null))); | ||
+ | |||
+ | mav.setViewName("pay"); | ||
+ | return mav; | ||
+ | } | ||
+ | |||
+ | private Value calculatePricePaid() { | ||
+ | return getDestination().sumStock(getDataBasket(), new CatalogItemValue(), new DoubleValue(0)).divide(new IntegerValue(100)); | ||
+ | } | ||
+ | |||
+ | private Value calculatePriceToPay() { | ||
+ | ??? | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | Ein weiterer konkreter DoubleViewController: diesmal im Konstruktor initialisiert, da wir kein Request-Objekt benötigen. Der PayController verschiebt aus einem speziellen Geldkatalog "Currency" (sp incl.) in einen speziellen CountingStock "MoneyBag" (sp incl.) und symbolisiert das Einwerfen von Geldscheinen/-münzen in den Automat. | ||
+ | |||
+ | Innerhalb der ''index()''-Methode berechnen wir den bereits gezahlten Preis bequem über die mittels ''sumStock()''-Methode. Den zu zahlenden Betrag allerdings lässt sich nur mit der Information was ausgeliehen werden soll berechnen. Wir benötigen also den DataBasket aus dem RentController. Hierzu bedienen wir uns wieder der ''@Autowired''-Annotation und lassen uns den RentController automatisch injizieren. Das geht, weil dieser mit ''@Controller'' annotiert und somit von Spring instanziiert wurde. | ||
+ | |||
+ | <code java> | ||
+ | public class PayController extends DoubleViewController<Currency<CurrencyItemImpl>, MoneyBag> { | ||
+ | |||
+ | @Autowired | ||
+ | private RentController rentController; | ||
+ | |||
+ | public void setRentController(RentController rentController) { | ||
+ | this.rentController = rentController; | ||
+ | } | ||
+ | . | ||
+ | . | ||
+ | private Value calculatePriceToPay() { | ||
+ | return rentController.getDataBasket().sumBasket( | ||
+ | DataBasketConditionImpl.allStockItemsWithDest(rentController.getDestination()), | ||
+ | new BasketEntryValue() { | ||
+ | @Override | ||
+ | public Value getEntryValue(DataBasketEntry dbe) { | ||
+ | return ((VideoCassette)dbe.getValue()).getAssociatedItem(null).getValue(); | ||
+ | } | ||
+ | }, | ||
+ | new DoubleValue(0)); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | Nun können wir über den DataBasket fast genauso komfortabel summieren: "fast", weil eine Mapperklasse mitgegeben werden muss, dessen einzige Aufgabe es ist, das zu summierende ''Value'' aus dem DataBasketEntry herauszuholen. | ||
+ | |||
+ | In der ModelMap stehen nun neben der FlashMessage, wieviel noch gezahlt werden muss, ein boolscher Wert "pricePayedEnough", der ''true'' wird, wenn genug gezahl wurde, sowie das ATM des Geldkatalogs und das des bereits eingezahlten Geldes. Neu hierbei sind die ''ExtraColumn''s (EC), die als Alternative bzw. Erweiterung neben den bereits bekannten TEDs benutzt werden können, um eine bzw. mehrere Zusatzspalten hinzurendern. ECs werden in einer Liste verpackt direkt dem ViewTag übergeben. Die verwendete EC benutzt die ImageTag-Klasse, die sonst aus dem JSP-Context heraus als JSP-Tag benutzt wird, beispielhaft im JavaSourceCode, um zur jeweiligen Geldeinheit das entsprechende Bild zu rendern. | ||
+ | <code java> | ||
+ | public class EuroCurrencyImageEC extends AbstractExtraColumn<CurrencyItemImpl> { | ||
+ | |||
+ | private String height; | ||
+ | private String width; | ||
+ | |||
+ | public EuroCurrencyImageEC(String height, String width) { | ||
+ | super("x", null); | ||
+ | this.height = height; | ||
+ | this.width = width; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public String getCellContent(CurrencyItemImpl identifier) { | ||
+ | ImageTag tag = new ImageTag(); | ||
+ | tag.setImage(identifier.getImage()); | ||
+ | tag.setAlt(identifier.getName()); | ||
+ | tag.setHeight(height); | ||
+ | tag.setWidth(width); | ||
+ | return tag.render().toString(); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | Die ''WebContent/jsp/pay.jsp'' könnte so aussehen: | ||
+ | <code xml> | ||
+ | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> | ||
+ | <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | ||
+ | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | ||
+ | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> | ||
+ | <%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %> | ||
+ | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | ||
+ | <html xmlns="http://www.w3.org/1999/xhtml"> | ||
+ | <head> | ||
+ | <meta http-equiv="Content-Type" content="text/html; charset=uft-8" /> | ||
+ | <link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" /> | ||
+ | <title><spring:message code="videoautomat.pay" /></title> | ||
+ | </head> | ||
+ | <body> | ||
+ | |||
+ | <sp:Messages messages="${spErrors}" /> | ||
+ | <sp:Messages messages="${spFlash}" /> | ||
+ | |||
+ | <sp:LoggedIn status="true"> | ||
+ | |||
+ | <sp:DoubleView showNumberField="true" > | ||
+ | <sp:List abstractTableModel="${currency}" extraCols="${currencyExtraColumns}" /> | ||
+ | <sp:Table abstractTableModel="${moneyBag}" /> | ||
+ | </sp:DoubleView> | ||
+ | |||
+ | <form:form method="get" action="pay/success" id="success"> | ||
+ | <c:if test="${pricePayedEnough}"> | ||
+ | <input type="submit" value="<spring:message code="videoautomat.pay" />" /> | ||
+ | </c:if> | ||
+ | <c:if test="${!pricePayedEnough}"> | ||
+ | <input disabled="disabled" type="submit" value="<spring:message code="videoautomat.pay" />" /> | ||
+ | </c:if> | ||
+ | </form:form> | ||
+ | <form:form method="get" action="pay/cancel" id="cancel"> | ||
+ | <input type="submit" value="<spring:message code="videoautomat.cancel" />" /> | ||
+ | </form:form> | ||
+ | |||
+ | </sp:LoggedIn> | ||
+ | |||
+ | </body> | ||
+ | </html> | ||
+ | </code> | ||
+ | Neu hierbei ist die Verwendung von Kontrollstrukturen aus der JSTL-TagLibrary (''c''-Namespace) mit dessen Hilfe hier der boolsche Wert, ob genug bezahlt worden ist, abgeprüft wird und entsprechend der ''pay''-Button de-/aktiviert wird. | ||
+ | |||
+ | Der Pay- sowie Cancel-Button brauchen natürlich noch entsprechende Actions im PayController: | ||
+ | <code java> | ||
+ | @RequestMapping("/success") | ||
+ | public ModelAndView success(ModelAndView mav, HttpServletRequest request) { | ||
+ | /* videoautomat.rent.successful = rent was process successful, payback is {0} EUR */ | ||
+ | MessagesUtil.addFlash(mav, messageSource.getMessage("videoautomat.rent.successful", | ||
+ | new Object[]{calculatePricePaid().subtract(calculatePriceToPay()), | ||
+ | EUROCurrencyImpl.ABBREVIATION}, | ||
+ | RequestContextUtils.getLocale(request))); | ||
+ | /* add videocassettes to uservideostock */ | ||
+ | rentController.getDataBasket().commit(); | ||
+ | |||
+ | /* clean the moneybag by setting fresh instance */ | ||
+ | setDestination(new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true)); | ||
+ | |||
+ | AutomatUser currentUser = (AutomatUser) UserManager.getInstance().getCurrentUser(request.getSession()); | ||
+ | mav.addObject("currentUserVideoStockATM", new ATMBuilder<UserVideoStock>(currentUser.getVideoStock()).getATM()); | ||
+ | mav.setViewName("rentSuccessful"); | ||
+ | return mav; | ||
+ | } | ||
+ | |||
+ | @RequestMapping("/cancel") | ||
+ | public ModelAndView cancel(ModelAndView mav, HttpServletRequest request) { | ||
+ | setDestination(new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true)); | ||
+ | return rentController.cancel(mav, request); | ||
+ | } | ||
+ | </code> | ||
+ | Die ''success''-Methode bestätigt den erfolgreichen Ausleihvorgang inkl. Rückgeldbetrag mit einer Erfolgsnachricht und ''committ''et den DataBasket vom RentController (verschiebt die VideoCassette-Instanzen persistent vom AutomatVideoStock zum UserVideoStock). '''Achtung:''' Außerdem setzt sie eine neue MoneyBag-Instanz - das ist wichtig, denn ein Controller mit Session-Scope bleibt erhalten bis die Sission endet(ServerEinstellung), d.h. beim eventl. 2.Ausleihvorgang während der gleichen Session wäre sonst der Bezahlbetrag vom 1.Ausleihvorgang noch enthalten! | ||
+ | Abschließend soll in der JSP der UserVideoStock angezeigt werden, also holen wir uns den User der aktuellen Session und fügen ein mittels ATMBuilder erstelltes ATM von seinem UserVideoStock der ModelMap hinzug. | ||
+ | |||
+ | ''WebContent/jsp/rentSuccessful.jsp'': | ||
+ | <code xml> | ||
+ | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> | ||
+ | <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> | ||
+ | <%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %> | ||
+ | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> | ||
+ | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | ||
+ | <html xmlns="http://www.w3.org/1999/xhtml"> | ||
+ | <head> | ||
+ | <meta http-equiv="Content-Type" content="text/html; charset=uft-8" /> | ||
+ | <link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" /> | ||
+ | <title>SalesPoint2010-BlankWebapplication</title> | ||
+ | </head> | ||
+ | <body> | ||
+ | |||
+ | <sp:Messages messages="${spErrors}" styleName="error"/> | ||
+ | <sp:Messages messages="${spFlash}" styleName="flash"/> | ||
+ | |||
+ | <sp:LoggedIn status="true"> | ||
+ | <sp:Table abstractTableModel="${currentUserVideoStockATM}" /> | ||
+ | </sp:LoggedIn> | ||
+ | |||
+ | <a href="<c:url value="/"/>">back</a> | ||
+ | |||
+ | </body> | ||
+ | </html> | ||
+ | </code> | ||
+ | |||
+ | =Abschlussbetrachtung= | ||
+ | In diesem Tutorial wurden die wichtigsten Punkte anhand des Videoautomatenbeispiels angesprochen. Das runterladbare Webprojekt enthält ein paar weitere Funktionalitäten wie z.B. das Zurückgeben von Videos, eine Nutzerprofilseite sowie eine Adminseite. Ein Blick in diesen Quellcode sollte bis auf die zusätzliche Nutzung von CSS nichts Neues offenbaren. |
Aktuelle Version vom 16:17, 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.
Des weiteren sehr hilfreich zu Installation der Entwicklungsumgebung ist dieser ScreenCast.
Spring Basics
Zusätzlich kommt als Webframework Spring zum Einsatz. Dieses Framework stellt eine 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) 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 annotationsbasierte Konfiguration, die in der salespoint-2010-blankweb.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.examples.videoautomat" />
<!-- 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 String C_VIDEOS = "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:
public class VideoCatalog extends CatalogImpl<CatalogItemImpl>
{
@RecoveryConstructor(parameters = {"m_sName"})
public VideoCatalog(String name) {
super(name);
}
}
Der Katalog wird im Konstruktor von VideoShop wie folgt instantiiert:
public class VideoShop extends Shop {
.
.
public VideoShop() {
.
.
try
{
addCatalog(new VideoCatalog(C_VIDEOS));
.
.
} catch(DuplicateKeyException dke){}
.
.
}
}
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 Categorizable implementiert, um 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 DoubleValue (neben IntegerValue eine der beiden Spezialisierung von NumberValue) übergeben. 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.
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 DoubleValue(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 gleich noch weitere Initialisierungsmethoden dazukommen werden und fügen einen Aufruf dessen dem Kontruktor hinzu. Wenn die diese Daten bereits in der Datenbank enthalten sind, fällt eine DuplicateKeyException, die wir in diesem Fall einfach ignorieren können.
public class VideoShop extends Shop {
.
.
public VideoShop() {
.
.
try {
.
.
initializeData();
} catch(DuplicateKeyException dke){}
}
.
.
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:
public class AutomatVideoStock extends CountingStockImpl<StockItemImpl, CatalogItemImpl>
{
@RecoveryConstructor(parameters={"m_sName", "m_ciCatalog"})
public AutomatVideoStock(String name, VideoCatalog ciRef) {
super(name, 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 String CC_VIDEOS = "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() {
.
.
try
{
.
.
addStock(new AutomatVideoStock(CC_VIDEOS, getCatalog(C_VIDEOS)));
.
.
} catch(DuplicateKeyException dke){}
}
.
.
}
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 im VideoShop können dem Videobestand die benötigten Testdaten zugefügt werden.
public class VideoShop extends Shop {
.
.
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.
public class VideoCassette extends StockItemImpl {
@RecoveryConstructor(parameters = { "m_sName" })
public VideoCassette(String key) {
super(key);
}
}
Geldkatalog
Da wir eine Bezahlfunktion einbauen wollen, brauchen wir eine Repräsentation von gültigen Geldeinheiten respektive Münzen/Scheine. Um diesen nicht bei Gebrauch ständig neu instanziieren zu müssen, legen wir ihn einfach als Katalog im Shop an.
public class VideoShop extends Shop {
.
.
public static final String C_CURRENCY = "CurrencyCatalog";
.
.
public VideoShop() {
.
.
try
{
.
.
super.addCatalog(new EUROCurrencyImpl(C_CURRENCY));
.
.
}
.
.
public void initializeData() {
.
.
initializeMoney();
}
.
.
public void initializeMoney() {
getCurrency().initCurrencyItems();
}
.
.
public static EUROCurrencyImpl getCurrency() {
return (EUROCurrencyImpl)Shop.getTheShop().getCatalog(C_CURRENCY);
}
}
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.getInstance() referenziert werden kann.
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 recover Methode(siehe Persistence Layer) wird bei der Wiederherstellung des Benutzers aufgerufen und initialisiert dort seinen VideoStock.
public class AutomatUser extends User implements Recoverable {
@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) {
super(user_ID);
setPassWd(garblePassWD(passWd));
ss_videos = new UserVideoStock(user_ID, VideoShop.getVideoCatalog());
}
public UserVideoStock getVideoStock() {
return ss_videos;
}
public void recover(Map<String, Object> data, Object recoveryContext, boolean reInit) {
if (!reInit)
ss_videos = new UserVideoStock(getName(), VideoShop.getVideoCatalog());
}
}
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:
public class UserVideoStock extends StoringStockImpl<VideoCassette, CatalogItemImpl>
{
@RecoveryConstructor(parameters={"m_sName", "m_ciCatalog"})
public UserVideoStock(String sName, VideoCatalog ciRef) {
super(sName, ciRef, VideoCassette.class);
}
}
Im Gegensatz zu Katalog und Bestand des Automaten benötigen wir hier keinen eigenen Identifikator, da der UserVideoStock stets nur beim Nutzer und nie im Shop selbst registriert wird. Deswegen überladen wir auch den String - Konstruktor anstelle des Identifikator - Konstruktors.
Weiterhin ist zu erkennen, dass beim Setzen des Passworts nicht das Passwort selbst, sondern der Rückgabewert der Methode garblePassWD(passWd) übergeben wird. Dabei handelt es sich um eine Sicherheitsmaßnahme. Damit das Passwort nicht im Klartext abgespeichert wird und ausgelesen werden kann, wird es vorher umschlüsselt. Welcher Algorithmus dabei verwendet wird, kann ebenfalls über die Klasse User gesetzt werden. Der voreingestellte Algorithmus für die Umschlüsselung ist einfach umkehrbar und daher für sicherheitsrelevante Kontexte nicht zu empfehlen. Wird das Passwort mit der Methode isPassWd(String s) überprüft, ist darauf zu achten, dass der übergebene String vorher ebenfalls umschlüsselt wird.
Die boolesche Variable wird erst zu einem späteren Zeitpunkt für die Rechtevergabe benötigt.
Zum Schluss sollen einige Testkunden und ein Administrator der Nutzerverwaltung hinzugefügt werden. Als Passwort wird der vereinfachten Testbarkeit halber jeweils eine leere Zeichenkette übergeben.
Um den verschiedene Nutzergruppen zu unterscheiden, können ihnen verschiedene Capabilities zugeordnet werden. In unserm Fall eine mit dem Namen "admin".
public class VideoShop extends Shop {
.
.
public void initializeData() {
.
.
initializeUsers();
}
.
.
public static void initializeUsers() {
try {
AutomatUser usr = new AutomatUser("Administrator", ""));
usr.setCapability(new NameCapability("admin"));
UserManager.getInstance().addUser(usr);
} catch (DuplicateUserException dke) {}
}
}
Spring MVC Roundtrip
Startseite mit Login
Nun haben wir das Model aus dem MVC-Pattern aufgebaut und können unseren ersten Controller mit entsprechender View erstellen. Zunächst soll die Startseite der Applikation erstellt werden, auf der man sich einloggen sowie nach dem Login den Videokatalog ansehen kann.
Dazu erstellen wir die Datei WebContent/jsp/index.jsp mit folgendem Inhalt:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=uft-8" />
<link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" />
<title>SalesPoint2010-Videoautomat</title>
</head>
<body>
<sp:Messages messages="${spErrors}" />
<sp:Messages messages="${spFlash}" />
<sp:LoggedIn status="false">
<sp:LoginDialog />
</sp:LoggedIn>
<sp:LoggedIn status="true">
<sp:List abstractTableModel="${videoCatalog}" />
</sp:LoggedIn>
</body>
</html>
In der JSP sehen wir neben gewöhnlichen HTML-Tags im oberen Bereich taglib-Deklarationen und im unteren Bereich dann dessen Verwendung. TagLibraries sind vorgefertigte Konstrukte, die zur Auswertung mit echtem HTML ersetzt werden. Salespoint stellt hiervon einige sehr nützliche bereit. Diese werden unter Web Erweiterung#TagLibrary detailiert beschrieben. Für uns im Moment wichtig zu wissen ist, dass der Message-Tag jeweils eine Liste von Strings HTML-formatiert ausgibt (Fehler bzw. Erfolgsnachrichten). Der LoggedIn-Tag rendert seinen Inhalt immer dann, wenn der diese Seite aufrufende Nutzer entweder eingeloggt ist oder nicht (entsprechend dem Attribut status). Der LoginDialog stellt ein LoginFormular bereit und der List-Tag listet uns ein übergebenes AbstractTableModel auf, welches eine Abstraktion von Katalogen und Beständen darstellt. Auffällig sind die Konstrukte alá "${videoCatalog}". Das sind Java-Objekte aus der sog. ModelMap, die wir im Controller befüllen.
Dazu erstellen wir eine Klasse im Package bzw. in einem Unterpaket, welches nach Springkomponenten durchsucht (siehe oben) wird.
package org.salespointframework.web.examples.videoautomat.controller;
@Controller
@Interceptors({LoginInterceptor.class})
public class Maincontroller {
@RequestMapping("/")
public ModelAndView index(ModelAndView mav) {
ATMBuilder<VideoCatalog> videoCatalog = new ATMBuilder<VideoCatalog>(VideoShop.getVideoCatalog());
mav.addObject("videoCatalog",videoCatalog.getATM());
mav.setViewName("index");
return mav;
}
}
Wir annotieren die Klasse mit @Controller, damit Spring und der DispatchServlet diese Klasse kennen und sie nach Methoden durchsuchen, die mit @RequestMapping annotiert sind. Somit weiß der DispatchServlet, dass er Anfragen auf das Wurzelverzeichnis unserer Webapplikation auf die auch als Action bezeichnete Methode MainController.index() weiterzuleiten hat. Diese wiederum erstellt bzw. befüllt das übergebene ModelAndView-Object mit den Daten, die wir in unserer JSP brauchen und gibt mit setViewName() auch die JSP an (der in der dispatch-servlet.xml angegebene viewResolver löst den String zu einem Pfad auf). Der ATMBuilder baut uns dem übergebenen VideoCatalog, den wir uns aus unserem VideoShop-Singleton holen, ein AbstractTabelModel, welches wir in das ModelAndView legen. Die zweite Annotation auf Klassenebene @Interceptors({LoginInterceptor.class}) ist notwendet für die Loginfunktionalität. Interceptor in Spring unterbrechen die Ausführung von Actions, in dem sie entweder vor oder nach der Action ausgeführt werden. Die Annotation sagt aus, dass alle Methoden im MainController vom LoginInterceptor unterbrochen werden.
Doch genug der grauen Theorie, nun ist der richtige Zeitpunkt, die Webapplikation zum ersten Mal zu starten. Es sollte ein Loginformular zu sehen sein mit dem Administrator (leeres Passwort). Nach dem Klick auf den LoginButton sollte eine Liste mit Videos zu sehen sein.
Nutzerregistrierung
Damit wir zwischen verschiedenen Nutzern unterscheiden können, brauchen diese eine Repräsentation im Model - einen Nutzeraccount. Hierzu können wir eine einfache Formularverarbeitung von Spring nutzen. Zuerst einmal fügen wir auf der Startseite einen Link auf das Registrierformular hinzu:
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
.
<sp:LoggedIn status="false">
<sp:LoginDialog />
<a href="register"><spring:message code="videoautomat.register" /></a>
</sp:LoggedIn>
.
Augenmerk hierbei liegt auf dem MessageTag aus dem Spring-Namespace. Dieser schaut in unserer MessageSource nach diesem Code und ersetzt ihn mit der richtigen Sprachversion. Wenn man von Beginn einer neuen Webapplikation dieses Mechanismus nutzt, macht es bis auf das Übersetzen der messages.properties keine Zusatzarbeit, die gesamte Applikation in anderen Sprachen anzubieten. Wir fügen also diesen Code in unsere StandardMessageSourceDatei ein:
videoautomat.register = register
Die nächste Aufgabe ist es, eine Action für diese URL zu definieren. Aus Bequemlichkeit gleich im MainController:
.
@RequestMapping(value="/register",method=RequestMethod.GET)
public ModelAndView registerForm(ModelAndView mav) {
mav.setViewName("registerForm");
return mav;
}
.
Diese Action nimmt ausschließlich GET-Anfragen auf der URL "/register" an und bestimmt ausschließlich die View(WebContent/jsp/registerForm.jsp):
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=uft-8" />
<link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" />
<title>SalesPoint2010-Videoautomat</title>
</head>
<body>
<sp:Messages messages="${spErrors}" />
<sp:Messages messages="${spFlash}" />
<form:form method="post" action="register">
<table>
<tr>
<td><spring:message code="videoautomat.userName" /></td>
<td><input type="text" name="userName" /></td>
</tr>
<tr>
<td><spring:message code="videoautomat.userPasswd" /></td>
<td><input type="password" name="userPasswd" /></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="<spring:message code="videoautomat.register" />" /></td>
</tr>
</table>
</form:form>
</body>
</html>
sowie die benutzten neuen MessageSourceCodes:
videoautomat.userName = your name
videoautomat.userPasswd = password
Das HTML-Formular wird wie üblich als POST-Anfrage abgesendet und zwar an dieselbe URL ("/register"). Wir benötigen also eine weitere Action, die nun die POST-Anfragen mit den mitgesendeten Parametern annimmt. Spring bietet dafür eine sehr einfache Variante an, in dem annotationsbasiert diese Requestparameter an Parameter der Action gebunden werden.
@Controller
@Interceptors({LoginInterceptor.class})
public class MainController {
.
.
@Autowired
private MessageSource messageSource;
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
.
.
@RequestMapping(value="/register",method=RequestMethod.POST)
public ModelAndView register(
@RequestParam("userName") String userName,
@RequestParam("userPasswd") String userPasswd,
HttpServletRequest request,
ModelAndView mav) {
if(userName.isEmpty())
MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.empty", new String[]{"user name"}, RequestContextUtils.getLocale(request)));
if(userPasswd.isEmpty())
MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.empty", new String[]{"password"}, RequestContextUtils.getLocale(request)));
if(UserManager.getInstance().getUser(userName) != null)
MessagesUtil.addError(mav, messageSource.getMessage("videoautomat.userNameNotFree", new String[]{userName}, RequestContextUtils.getLocale(request)));
if(MessagesUtil.hasErrors(mav)) {
return registerForm(mav);
} else {
AutomatUser usr = new AutomatUser(userName, userPasswd);
UserManager.getInstance().addUser(usr);
MessagesUtil.addFlash(mav,
mav.setViewName("redirect:/");
return mav;
}
}
}
.
mit den neuen MessageSourceCodes:
videoautomat.empty = input field ''{0}'' should not be empty
videoautomat.userNameNotFree = user name ''{0}'' is already taken by another user
OK, nun der Reihe nach:
- MessageSource
- Die über dem Attribut definierte Annotation @Autowired gibt Spring Bescheid, dass eine unter diesem Namen instanziierte JavaKlasse über die entsprechende Setter-Methode in diese Klasse injiziert werden soll (wir erinnern uns an den in der dispatch-servlet.xml definierten beanTag mit der id="messageSource"). Diese Instanz kann auf diese Weise zwischen allen Klassen der Applikation gemeinschaftlich und entsprechend speicherschonend benutzt werden, um lokalisierte Strings aufzulösen. Um das aktuelle Locale-Object zu bekommen, gibt es einen Toolbox-Klasse von Spring, die es aus dem Request-Object, welches man sich einfach mit übergeben lassen kann, herausholt. Ein Zusatzfeature hierbei ist, dass auch für MessageSourceCodes Parameter mitgegeben und per geschweiften Klammern mit dem Index darin referenziert werden.
- MessageUtil
- Toolbox zum Hinzufügen(add)/Abfragen(has)/Löschen(clear) von Fehler-(Error) oder Erfolgs-(Flash)nachrichten. Diese werden in einer Liste von Strings unter den Schlüsseln "spErrors" bzw. "spFlash" in der ModelMap abgelegt und können unter diesen auch wieder abgefragt werden (siehe JSP).
Falls etwas schiefgegangen ist und Fehler dem ModelAndView hinzugefügt wurden, wird diesem der registerForm()-Methode übergeben, die auf der JSP das Formular sowie die Fehlermeldungen aus der MAV anzeigt. Falls keine Fehler aufgetreten sind, wird eine neue Instanz der Klasse AutomatUser erstellt, dem UserManager hingefügt sowie auf die Hauptseite "geroutet".
Diese Funktionalität sollte nun erstmal wieder getestet werden.
Ausleih- und Bezahlvorgang
Um Elemente von einem Catalog/Bestand benutzereigabengesteuert in einen anderen zu verschieben sollte stets die DoubleViewController/DoubleViewTag-Kombination verwendet werden. Der DoubleViewController stellt auf Basis seiner Initialisierung sämtliche Verschiebeaktionen bereit.
Wenn ein Nutzer Videos ausleiht, verschiebt er sie vom Bestand des Automaten in seinen eigenen Videobestand. Diese Verschiebeaktion gilt allerdings solange als temporär, bis er sie im 2.Schritt auch bezahlt hat. Dies ist also ein Einsatzgebiet vom Salespoint DataBasket. Eine Instanz vom DataBasket speichert die Information, welche Videos der Nutzer ausleihen will, und wenn er sie bezahlt hat, wird auf ihm commit() aufgerufen und damit die Verschiebeaktion persistent oder aber, falls der Nutzer sich anders entschieden hat und die Aktion abbricht, wird mit rollback() die temporäre Verschiebeaktion einfach rückgängig gemacht.
Ausleihen
Ein Link von der Hauptseite WebContent/jsp/index.jsp aus bietet den Zugang zum Ausleihvorgang:
.
<sp:LoggedIn status="true">
<sp:List abstractTableModel="${videoCatalog}" />
<a href="rent"><spring:message code="videoautomat.rent"/></a>
</sp:LoggedIn>
.
Auf die Angabe der entsprechenden MessageSourceCodes wird absofort verzichtet.
Eine neue Controller-Klasse, die vom DoubleViewController erbt, bildet die Ausgangsbasis. Fundamental wichtig hierbei ist die richtige Typisierung dieser Klasse. Macht euch bewusst, von welchem Datentyp zu welchem verschoben werden soll.
@Controller
@Scope("session")
@RequestMapping("/rent")
public class RentController extends DoubleViewController<AutomatVideoStock, UserVideoStock> {
@RequestMapping("")
public ModelAndView index(ModelAndView mav, HttpServletRequest request, HttpServletResponse response) {
initialize(
/*source*/ VideoShop.getVideoStock(),
/*destination*/ ((AutomatUser)UserManager.getInstance().getCurrentUser(request.getSession())).getVideoStock(),
/*moveStrategy*/ new CSSSStrategy() {
@Override protected StockItem createStockItem(StockItem ci) {
return new VideoCassette(ci.getName());
}
},
/*dataBasket*/ new DataBasketImpl());
mav.setViewName("rent");
mav.addObject("videoStock", new ATMBuilder<AutomatVideoStock>(getSource())
.db(getDataBasket())
.getATM());
mav.addObject("basket", new ATMBuilder<DataBasket>(getDataBasket())
.ted(new VideoCassetteDBESSTED())
.dbc(DataBasketConditionImpl.allStockItemsWithDest(getDestination()))
.getATM());
return mav;
}
}
Nun wieder der Reihe nach die Neuerungen:
- @Scope("session")
- Diese Annotation besagt, dass für jeden Nutzer eine neue Instanz dieses Controllers angelegt wird. Dies ist notwendig, da wir hier einen sog. stateful Controller habn, der über mehrere Requests hinweg unterschiedliche Daten für einen speziellen Nutzer speichert. Controller ohne explizit angegebenem Scope werden von Spring nur einmal instanziiert und für alle Nutzer verwendet.
- @RequestMapping
- Die RequestMapping-Annotation kann auch auf Klassenebene verwendet werden und dessen Attribut wird dann als Prefix für alle Actions verwendet. Dies ist beim DoubleViewController sogar notwendet, da die index()-Action unbedingt ein leeres RequestMapping haben muss.
- initialize()
- Mit dieser Methode wird der DoubleViewController initialisiert. Dies sollte im Normalfall im Konstruktor des Controllers geschehen oder aber, wie in diesem Fall, in der index()-Action, wenn z.B. der Request nötig ist. Über den Request kommt man an das Session-Object über das ein Nutzer im UserManager angemeldet wird.
- source
- Der Quellbestand, aus dem heraus verschoben werden soll.
- dest
- Der Zielbestand, in den verschoben werden soll.
- moveStrategy
- Die richtige(!) MoveStrategy. In unserm Fall ist der Quellbestand - der AutomatVideoStock - ein CountingStock, der Zielbestand - der UserVideoStock - ein StoringStock -, also brauchen wir die CSSSStrategy aus dem Web-Package. Nicht alle dieser Strategien sind konkret, da z.b. aus einem CountingStock kein StockItem wieder herausgeholt werden kann. Deshalb muss in diesem Fall eine FactoryMethod implementiert werden, die ein frisches konkretes VideoCassette-Objekt aus einem StockItemImpl generiert.
- db
- Eine DataBasket-Instanz, in der die Verschiebeaktionen gespeichert werden, um sie im Anschluss zu committn oder zu rollbackn.
- ATMBuilder
- Den ATMBuilder haben wir bereits auf der Startseite kennengelernt. Hier wird er allerdings in einer Version genutzt, die sich FluentBuilder nennt und auf dem MethodChaining-Prinzip basiert. Methoden, die im Javadoc der Klasse als MethodChaining-Methoden ausgegeben wurden, geben nach dem Setzen des Attributes das Object selbst zurück, sodass in der selben Zeile damit weitergearbeitet werden kann.
- VideoCassetteDBESSTED
- Mit einem TableEntryDescriptor(TED) kann man bestimmen, wie sich die Elemente eines Catalogs/Stocks/DataBaskets in einer Visualisierung beschreiben sollen. Da wir hier einen DataBasket visualisieren wollen und dafür kein DefaultTED existiert, müssen wir einen angeben, der z.B. so aussieht:
public class VideoCassetteDBESSTED extends AbstractTableEntryDescriptor {
private static final String[] cNames = { "Name", "Price" };
private static final Class<?>[] cClasses = { String.class, Double.class };
public int getColumnCount() {
return cNames.length;
}
public String getColumnName(int nIdx) {
return cNames[nIdx];
}
public Class<?> getColumnClass(int nIdx) {
return cClasses[nIdx];
}
@Override
public Object getValueAt(Object oRecord, int nIdx) {
StoringStockItemDBEntry dbe = (StoringStockItemDBEntry)oRecord;
VideoCassette si = (VideoCassette)dbe.getValue();
switch (nIdx) {
case 0: return si.getName();
case 1: return new DecimalFormat("#.## €").format(((NumberValue)si.getAssociatedItem(null).getValue()).getValue());
}
return null;
}
}
Jetz haben wir 2 ATMs in der ModelMap und überführen diese, wie auf der Startseite, in der neuen WebContent/jsp/rent.jsp jeweils in einen ViewTag, die sich diesmal jedoch in einem DoubleViewTag befinden.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=uft-8" />
<link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" />
</head>
<body>
<sp:Messages messages="${spErrors}" styleName="error"/>
<sp:Messages messages="${spFlash}" styleName="flash"/>
<sp:LoggedIn status="true">
<sp:DoubleView showNumberField="true">
<sp:Table abstractTableModel="${videoStock}" />
<sp:Table abstractTableModel="${basket}" />
</sp:DoubleView>
<form:form method="get" action="pay" id="pay">
<input type="submit" value="<spring:message code="videoautomat.rent" />" />
</form:form>
<form:form method="get" action="rent/cancel" id="rent">
<input type="submit" value="<spring:message code="videoautomat.cancel" />" />
</form:form>
</sp:LoggedIn>
</body>
</html>
2 weitere Buttons fügen wir ein und bestimmen action-Attribut, wohin die Reise geht, wenn der entsprechende Submit-Button gedrückt wird. Ersterer verlinkt nach "/pay", was in der nächsten Rubrik behandelt wird. Der zweite Button ruft die URL "rent/cancel" auf. Hierfür definieren wir eine Action im RentController:
.
@RequestMapping("/rent")
public class RentController extends DoubleViewController<AutomatVideoStock, UserVideoStock> {
.
@RequestMapping("/cancel")
public ModelAndView cancel(ModelAndView mav, HttpServletRequest request) {
getDataBasket().rollback();
mav.setViewName("redirect:/");
return mav;
}
}
Wie bereits erwähnt werden die RequestMapping-Annotation-Attribute konkateniert und ergeben die richtige URL. Auf dem DataBasket wird rollback() aufgerufen und damit verfallen alle getätigten Verschiebeaktionen. Danach wird kein echter ViewName gesetzt, sondern ein Redirect ausgelöst, der den ClientBrowser auf die Startseite weiterleitet.
Bezahlen
Für das Ziel des "pay"-Buttons auf der Ausleihseite - die Bezahlseite - brauchen wir einen neuen Controller, der von DoubleViewController erbt und auf die URL "/pay" lauscht:
@Controller
@Scope("session")
@RequestMapping("/pay")
public class PayController extends DoubleViewController<Currency<CurrencyItemImpl>, MoneyBag> {
public PayController() {
initialize(
VideoShop.getCurrency(),
new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true),
new CCSStrategy(),
null);
}
@RequestMapping("")
public ModelAndView index(ModelAndView mav, HttpServletRequest request, HttpServletResponse response) {
Value pricePaid = calculatePricePaid();
Value priceToPay = calculatePriceToPay();
mav.addObject("pricePayedEnough", pricePaid.compareTo(priceToPay) >= 0);
MessagesUtil.addFlash(mav, messageSource.getMessage("videoautomat.pricePayed",
new String[{""+pricePaid,
""+priceToPay,
EUROCurrencyImpl.ABBREVIATION},
LocaleContextHolder.getLocale()));
/* videoautomat.pricePayed = {0} of {1} {2} payed */
mav.addObject("currency",new ATMBuilder<Currency<CurrencyItemImpl>>(getSource()).getATM());
mav.addObject("moneyBag", new ATMBuilder<MoneyBag>(getDestination())
.zeros(false)
.getATM());
mav.addObject("currencyExtraColumns", Collections.singletonList(new EuroCurrencyImageEC("50", null)));
mav.setViewName("pay");
return mav;
}
private Value calculatePricePaid() {
return getDestination().sumStock(getDataBasket(), new CatalogItemValue(), new DoubleValue(0)).divide(new IntegerValue(100));
}
private Value calculatePriceToPay() {
???
}
}
Ein weiterer konkreter DoubleViewController: diesmal im Konstruktor initialisiert, da wir kein Request-Objekt benötigen. Der PayController verschiebt aus einem speziellen Geldkatalog "Currency" (sp incl.) in einen speziellen CountingStock "MoneyBag" (sp incl.) und symbolisiert das Einwerfen von Geldscheinen/-münzen in den Automat.
Innerhalb der index()-Methode berechnen wir den bereits gezahlten Preis bequem über die mittels sumStock()-Methode. Den zu zahlenden Betrag allerdings lässt sich nur mit der Information was ausgeliehen werden soll berechnen. Wir benötigen also den DataBasket aus dem RentController. Hierzu bedienen wir uns wieder der @Autowired-Annotation und lassen uns den RentController automatisch injizieren. Das geht, weil dieser mit @Controller annotiert und somit von Spring instanziiert wurde.
public class PayController extends DoubleViewController<Currency<CurrencyItemImpl>, MoneyBag> {
@Autowired
private RentController rentController;
public void setRentController(RentController rentController) {
this.rentController = rentController;
}
.
.
private Value calculatePriceToPay() {
return rentController.getDataBasket().sumBasket(
DataBasketConditionImpl.allStockItemsWithDest(rentController.getDestination()),
new BasketEntryValue() {
@Override
public Value getEntryValue(DataBasketEntry dbe) {
return ((VideoCassette)dbe.getValue()).getAssociatedItem(null).getValue();
}
},
new DoubleValue(0));
}
}
Nun können wir über den DataBasket fast genauso komfortabel summieren: "fast", weil eine Mapperklasse mitgegeben werden muss, dessen einzige Aufgabe es ist, das zu summierende Value aus dem DataBasketEntry herauszuholen.
In der ModelMap stehen nun neben der FlashMessage, wieviel noch gezahlt werden muss, ein boolscher Wert "pricePayedEnough", der true wird, wenn genug gezahl wurde, sowie das ATM des Geldkatalogs und das des bereits eingezahlten Geldes. Neu hierbei sind die ExtraColumns (EC), die als Alternative bzw. Erweiterung neben den bereits bekannten TEDs benutzt werden können, um eine bzw. mehrere Zusatzspalten hinzurendern. ECs werden in einer Liste verpackt direkt dem ViewTag übergeben. Die verwendete EC benutzt die ImageTag-Klasse, die sonst aus dem JSP-Context heraus als JSP-Tag benutzt wird, beispielhaft im JavaSourceCode, um zur jeweiligen Geldeinheit das entsprechende Bild zu rendern.
public class EuroCurrencyImageEC extends AbstractExtraColumn<CurrencyItemImpl> {
private String height;
private String width;
public EuroCurrencyImageEC(String height, String width) {
super("x", null);
this.height = height;
this.width = width;
}
@Override
public String getCellContent(CurrencyItemImpl identifier) {
ImageTag tag = new ImageTag();
tag.setImage(identifier.getImage());
tag.setAlt(identifier.getName());
tag.setHeight(height);
tag.setWidth(width);
return tag.render().toString();
}
}
Die WebContent/jsp/pay.jsp könnte so aussehen:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=uft-8" />
<link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" />
<title><spring:message code="videoautomat.pay" /></title>
</head>
<body>
<sp:Messages messages="${spErrors}" />
<sp:Messages messages="${spFlash}" />
<sp:LoggedIn status="true">
<sp:DoubleView showNumberField="true" >
<sp:List abstractTableModel="${currency}" extraCols="${currencyExtraColumns}" />
<sp:Table abstractTableModel="${moneyBag}" />
</sp:DoubleView>
<form:form method="get" action="pay/success" id="success">
<c:if test="${pricePayedEnough}">
<input type="submit" value="<spring:message code="videoautomat.pay" />" />
</c:if>
<c:if test="${!pricePayedEnough}">
<input disabled="disabled" type="submit" value="<spring:message code="videoautomat.pay" />" />
</c:if>
</form:form>
<form:form method="get" action="pay/cancel" id="cancel">
<input type="submit" value="<spring:message code="videoautomat.cancel" />" />
</form:form>
</sp:LoggedIn>
</body>
</html>
Neu hierbei ist die Verwendung von Kontrollstrukturen aus der JSTL-TagLibrary (c-Namespace) mit dessen Hilfe hier der boolsche Wert, ob genug bezahlt worden ist, abgeprüft wird und entsprechend der pay-Button de-/aktiviert wird.
Der Pay- sowie Cancel-Button brauchen natürlich noch entsprechende Actions im PayController:
@RequestMapping("/success")
public ModelAndView success(ModelAndView mav, HttpServletRequest request) {
/* videoautomat.rent.successful = rent was process successful, payback is {0} EUR */
MessagesUtil.addFlash(mav, messageSource.getMessage("videoautomat.rent.successful",
new Object[]{calculatePricePaid().subtract(calculatePriceToPay()),
EUROCurrencyImpl.ABBREVIATION},
RequestContextUtils.getLocale(request)));
/* add videocassettes to uservideostock */
rentController.getDataBasket().commit();
/* clean the moneybag by setting fresh instance */
setDestination(new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true));
AutomatUser currentUser = (AutomatUser) UserManager.getInstance().getCurrentUser(request.getSession());
mav.addObject("currentUserVideoStockATM", new ATMBuilder<UserVideoStock>(currentUser.getVideoStock()).getATM());
mav.setViewName("rentSuccessful");
return mav;
}
@RequestMapping("/cancel")
public ModelAndView cancel(ModelAndView mav, HttpServletRequest request) {
setDestination(new MoneyBagImpl("moneyPayed", VideoShop.getCurrency(), true));
return rentController.cancel(mav, request);
}
Die success-Methode bestätigt den erfolgreichen Ausleihvorgang inkl. Rückgeldbetrag mit einer Erfolgsnachricht und committet den DataBasket vom RentController (verschiebt die VideoCassette-Instanzen persistent vom AutomatVideoStock zum UserVideoStock). Achtung: Außerdem setzt sie eine neue MoneyBag-Instanz - das ist wichtig, denn ein Controller mit Session-Scope bleibt erhalten bis die Sission endet(ServerEinstellung), d.h. beim eventl. 2.Ausleihvorgang während der gleichen Session wäre sonst der Bezahlbetrag vom 1.Ausleihvorgang noch enthalten! Abschließend soll in der JSP der UserVideoStock angezeigt werden, also holen wir uns den User der aktuellen Session und fügen ein mittels ATMBuilder erstelltes ATM von seinem UserVideoStock der ModelMap hinzug.
WebContent/jsp/rentSuccessful.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.salespoint-framework.org/web/taglib" prefix="sp" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=uft-8" />
<link rel="stylesheet" type="text/css" href="<c:url value="/static/resources/css/style.css" />" />
<title>SalesPoint2010-BlankWebapplication</title>
</head>
<body>
<sp:Messages messages="${spErrors}" styleName="error"/>
<sp:Messages messages="${spFlash}" styleName="flash"/>
<sp:LoggedIn status="true">
<sp:Table abstractTableModel="${currentUserVideoStockATM}" />
</sp:LoggedIn>
<a href="<c:url value="/"/>">back</a>
</body>
</html>
Abschlussbetrachtung
In diesem Tutorial wurden die wichtigsten Punkte anhand des Videoautomatenbeispiels angesprochen. Das runterladbare Webprojekt enthält ein paar weitere Funktionalitäten wie z.B. das Zurückgeben von Videos, eine Nutzerprofilseite sowie eine Adminseite. Ein Blick in diesen Quellcode sollte bis auf die zusätzliche Nutzung von CSS nichts Neues offenbaren.