Freitag, 25. Mai 2012


Artikel

November 2002 | Artikel

Zum ersten, zum Zweiten, ...

(Link zum Artikel: http://www.entwickler.de/jaxenter//000256)

Jetspeed-Praixprojekt: Auctioner = eBay + MyNetscape

Text: von Stefan Kuhn und Fabian Theis
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Für Open Source-basierte Portal-Projekte bietet sich das Framework Jetspeed von Apache als leistungsfähige Umgebung an. Wer schon einmal im Web danach gesucht hat, weiß, das anspruchsvollere Jetspeed-Beispiele mit Sourcecode im Web rar sind. Deshalb wollen wir, nachdem wir in früheren Ausgaben des Java Magazins das Framework ausführlich vorgestellt haben, uns hier in die Praxis stürzen und Ihnen ein komplexeres Beispiel einer Portlet-basierten Auktions-Site à la eBay demonstrieren.

Dazu wird ein Online-Auktionsportal namens Auctioner vergleichbar Ebay erstellt, das Funktionen für Produktübersicht, Produkteintragung und Bieten umfasst. Diese Features sollen allesamt in Portlets verpackt werden, so dass sich Benutzer ganz nach Belieben ihre Weboberfläche innerhalb unseres Auctioners zusammenstellen können. Abschließend werden wir neben der reinen HTML-Ausgabe auch ein WML-Portlet erstellen, mit dem man vom Handy aus Produkte ersteigern kann.
Dieser Artikel basiert auf einem komplexeren Jetspeed- und Turbine-Beispiel, das im Rahmen des Buches Web-Applikationen und Portale mit Apache-Frameworks [Portale] erstellt wurde; dort werden auch die Grundlagen zu Velocity, Turbine, Torque und Jetspeed ausführlich dargestellt.
Die Grundidee des Auctioner ist, dass die Auktionseigenschaften im Rahmen eines Portals als Webapplikation angeboten werden sollen. Die Funktionalität des Auctioners soll folgende Punkte umfassen:

  • Anlegen von neuen Auktionen
  • Betrachten verschiedener Auktionen, nach Kategorien sortiert
  • Bieten bei einer ausgewählten Auktion
  • Regelmäßige Überprüfung auf abgelaufene Auktionen; nach Ablauf eMail an Bieter und Verkäufer
Diese Features sollen allesamt in Module verpackt werden. Dazu wird ein Portlet zum Erstellen neuer Auktionen und ein weiteres, das für die Produktansicht sowie das Ersteigern benutzt werden kann. In den Abpictureungen 3, 4 und 5 kann man sehen, wie die Applikation später aussehen soll.
Die Realisierung der Portaleigenschaften geschieht mit dem Jetspeed-Framework, welches bereits in früheren Ausgaben des Java Magazins vorgestellt wurde [Jetspeed JM01] [Jetspeed JM02]. Über Jetspeed wird intern damit auch Turbine und Torque verwendet [Turbine JM]. Für detaillierte Erklärungen dieser einzelnen Frameworks und Tools sei an deren Dokumentation, obige Java Magazin Artikel und natürlich das Buch verwiesen.
Um das Beispiel nachvollziehen zu können, benötigen Sie folgendes:
  • Den Jetspeed-Quellcode (jetspeed-1.4b1-release-src.zip) [Jetspeed]
  • Das Apache Build-Tool Ant [Ant]
  • Einen J2EE-kompatiblen Servlet Container, z. B. Tomcat [Tomcat]
  • Sofern der verwendete Servlet Container diese noch nicht enthält (beispielsweise Tomcat 3.x), die Java-Bibliothek activation.jar aus dem Javabeans Activation Framework (JAF) von Sun [JAF]. Dieses muss im Klassenpfad für die Webanwendung bereitliegen.
Dummerweise haben die Jetspeed-Entwickler in der 1.4b1-Version eine nicht-öffentliche Developer-Version von Torque verwendet, die mit keiner der zugänglichen Torque Beta Versionen kompatibel ist. Daher bleibt uns nichts anderes übrig, als direkt die Source-Distribution von Jetspeed zu benutzen, und darin unser Projekt zu realisieren; diese Distribution enthält eine funktionierende Torque-Version. In späteren Versionen soll das Projektmanagement mit dem Tool Maven [Maven], ebenfalls Subprojekt des Turbine-Projektes, deutlich vereinfacht werden.
Auf Wunsch können Sie auch das erweiterte Beispiel (inklusive Produkt- und Auktionsansicht und Benutzerbewertungen) mitsamt Quellcode von der Buchhomepage [Webapps] als Source-Distribution oder als war-Archiv herunterladen, das dann nur noch in das Tomcat-Webapps-Verzeichnis hineinkopiert werden muss.
Projektaufbau: Kompilieren und Deployen
Zur Projekterstellung benutzen wir das Build-Tool Ant von Apache. Die Haupt-Build-Datei build.xml liegt im Unterverzeichnis /build. Nach eventuell gewünschten Namens- und Verzeichnisanpassungen in build/build.properties kann unter /build mit
  1. ant deploy
die Anwendung erst kompiliert und in ein in build.properties angegebenes Webapps-Verzeichnis des Servlet Containers kopiert werden.
Das Kompilieren und Deployen kann nun kurz getestet werden; anschließend sollten ein paar Designänderungen wie Einbinden eines eigenen Logos und Änderung von Kopf- und Fußzeile vorgenommen werden, um sich noch einmal kurz den Jetspeed-Aufbau vor Augen zu führen. Zusätzlich haben wir für die Beispielanwendung noch zwei HTML-Portlets mit Begrüßungstexten und ein RSS Portlet für einen dynamischen Newsfeed eingebunden, die das Portal später lebendiger aussehen lassen. Wir verzichten an dieser Stelle auf Beschreibung und Vorgehensweise zur Erstellung solcher Portlets und verweisen auf Dokumentation und ältere Jetspeed-Artikel im Java Magazin.
Die Datenbankschicht
Bevor wir die eigentlichen Auktionsportlets schreiben können, muss die Geschäftslogik erstellt werden. Zusätzlich zu dem Benutzer, der in der Tabelle TURBINE_USER von Turbine und Jetspeed verwaltet wird, benötigen wir zwei weitere Entitäten; das zugehörige Datenbankschema ist in Abpictureung 1 gezeigt:
  • Kategorie: Die verschiedenen zu ersteigernden Produkte werden in (nicht weiter hierarchisch geordnete) Kategorien eingeteilt. Eine Kategorie besteht neben einer eindeutigen ID aus einem Namen und einer Beschreibung, die im Auctioner angezeigt werden kann.
  • Auktion: Die Auktion ist offensichtlich die zentrale Entität des Auctioner. Auktionen werden vom Verkäufer angelegt mit einem festen Ablaufdatum, einem Startgebot und einem Mindestinkrement. Bieter können Gebote abgeben. Sie können um mehr als den geforderten Betrag erhöhen; dann wird das aktuelle Gebot zwar nur um das Mindestinkrement erhöht, aber sobald andere Bieter erhöhen wollen, bietet der aktuelle Bieter bis zu seinem Höchstgebot mit - in eBay wird diese Eigenschaft als Bidagent bezeichnet. Ist eine Auktion abgelaufen, so wird sie als abgelaufen deklariert, aber dennoch weiter gespeichert, da sie für Bewertungen und eine Historie immer noch benötigt wird. Eine Auktion besteht neben Produktnamen, Beschreibung und weiteren Daten für die Gebotsverwaltung aus einem Verweis auf eine Kategorie, dem Ablaufdatum der Auktion und einem Flag Abgelaufen, das bei abgelaufenen Auktionen gesetzt wird, um bei einer Abfrage nach noch nicht abgelaufenen Auktionen einen aufwändigeren Datumsvergleich zu vermeiden. Die Verknüpfung einer Auktion mit dem Benutzerschema geschieht nicht über eine ID, da diese von Turbine aus standardmäßig nicht lesbar ist, sondern über den ebenfalls eindeutigen Loginnamen des Benutzers. Wir haben in der Auktion noch ein weiteres Feld namens Bild eingefügt, in dem Binärdaten wie beispielsweise ein Bild des Produktes gespeichert und angezeigt werden kann, was wir im Folgenden aber nicht verwenden werden.
Zur Modellierung und für den Datenbankzugriff benutzen wir das in Turbine und damit in Jetspeed enthaltene Tool Torque. Torque liest alle Datenbankinformationen aus einem sogenannten XML-Datenbankschema aus, erstellt zugehörige Objektmodell (OM)- und Peer-Klassen, die Zeileneinträge beziehungsweise Tabellen der Datenbank repräsentieren.
Nachdem Torque für uns ein Grundgerüst an Objektmodell erstellt hat, ist es an uns, dieses mit Leben zu füllen entsprechend den obigen Anforderungen an ein Auktionshaus. Dazu werden Methoden zu den leeren Entitätsklassen AuctionerUser, Auktion und Kategorie hinzugefügt; hierbei ist zu beachten, dass bei erneutem Torqueaufruf diese nicht mehr erzeugt werden, es werden ausschließlich die Base*-Klassen überschrieben. Auch werden wir einige Funktionen zu den entsprechenden Peer-Klassen hinzufügen, in denen statische Methoden zum Auffinden, Persistieren und Löschen der Objekte aus der Datenbank enthalten sind.
Die neu hinzuzufügenden Methoden sind in Abpictureung 2 im Objektmodell gezeigt. Da in diesem Artikel der Hauptaugenmerk der Portleterstellung selbst liegt, werden wir die einzelnen Methoden hier nicht weiter erläutern - hierfür sei an deren Javadoc-Dokumentation verwiesen. Alternativ hätten wir übrigens die gesamte Funktionalität natürlich auch mit EJBs modellieren können.
Das erste Auktionsportlet
Nachdem das Geschäftsmodell steht, können wir passende Views, in unserem Fall Portlets, erstellen. Für die Programmierung des Auctioners haben wir uns für die Verwendung von Velocity-Portlets mit Action-Klassen entschieden. Velocity ist eine scriptbasierte Template Engine aus dem Jakarta Projekt [Velocity JM]. VelocityPortlets unterstützen den Programmierer bei der Anwendung des Model-View-Controlling Musters; sie benutzen Velocity (und intern Turbine) zur View-Erstellung. Die Portlets des Projekts bestehen also jeweils aus einem oder mehreren Velocity-Templates und einer Java-Action-Klasse, die die Controller-Funktionalität übernimmt. Das Modell wurde oben bereits erstellt.
Unser erstes Portlet soll dem Anlegen neuer Auktionen durch die Benutzer dienen. Dazu registrieren wir das Portlet in einer neu anzulegenden Datei local-portlets.xreg unter webapp/WEB-INF/conf (Listing 1).
Listing 1: local-portlets.xreg (nach Anlegen eines VelocityPortlets)
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <registry>
  3. <portlet-entry name="NewAuction" hidden="false" type="ref"
  4. parent="Velocity" application="false">
  5. <meta-info>
  6. <title>Neue Auktion anlegen</title>
  7. <description>
  8. Portlet, um neue Auktionen anzulegen
  9. </description>
  10. </meta-info>
  11. <parameter name="template" value="auctionnew-main"
  12. hidden="true"/>
  13. <parameter name="action"
  14. value="portlets.NewAuctionPortletAction"
  15. hidden="true"/>
  16. <parameter name="lieblingskategorie" value=""
  17. hidden="false"/>
  18. <media-type ref="html"/>
  19. </portlet-entry>
  20. </registry>
Das Portlet ist ein Velocity-Portlet, deshalb ist es mit
  1. type="ref" parent="Velocity"
registriert. Dem Portlet wird ein Velocity-Template (auctionnew-main.vm) zugeordnet und eine Java-Klasse (portlets.NewAuctionPortletAction). Der tatsächlich Klassenpfad des Portlets ist allerdings de/instantsolutions/auctioner/modules/actions/portlets/NewAuctionPortletAction. Um zu erreichen, dass die Klasse gefunden wird, erweitern wir in der Datei TurbineResources.properties den Pfad für die Action-Klassen folgendermassen:
  1. module.packages=de.instantsolutions.auctioner.modules,
  2. org.apache.jetspeed.modules
Wie in Turbine üblich, wird durch diese Erweiterung des Modulpfades erreicht, dass Action-Klassen in beiden Klassenpfaden gefunden werden. Natürlich liegen Actions dann in dem Unterpackage actions, und sinnigerweise legen wir Portlet-Actions wiederum unter actions.portlets ab.
Das Haupt-Velocity-Template zum Auktionen-Anlegen, auctionnew-main.vm, das im von Jetspeed eingestellten Portlet-Template-Pfad webapp/WEB-INF/templates/vm/portlets/html liegen muss, ist in Listing 2 gezeigt. Zum größeren Teil handelt es sich dabei um einfachen HTML-Code. Interessant sind folgende Punkte: Die Liste der Auktionen wird über eine Schleife dynamisch gebaut. Die Herkunft dieser Werte folgt weiter unten. Datei wird die Kategorie ausgewählt, die der Benutzer als seine Lieblingskategorie gekennzeichnet hat. Gespeichert ist sie als Paramter in der Portlet-Registry:
  1. <parametername="lieblingskategorie" value="" hidden="false"/>
Da hidden=false gilt, ist der Wert benutzerveränderbar. Das Formular verweist mit
  1. name="eventSubmit_doAddnew"
auf die Aktion doAddnew(RunData, Context) der zugehörigen Java-Klasse (Listing 3).
Listing 2: auctionnew-main.vm zum Anlegen einer neuen Auktion
  1. <form action="$jlink" method="post">
  2. Produktname: <input name="produktName" size="20"/> <br/>
  3. Beschreibung: <input name="beschreibung" size="20"/> <br/>
  4. Kategorie:
  5. <select name="kategorieId" size="1">
  6. #foreach ($kategorie in $auctioner.kategorien)
  7. <option value="$kategorie.id"
  8. #if ($kategorie.name == $lieblingskategorie) selected #end
  9. >$kategorie.name</option>
  10. #end
  11. </select> <br/>
  12. Ablaufdatum:
  13. <input name="ablaufday" size="10" maxlength="10"
  14. value="$auctioner.tomorrow"/> (dd.mm.yyyy)
  15. <input name="ablauftime" size="5" maxlength="5"
  16. value="12.00"/> (hh.mm) <br/>
  17. Startgebot: <input name="gebot" value="1" size="20"/> <br/>
  18. Inkrement: <input name="inkrement" value="1" size="20"/><br/>
  19. <input type="submit" name="eventSubmit_doAddnew"
  20. value="Anlegen"/>
  21. </form>
Listing 3: NewAuctionPortletAction zum Anlegen einer neuen Auktion
  1. package
  2. de.instantsolutions.auctioner.modules.actions.portlets;
  3. import org.apache.jetspeed.modules.
  4. actions.portlets.VelocityPortletAction;
  5. import org.apache.jetspeed.portal.portlets.VelocityPortlet;
  6. import org.apache.turbine.util.RunData;
  7. import org.apache.velocity.context.Context;
  8. import de.instantsolutions.auctioner.om.Auktion;
  9. public class NewAuctionPortletAction
  10. extends VelocityPortletAction {
  11. private static final String LIEBLINGSKATEGORIE_KEY =
  12. "lieblingskategorie";
  13. protected void buildNormalContext(VelocityPortlet portlet,
  14. Context context, RunData data) {
  15. context.put(
  16. LIEBLINGSKATEGORIE_KEY,
  17. portlet.getPortletConfig().
  18. getInitParameter(LIEBLINGSKATEGORIE_KEY));
  19. }
  20. public void doMain(RunData data, Context context) {
  21. setTemplate(data, "auctionnew-main.vm");
  22. }
  23. public void doAddnew(RunData data, Context context)
  24. throws Exception {
  25. Auktion auktion = new Auktion();
  26. // klappt wegen alter Turbine2.2 beta noch nicht :(
  27. // data.getParameters().setProperties(auktion);
  28. auktion.setProduktName(data.getParameters().
  29. getString("produktName"));
  30. auktion.setBeschreibung(data.getParameters().
  31. getString("beschreibung"));
  32. auktion.setGebot(data.getParameters().getInt("gebot"));
  33. auktion.setInkrement(data.getParameters().
  34. getInt("inkrement"));
  35. auktion.setKategorieId(data.getParameters().
  36. getString("kategorieId"));
  37. auktion.setAblaufdatumstring(
  38. data.getParameters().getString("ablaufday"),
  39. data.getParameters().getString("ablauftime"));
  40. auktion.setVerkaeuferLogin(data.getUser().getUserName());
  41. auktion.save();
  42. setTemplate(data, "auctionnew-done.vm");
  43. }
  44. }
Zuerst beschäftigen wir uns mit der doAddnew-Methode. Diese entspricht dem Buttonnamen des Formulars, nach dem Abschicken wird also diese Methode durchgeführt [Turbine JM]. Der Code der Methode ist recht einfach: Es wird ein neues Auktion-Object angelegt und ihm die Werte zugewiesen, die aus dem Formular gelesen werden - dies könnte man ganz einfach durch data.getParameters().getString() erledigen, aber der Turbine-Torque-Versionskonflikt in Jetspeed 1.4b1 wirft beim Setzen der Kategorie einen Fehler, der sich nur so vermeiden lässt - , dann als Verkäufer der eingeloggte Benutzer genommen und schließlich das Objekt abgespeichert.
Am Ende der doAddnew-Methode wird ein neues Template namens auctionnew-done.vm gesetzt, das sehr einfach gehalten ist:
  1. <font color="$!{skin.Color}">
  2. Neue Auktion angelegt!
  3. <form action="$jlink" method="post">
  4. <input type="submit" name="eventSubmit_doMain"
  5. value="Weiter" />
  6. </form>
  7. </font>
Es gibt eine Erfolgsmeldung aus und stellt einen Link zur Verfügung. Durch diesen wird die Methode doMain der NewAuctionPortletAction aufgerufen; das einzige, was diese Methode macht, ist wieder das ursprüngliche Template auszuwählen, sodass die nächste Auktion angelegt werden kann.
Durch diese einfach gehaltene Template-Action-Interaktion wird ohne großen Aufwand die Anzeige von HTML und die Bearbeitung von Anfragen realisiert, was dem Pull-Modell aus Turbine entsprecht, nur ohne (vom Programmierer erstellbare) Screen-Klassen. Man kann in diesem vorhandenen Gerüst unmittelbar den eigentlichen Code schreiben. Dies wird durch Turbine noch zusätzlich erleichtert, da Torque einen einfachen Datenbankzugriff erlaubt - zum Speichern eines Objektes in der Datenbank genügt es, dessen Attribute zu setzen und die save()-Methode aufzurufen. Außerdem hat Turbine die gesamte Benutzerverwaltung durchgeführt - wir haben keinen Code für das Login schreiben müssen und können einfach den eingeloggten Benutzer abfragen.
Bevor wir dieses Portlet erfolgreich testen können, muss noch erklärt werden, woher die Variable $auctioner im Template aus Listing 1 kommt.
Das auctioner-Tool
Im obigen Velocity-Template wurde aus allen Kategorien eine Dropdownliste erstellt. Hierzu benötigt man eine java.util.List, die die Kategorien enthält und über die mit dem Velocity-Kommando #foreach (... in ...) iteriert werden kann. Natürlich könnte man in der Action-Klasse dieses Objekt anlegen und in den Context schreiben. Da wir ähnliche Funktionen jedoch noch in anderen Portlets benötigen, wollen wir sie an einer zentralen Stelle anlegen. Wir benutzen hierzu das Pull-Tool-Konzept von Turbine, das es ermöglicht, Klassen in der TurbineResources.properties zu registrieren, die dann in allen Contexts zu Verfügung stehen.
Wir wollen ein Request-Tool schreiben, daher muss das Interface ApplicationTool implementiert werden; wir erinnern uns, dass Request-Tools bei Init dann das jeweilige RunData-Objekt des Requests von Turbine gesetzt bekommen, sodass auch auf User- und Formulardaten zugegriffen werden kann. Die Klasse de.instantsolutions.auctioner.tools.AuctionerTool muss dafür das Interface ApplicationTool implementieren; die Methode
  1. public List getKategorien() throws TorqueException {
  2. return KategoriePeer.getKategorien();
  3. }
liefert dann eine Liste aller Kategorien zurück, indem sie auf die entsprechende OM-Methode zurückgreift. Weitere Methoden liefern den aktuellen AuctionerUser zurück - $data.user gibt ja den JetspeedUser zurück. Außerdem enthält das Tool zwei Methoden, die den heutigen und den morgigen Tag entsprechend formatiert zurückgeben.
In den TurbineResources.properties registrieren wir diese Klasse als Pull-Tool mit Namen auctioner durch
  1. tool.request.auctioner=
  2. de.instantsolutions.auctioner.tools.AuctionerTool
Unter der Bezeichnung $auctioner steht sie dann in jedem Velocity-Template zur Verfügung. Der Methodenaufruf im NewAuction-Portlet kann mit $auctioner.kategorien erfolgen, da in Velocity die Vorsilbe get zusammen mit den nachfolgenden Klammern () weggelassen werden kann - Attributzugriff anstelle eines JavaBean-Get-Zugriffes.
Damit ist das erste Portlet funktionsfähig. Nach dem Erstellen des .war-Files und dem Deployment können sie sich als Benutzer einloggen und das Portlet hinzufügen (bei den Standardbenutzern ist ja das Portlet noch nicht auf der Seite enthalten). Das Anlegen von Auktionen ist damit jetzt möglich.
Portlet zur Auktionsteilnahme
Natürlich wollen die Benutzer bei Auktionen auch bieten; dem dient das nächste Portlet. Das Schema bei der Erstellung ist das gleiche wie oben. Zuerst tragen wir ein VelocityPortlet mit Namen AllAuctions, Actionklasse portlets.AllAuctionsPortletAction und Haupttemplate auctionall-main in die local-portlets.xreg ein. Dieses Template ist in Listing 4 gezeigt.
Listing 4: auctionall-main.vm zum Ansehen von Auktionen
  1. #if ($auktionen)
  2. Produkte:
  3. #foreach ($auktion in $auktionen)
  4. $auktion.produktName
  5. $auktion.ablaufdatumformatted
  6. $auktion.gebot €
  7. <a href="$jlink.addPathInfo(
  8. "eventSubmit_doDetail","true").
  9. addPathInfo("auktionid", $auktion.id)">
  10. Details
  11. </a> <br/>
  12. #end
  13. #else
  14. Kategorie auswählen: <br/>
  15. #foreach ($kategorie in $auctioner.kategorien)
  16. <a href="$jlink.addPathInfo(
  17. "eventSubmit_doSetkategorie","true").
  18. addPathInfo("kategorie", $kategorie.id)">
  19. $kategorie.name
  20. </a>$kategorie.beschreibung<br/>
  21. #end
  22. #end
Hier werden zwei Fälle unterschieden: entweder wurde noch keine Kategorie ausgewählt oder eben doch. Im ersten Fall werden die verfügbaren Kategorien angezeigt. Hierfür dient wieder das auctioner-Tool, das ja auch hier im Context zu finden ist. Dessen getKategorien()-Methode selektiert per Torque alle verfügbaren Kategorien. Jede Kategorie wird als Link angezeigt, welcher mit dem Jetspeed-werkzeug JetspeedLink erstellt wird. Dieser Link enthält zwei Parameter, die Id der Kategorie und ein Methode aus der Action-Klasse, die wir weiter unten betrachten.
Falls bereits eine Kategorie ausgewählt wurde, wird entweder angezeigt, dass die Kategorie keine Auktionen enthält oder aber eine Liste der Auktionen der Kategorie. Bei dieser Liste werden die Auktionen wieder per JetspeedLink als Links gestaltet. Hier wird die AuktionId und eine Methode der Action-Klasse angegeben. Diese Methode sieht so aus:
  1. public void doDetail(RunData data, Context context)
  2. throws Exception {
  3. if (data.getParameters().containsKey(AUKTIONID_KEY)) {
  4. auktion = AuktionPeer.retrieveByPK(
  5. new NumberKey(data.getParameters().
  6. getNumberKey(AUKTIONID_KEY).getBigDecimal()));
  7. auktion.checkForAbgelaufen();
  8. context.put(AUKTION_KEY, auktion);
  9. setTemplate(data, "auctionall-detail.vm");
  10. }
  11. }
Hier wird zuerst die gewählte Auktion in den Context gesetzt und dann ein neues Template gewählt. Dieses Template - auctionall-detail.vm - ist in Listing 5 gezeigt.
Wie wird nun die temporäre Variable kategorie des Benutzers gesetzt? Hierfür dient die Methode doSetkategorie() der Action-Klasse, die aufgerufen wird, wenn der Benutzer eine Kategorie ausgewählt hat. Diese setzt durch
  1. data.getUser().setTemp("kategorie",
  2. data.getParameters.getInt(KATEGORIE_KEY));
genau diese temporäre Variable in der Session.
Listing 5: auctionall-detail.vm zum Ansehen einer einzelnen Auktion und zum Bieten
  1. $auktion.produktName ($auktion.beschreibung)
  2. #if ($auktion.isabgelaufen)
  3. Die Auktion wurde bereits beendet.
  4. #else
  5. #if ($data.user.userName.equals("anon"))
  6. Zum Bieten muessen Sie sich einloggen.
  7. #else
  8. #if ($auktion.verkaeufer.loginName.equals(
  9. $data.user.userName))
  10. Sie sind der Verkäufer.
  11. #else
  12. <form action="$jlink" method="post">
  13. #if ($toosmall) Sie haben zu wenig geboten. #end
  14. #if ($wronginkrement)
  15. Sie haben nicht um das gegebene Inkrement erhöht.
  16. #end
  17. #if ($belowmax)
  18. Sie wurden von $auktion.bieter.userName
  19. sofort überboten.
  20. #end
  21. Gebot abgeben:
  22. <input name="gebot" size="10" maxlength="10"
  23. value="$possgebot"/> € <br/>
  24. <input type="hidden" name="auktionid"
  25. value="$auktion.id"/>
  26. <input type="submit" name="eventSubmit_doBid"
  27. value="Abgeben" />
  28. </form>
  29. #end
  30. #end
  31. #end
  32. <a href="$jlink.addPathInfo("eventSubmit_doMain","true")">
  33. Main
  34. </a>
Das Template auctionall-detail.vm aus Listing 5 zeigt zuerst die Details der Auktion, berücksichtigt dann einige Sonderfälle (Auktion abgelaufen, kein eingeloggter Benutzer, eigene Auktion) und zeigt das entscheidende Formular zur Abgabe eines Gebotes. Als Action ist hierbei eventSubmit_doBid definiert. Die entsprechende Methode doBid in AllAuctionsPortletAction prüft zunächst, ob das Gebot in seiner Höhe korrekt ist und ruft dann die Methode auktion.bid auf, die oben bereits fertig programmiert wurde:
  1. public void doBid(RunData data, Context context)
  2. throws Exception {
  3. Auktion auktion = AuktionPeer.retrieveByPK(
  4. new NumberKey(data.getParameters().
  5. getNumberKey(AUKTIONID_KEY).getBigDecimal()));
  6. if (data.getParameters().containsKey(GEBOT_KEY)) {
  7. int gebot = data.getParameters().getInt(GEBOT_KEY);
  8. if (gebot < auktion.getGebot() ||
  9. (gebot == auktion.getGebot() &&
  10. auktion.getBieterLogin() != null))
  11. context.put("toosmall", Boolean.TRUE);
  12. else
  13. if ((gebot-auktion.getGebot()) %
  14. auktion.getInkrement() != 0)
  15. context.put("wronginkrement", Boolean.TRUE);
  16. else
  17. if (!auktion.bid(gebot, AuctionerUserPeer.
  18. getFromUser(data.getUser())))
  19. context.put("belowmax", Boolean.TRUE);
  20. }
  21. buildAuktion(context, data, auktion);
  22. buildAuctionContext(context, data);
  23. setTemplate(data, "auctionall-detail.vm");
  24. }
Ablauf von Auktionen
Wenn eine Auktion ihr Enddatum überschritten hat, muss diese als abgeschlossen gespeichert werden. Da der Ablauf einer Auktion nicht durch eine Benutzeraktion bewirkt wird, sondern durch Zeitablauf, muss ein Timer benutzt werden. Turbine stellt hierfür den sogenannten Scheduler zur Verfügung, den wir verwenden wollen.
Nun zur Erstellung des eigentlichen Jobs, den wir AblaufCheckJob nennen wollen. Da er ein Assembler ist, muss er im Modulpfad unter scheduledjobs liegen. Diese Klasse erweitert ScheduledJob; ähnlich wie in einer Thread-Klasse findet sich der eigentliche Code in der run()-Methode:
  1. public void run( JobEntry job ) throws Exception {
  2. int anz = AuktionPeer.checkForAbgelaufen();
  3. Log.info("AblaufCheckJob: found " +
  4. Integer.toString(anz) + " abgelaufene Auktionen");
  5. }
Dieser ist hier denkbar einfach, abgesehen von einigen Logging-Aktivitäten wird nur AuktionPeer.checkForAbgelaufen() aufgerufen, welche die eigentliche Arbeit macht, sprich eventuell abgelaufene Auktionen als abgelaufen markiert und die entsprechenden Bieter und Verkäufer kontaktiert.
Als Scheduler Service in Turbine benutzen wir den nicht-persisten TurbineNonPersistentSchedulerService. Dazu ersetzen wir SchedulerService in TurbineResources.properties durch
  1. services.SchedulerService.classname=org.apache.turbine.
  2. services.schedule.TurbineNonPersistentSchedulerService
Den eigentlichen Job wählen wir dann durch
  1. scheduler.jobs=AblaufCheckJob
  2. scheduler.job.AblaufCheckJob.ID=1
  3. scheduler.job.AblaufCheckJob.SECOND=0
  4. scheduler.job.AblaufCheckJob.MINUTE=5
  5. scheduler.job.AblaufCheckJob.HOUR=-1
  6. scheduler.job.AblaufCheckJob.WEEKDAY=-1
  7. scheduler.job.AblaufCheckJob.DAY_OF_MONTH=-1
aus; das bedeutet also, dass der Job AblaufCheckJob alle fünf Minuten aufgerufen wird.
Einrichten der Anwendung
Nun haben wir zwar zwei Portlets erstellt, aber davon ist eigentlich noch nicht viel zu merken. Natürlich kann man sich als Benutzer einloggen, und dann über Anpassen von HTML diese Portlets zu den bereits angezeigten hinzufügen (Abb. 4). Dennoch bekommt ein Benutzer, der die Seite besucht, die Standard-Jetspeed-Seite. Erst als eingeloggter Benutzer kann er sich das Auktionshaus mühsam zusammenstellen. Um die ganze Sache attraktiver zu machen, muss also als erstes eine schön aussehende Seite, die alle nötigen Portlets enhält, gestaltet werden. Diese Seite kann entweder als eingeloggter Benutzer per graphischem Interface oder direkt durch editieren der PSML-Dateien gestaltet werden [Jetspeed JM02]. Diese Seite muss dann ins webapp/WEB-INF/psml/anon und webapp/WEB-INF/psml/user/turbine kopiert werden. Damit erhalten wir einen Startpictureschirm, auf dem ein Benutzer noch ohne eingeloggt zu sein, bereits Kategorien nach Auktionen durchsuchen kann (Abb. 3).
WAP-Ausgabe
Bekanntlich kann man mit Jetspeed auch ohne größere Probleme Ausgaben in verschiedenen Formaten erzeugen - hierfür müssen nur die Portlets für die gewünschten Formate registriert sein und dann entsprechende Templates bereitstehen. Abschließend wollen wir dies exemplarisch am AllAuctionsPortlet vorführen. Dazu ergänzen wir diese Portletregistrierung um das neue Ausgabeformat, fügen also in local-portlets.xreg im Portlet-Tag von AllAuctions den Eintrag
  1. <media-type ref="wml"/>
hinzu. Damit wird im Customizer dieses Portlet auch bei der WML-Anpassung angeboten. Die PortletAction, also der Controller in unserem MVC-Modell, muss nicht verändert werden - wir fügen einfach eine weitere View in Form von WML-Ausgabe hinzu. Dazu erstellen wir unter webapp/WEB-INF/templates/vm/portlets/wml zwei Dateien auctionall-main.vm und auctionall-detail.vm, deren logischer Inhalt dem der jeweiligen gleichnamigen HTML-Templates entspricht - es wird nur HTML-Syntax durch WML-Syntax [WML] ersetzt.
Die neu erstellten WML-Portlets lassen sich nun nach Einloggen (in der HTML-Maske) durch Customize-WML hinzufügen; allerdings sind die ursprünglichen WML-PSML-Dateien in der derzeitigen Jetspeedversion fehlerhaft, es muss der FlowPortletController in
  1. <controller name="FlowPortletController"/>
durch einen ColumnController, also
  1. <controller name="ColumnController"/>
ersetzt werden.
In Abpictureung 5 sind die fertigen WML-Seiten in einem WAP-Browser, hier dem WAP-Emulator des Nokia Mobile Internet Toolkit [Nokia] gezeigt. Zuerst muss man sich als Benutzer einloggen, dann erhält man eine Übersicht über alle Portlets. Wählt man das Auktionen-Portlet aus, so bekommt man die übliche Kategorienübersicht. Anschließend kann man in der Detailansicht sogar bei Auktionen über das Handy mitbieten.
Ausblick
Wir haben ein Auktionsportal erstellt, mit dem man Auktionen anlegen und bei Auktionen mit bieten kann. Nach der Modellierung der eigentlichen Geschäftslogik mit Torque wurden sukzessive die einzelnen Portlets, basierend auf dem VelocityPortlet, erstellt, zunächst mit einer HTML-View. Unter anderem haben wir dafür die Turbine Services Scheduler zur Abfrage nach abgelaufenen Auktionen und Pull-Tool zur problemlosen Bereitstellung von Daten im Velocity-Context benutzt. Schließlich haben wir angedeutet, dass als zweite View für ein Portlet eine WML-Darstellung erzeugt, so dass man auch per Handy bei Auktionen mitbieten kann.
Viele weitere Funktionen ließen sich nun einbauen. In [Webapps] haben wir zusätzliche Funktionalitäten zum Bewerten von Benutzern nach erfolgreichen Auktionen hinzugefügt. Für solche Erweiterungen sowie detailliertere Erklärungen zu Jetspeed und Turbine sei an das Buch verwiesen, das demnächst im Software & Support Verlag erscheint [Portale]. Dennoch haben wir in dieser einfach gehaltenen Anwendung gesehen, wie Turbine den Programmierer bei der Aufteilung und Entwicklung von Modell- und Präsentationsschicht unterstützt, und Jetspeed dann das ganze in einen Portalrahmen verschnürt.
Übrigens finden Sie einige der Java Magazin-Artikel, auf die in diesem Beitrag verwiesen wird, unter www.javamagazin.de.
Links und Literatur

Kommentare