Kuchen-Zutat-Beispiel mit Struts 2
Inhalt:
Unterschiede zu JSF
Action-Basisklasse
Action in JSP aufrufen
Use-Case "Neuer Kuchen"
Use-Case "Zutat bearbeiten"
Validators
Link auf Action
Zeichensatz-Magie
Dieses Beispiel verwendet exakt die gleiche EJB-Schicht wie das Kuchen-Zutat-JSF-Beispiel, nur die
Webschicht ist in Struts 2 gebaut.
Hier gibt es das Projekt als EAR-Export-Datei: KuchenZutatStruts.ear.
Unterschiede zu JSF
Der größte Unterschied zum JSF-Beispiel ist sicherlich, dass hier jede Action nur eine einzige Execute-Methode enthält. Während
in JSF bei einem Form-Submit angegeben werden kann, welche Methode der Managed Bean aufgerufen wird, ist es bei Struts 2 immer die Methode
execute
.
Dadurch müssen wir die Logik auf dem JSF-Beispiel auf mehrere Klassen aufteilen. Dies führt aber meiner Meinung nach sogar zu eher saubererem Code.
Ein weiterer Unterschied ist das Vorgehen beim Datenaustausch zwischen den Properties der Action und der Webseite.
In JSF greifen Getter und Setter auf die gleiche Property einer festgelegten Managed Bean zu.
Struts 2 sucht sich die getter/setter für Feldwerte dynamisch anhand der aktuellen Umgebung. Aus meinem Beispiel: Auf der Seite "kuchenliste.jsp"
wird über die Kuchenliste iteriert, und im Kuchen-Bearbeiten-Formular jedes Kuchens wird die Kuchen-ID als Hidden Field aus dem aktuellen
Kuchen der Liste geholt (der als Objekt namens "kuchen" zur Verfügung steht). Das Ziel des Formulars ist die KuchenEditAction
.
Diese enthält eine Property getKuchen
, und in deren Property "id" wird die Kuchen-ID geschrieben.
Action-Basisklasse
Da alle Actions auf die KuchenZutatWorkerBean
zugreifen, wurde eine Basisklasse BaseAction
gebaut, die den Bean-Zugriff für Subklassen ermöglicht.
public abstract class BaseAction extends ActionSupport
{
private KuchenZutatWorkerLocal kuchenZutatWorker = null;
protected KuchenZutatWorkerLocal getWorker() throws Exception
{
if (this.kuchenZutatWorker == null)
{
try
{
InitialContext initialContext = new InitialContext();
this.kuchenZutatWorker = (KuchenZutatWorkerLocal) initialContext.lookup ("java:comp/env/ejb/KuchenZutatWorkerLocal");;
}
catch (NamingException ex)
{
throw new Exception("JNDI-Lookup von 'java:comp/env/ejb/KuchenZutatWorkerLocal' schlug fehl mit (" + ex.getClass().toString() + "): " + ex.getMessage(), ex);
}
}
return this.kuchenZutatWorker;
}
}
Die Worker-Bean wird nur beim ersten Abrufen aus dem JNDI geholt, das heißt mehrfache Aufrufe greifen auf die bereits gefundene Instanz zu.
Der Code ist verbesserungswürdig, da er keine sinnvolle Fehlerbehandlung betreibt :-(
Action in JSP aufrufen
"kuchenliste.jsp" ist von außen direkt aufrufbar, also ohne dass vorher eine Action angesteuert wird. Da dadurch benötigte Daten nicht initialisiert sind,
können wir innerhalb der JSP einen Aufruf der nötigen Action durchführen:
<s:action name="kuchenliste" id="kuchenliste"></s:action>
Dies ruft execute
der Action "kuchenliste" aus, die in struts.xml so definiert ist:
<action name="kuchenliste" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.KuchenlisteAction">
</action>
Da diese Action innerhalb einer JSP aufgerufen wird, ist keine Result-Seite nötig. In ihrem execute
wird die Kuchenliste eingeladen.
Die Action wird mit einer ID versehen, dadurch ist ein expliziter Zugriff auf genau diese Action mittels "#"-Operator an späterer Stelle möglich (dies klappt leider nur bei Actions,
die innerhalb einer JSP deklariert wurden).
<s:iterator value="#kuchenliste.kuchenListe">
...
</s:iterator>
Use-Case "Neuer Kuchen"
- Auf der Seite "kuchenliste.jsp" wird der Button "Neuer Kuchen" geklickt, der so definiert ist:
<s:form id="formkuchenneu" action="kuchenneu">
<s:submit id="kuchenneu" value="Neuer Kuchen"></s:submit>
</s:form>
- Die zugehörige Action "kuchenneu" in "struts.xml" is so definiert:
<action name="kuchenneu" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.KuchenNeuAction">
<result>/kuchendetail.jsp</result>
</action>
KuchenNeuAction
ist eine leere Action, die nur "success" zurückgibt.
public class KuchenNeuAction extends BaseAction
{
public String execute() throws Exception
{
return Action.SUCCESS;
}
}
- Die Zielseite der Action bei Erfolg ist "kuchendetail.jsp".
Dort wird ein Eingabeformular aufgebaut.
<s:form action="kuchensave">
<s:hidden name="kuchen.id"></s:hidden>
<s:textfield name="kuchen.name" label="Name"></s:textfield> <br/>
<br/>
<s:submit value="OK"></s:submit>
</s:form>
Dieses Formular greift auf Properties "kuchen.id" und "kuchen.name" zu, die allerdings bei einem neuen Kuchen nicht existieren. Hätten wir der
KuchenNeuAction
eine Methode getKuchen
gegeben, so würden deren Properties "id" und "name" als Defaults in das Eingabefeld
und das Hidden Field geschrieben.
- Ein Klick auf "OK" führt zur Action "kuchensave", die in "struts.xml" so definiert ist:
<action name="kuchensave" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.KuchenSaveAction">
<result>/kuchenliste.jsp</result>
</action>
(in der Beispielanwendung steht hier noch mehr, dazu mehr im Abschnitt "Validator").
- Die Ziel-Action
KuchenSaveAction
wird aufgerufen beim Speichern eines neuen sowie beim Speichern eines vorhandenen Kuchens.
Sie enthält eine Property "kuchen". In diese Property werden die Felder "kuchen.id" und "kuchen.name" von
"kuchendetail.jsp" beim Submit geschrieben. In unserem Fall (Anlegen eines neuen Kuchens) ist die ID natürlich leer !
public class KuchenSaveAction extends BaseAction
{
private KuchenBean kuchen = new KuchenBean();
public String execute() throws Exception
{
KuchenBean kuchenSave = null;
if (this.kuchen.getId() != null)
{
kuchenSave = this.getWorker().findKuchenById(this.kuchen.getId() );
}
else
{
kuchenSave = new KuchenBean();
}
kuchenSave.setName(this.kuchen.getName());
this.getWorker().saveKuchen(kuchenSave);
return Action.SUCCESS;
}
public KuchenBean getKuchen()
{
return this.kuchen;
}
}
Beim Execute wird geprüft, ob die ID der Kuchen-Instanzvariable vorhanden ist. Wenn ja, dann handelt es sich um einen vorhandenen Kuchen, ansonsten um einen neuen.
Im Fall des vorhandenen Kuchens wird dieser aus der Datenbank geladen, ansonsten wird ein neuer Kuchen erzeugt. Anschließend werden die Felder
der Instanzvariable "kuchen" umkopiert. Dies ist nötig, da die Felder dieser Instanzvariable zwar von Struts mit den aktuell eingegebenen Daten befüllt werden,
aber falls der Kuchen abhängige Daten enthalten würde, dann würden diese wahrscheinlich nicht mitgeführt.
Der so befüllte Kuchen wird gespeichert.
- Die Action führt zur kuchenliste.jsp (siehe Auszug aus struts.xml weiter oben).
Use-Case "Zutat bearbeiten"
Dieser Use-Case ist komplexer, und er zeigt, wie ein Feld mit unterschiedlichen Actions zusammenspielt.
- Ausgangspunkt ist wieder kuchenliste.jsp, in der pro Kuchen ein Bearbeiten-Formular definiert ist.
<s:iterator value="#kuchenliste.kuchenListe">
...
<s:form id="formkuchenedit" action="kuchenedit">
<s:hidden name="id"></s:hidden>
<s:submit id="kuchenedit" value="Bearbeiten"></s:submit>
</s:form>
...
</s:if>
Im Iterator-Tag wird die Kuchenliste durchlaufen, d.h. das Hidden Field für die Kuchen-ID greift auf getId
des aktuellen Kuchens zu. Struts mappt
"id" automatisch auf das aktuelle Iterator-Objekt.
- Die Action "kuchenedit" ist in struts.xml so definiert:
<action name="kuchenedit" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.KuchenEditAction">
<result>/kuchendetail.jsp</result>
</action>
KuchenEditAction
definiert eine Methode "setId", in die Struts die Kuchen-ID aus dem Hidden Field "id" beim Submit schreibt:
private Integer intKuchenId = null;
public void setId(Integer int_KuchenId)
{
this.intKuchenId = int_KuchenId;
}
Außerdem wird eine Variable "kuchen" sowie zugehöriger getter definiert, die nach dem execute
die Daten des Kuchens enthalten:
private KuchenBean kuchen = null;
public KuchenBean getKuchen()
{
return this.kuchen;
}
execute
lädt den Kuchen zur Kuchen-ID ein:
public String execute() throws Exception
{
this.kuchen = this.getWorker().findKuchenById(this.intKuchenId);
return Action.SUCCESS;
}
- Weiter geht es auf "kuchendetail.jsp" (siehe Deklaration in struts.xml). Hier wird das aus dem letzten Kapitel bekannte Formular aufgebaut,
wobei diesmal ID und Name aus den Properties der
KuchenEditAction
gezaubert werden: getKuchen.getId
bzw. getKuchen.getName()
.
<s:form action="kuchensave">
<s:hidden name="kuchen.id"></s:hidden>
<s:textfield name="kuchen.name" label="Name"></s:textfield> <br/>
<br/>
<s:submit value="OK"></s:submit>
</s:form>
<s:if test="kuchen.id != null">
<table>
...
<tbody>
<s:iterator value="kuchen.zutaten">
<tr>
<td>
<s:property value="id"/>
</td>
<td>
<s:property value="zutatName"/>
</td>
<td>
<s:property value="menge"/>
</td>
<td>
<s:form id="formzutatedit" action="zutatedit">
<s:hidden name="kuchen.id"></s:hidden>
<s:hidden name="id"></s:hidden>
<s:submit id="zutatedit" value="Bearbeiten"></s:submit>
</s:form>
</td>
<td>
<s:form id="formzutatdelete" action="zutatdelete">
<s:hidden name="kuchen.id"></s:hidden>
<s:hidden name="id"></s:hidden>
<s:submit id="zutatdelete" value="Löschen"></s:submit>
</s:form>
</td>
</tr>
</s:iterator>
</tbody>
</table>
...
</s:if>
Neu hier: die Zutaten werden ausgegeben (und zwar nur, wenn die Kuchen-ID nicht NULL ist, denn in diesem Fall würde es sich um einen neuen Kuchen handeln).
Beim Aufbau der Zutatentabelle wird die Methode getKuchen().getZutaten()
der Action aufgerufen.
Beim Bearbeiten- und Löschen-Formular wird die Kuchen-ID aus getKuchen().getId()
als Hidden Field mitgeführt (die Zutat bietet ja leider keine Möglichkeit,
an die Kuchen-ID zu gelangen). Die Zutat-ID wird aus der aktuellen Schleifen-Zutat mittels getId()
geholt.
Man erkennt hoffentlich, dass es eventuell sinnvoll wäre, die ID-Felder in den Entity-Beans eindeutig zu benennen !
Diese Hidden Fields "kuchen.id" und "id" bedingen besondere Logik im nächsten Schritt !
- Die Action "zutatedit" ist in struts.xml so definiert:
<action name="zutatedit" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.ZutatEditAction">
<result>/zutatdetail.jsp</result>
</action>
ZutatEditAction
enthält drei Felder: eine Zutat-ID, eine Zutat sowie einen Kuchen.
public class ZutatEditAction extends BaseAction
{
private ZutatBean zutat = null;
private Integer intZutatId = null;
private KuchenBean kuchen = new KuchenBean();
public ZutatBean getZutat()
{
return this.zutat;
}
public void setId(Integer int_ZutatId)
{
this.intZutatId = int_ZutatId;
}
public KuchenBean getKuchen()
{
return this.kuchen;
}
}
Die Zutat wird erst beim Execute eingelesen. Beim Submit des Formulars werden setId()
sowie getKuchen().setId()
aufgerufen, um die Hidden Fields "kuchen.id" und "id" (ID der Zutat) zu setzen. Dies ist der Grund, warum hier ein ganzes Kuchen-Objekt nötig ist: es
dient einerseits als Container für die Kuchen-ID und wird deshalb direkt bei der Deklaration erzeugt. Außerdem wird es später auf "kuchendetail.jsp"
zum Befüllen der Kuchen-Felder benötigt ! Das Setzen der Kuchen-Id mit Hilfe einer Methode setKuchenId
(also ohne Umweg über das Container-Objekt) ist hier nicht möglich, denn auf "kuchendetail.jsp" müsste in den zugehörigen Actions ebenfalls
eine Property "getKuchenId
"/"setKuchenId
" verfügbar sein !
Die Automatismen von Struts 2 beim Setzen/Auswerten von Feldern bieten zwar große Möglichkeiten, erfordern aber auch viel Benennungs-Disziplin
(und man muss höllisch aufpassen).
Im execute
wird die Zutat-Membervariable geladen.
public String execute() throws Exception
{
this.zutat = this.getWorker().findZutatById(this.intZutatId);
return Action.SUCCESS;
}
Mit dem Kuchen passiert nichts.
- Weiter geht es zur Zielseite, "zutatdetail.jsp":
<s:form action="zutatsave">
<s:hidden name="kuchen.id"></s:hidden>
<s:hidden name="zutat.id"></s:hidden>
<s:textfield name="zutat.zutatName" label="Name"></s:textfield> <br/>
<s:textfield name="zutat.menge" label="Menge"></s:textfield> <br/>
<br/>
<s:submit value="OK"></s:submit>
</s:form>
Wichtig hier: Kuchen-ID und Zutat-ID kommen aus den Methoden getKuchen().getId()
sowie getZutat().getId()
der KuchenZutatEditAction
.
- Die Action "zutatsave" ist in struts.xml so deklariert:
<action name="zutatsave" class="de.fhw.swtvertiefung.knauf.kuchenzutatstruts.actions.ZutatSaveAction">
<result>/kuchendetail.jsp</result>
</action>
- ZutatSaveAction deklariert zwei Membervariablen "zutat" und "kuchen":
public class ZutatSaveAction extends BaseAction
{
private ZutatBean zutat = new ZutatBean();
private KuchenBean kuchen = new KuchenBean();
public ZutatBean getZutat()
{
return this.zutat;
}
public KuchenBean getKuchen()
{
return this.kuchen;
}
}
Beim Submit des Formulars wird die aktuelle Kuchen-ID aus dem Hidden Field in getKuchen().setId()
geschrieben, die Zutat-ID in getZutat().setId()
.
Die Felder der Zutat (ZutatName und Menge) werden in getZutat().setZutatName()
und getZutat().setMenge()
gepackt.
execute
sieht so aus:
public String execute() throws Exception
{
...
ZutatBean zutatSave = null;
if (this.zutat.getId() != null)
{
zutatSave = this.getWorker().findZutatById(this.zutat.getId() );
}
else
{
zutatSave = new ZutatBean();
}
zutatSave.setZutatName(this.zutat.getZutatName());
zutatSave.setMenge(this.zutat.getMenge());
this.kuchen = this.getWorker().findKuchenById(this.kuchen.getId());
zutatSave.setKuchen(this.kuchen);
this.getWorker().saveZutat(zutatSave);
this.kuchen = this.getWorker().findKuchenById(this.kuchen.getId());
return Action.SUCCESS;
}
Die Zutat wird anhand der ID in der aktuellen Zutat geladen, und die Felder der Zutat-Membervariablen werden übernommen. Anschließend wird der
Kuchen anhand von kuchen.getId()
geladen. Zutat und Kuchen werden verknüpft (das geschieht hier immer, auch beim Bearbeiten von vorhandenen Zutaten),
danach wird die Zutat gespeichert.
Nach dem Ende der Action geht der Workflow zu "kuchendetail.jsp", dort wird intensiv auf die Property "kuchen" zugegriffen, unter anderem zum Aufbau der Zutatenliste.
Aus diesem Grund muss der Kuchen nach dem Speichern der Zutaten neu geladen werden !
- Als Ergebnis der Action geht es zurück auf "kuchendetail.jsp". Dort werden die Daten des Kuchens aus
ZutatSaveAction.getKuchen()
geholt.
Man sieht, das es drei Wege gibt, auf "kuchendetail.jsp" zu gelangen: von KuchenNeuAction
, von KuchenSaveAction
und von ZutatSaveAction
.
In allen drei Fällen wird eine Property "kuchen" abgerufen, die in allen drei Actions vorhanden sein sollte. In KuchenNeuAction
fehlt diese Property,
weil es dort sowieso nur leere Felder zu holen gibt.
Validators
Eine automatische Validierung von Feldern ist möglich. Dazu wird im Package der Action (also in meinen Beispielen im Verzeichnis "de\fhw\swtvertiefung\knauf\kuchenzutatstruts\actions")
eine Datei "ActionKlasse-validation.xml" angelegt, in der für die einzelnen Felder die Validatoren deklariert werden.
Ich habe das nur für die KuchenSaveAction
(Speichern eines Kuchens) implementiert. Die zugehörige Konfigurationsdatei "KuchenSaveAction-validation.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0.2//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
<field name="kuchen.name">
<!-- Feld "kuchen.name" ist ein Pflichtfeld (dieser Fall wird nicht vom Length-Validator abgedeckt !) -->
<field-validator type="requiredstring">
<param name="trim">true</param>
<message>Kuchenname muss angegeben werden !</message>
</field-validator>
<field-validator type="stringlength">
<param name="minLength">5</param>
<param name="trim">true</param>
<message>Kuchenname muss 5 oder mehr Zeichen lang sein !</message>
</field-validator>
</field>
</validators>
Für das Feld "kuchen.name" (unter diesem Namen taucht es auf "kuchendetail.jsp" auf) wird ein "requiredstring"-Validator definiert, der prüft, sicherstellt, dass
es eingegeben wurde. Außerdem wird ein "stringlength"-Validator definiert, der eine Mindestlänge von 5 Zeichen erfordert,
und außerdem vor der Längenprüfung Leerzeichen abschneidet (dieser Validator prüft leider nicht das Vorhandensein von Feldern).
Im Fehlerfall taucht die konfigurierte Fehlermeldung in der JSP-Seite auf.
Eine Doku der verfügbaren Validatoren findet man im Verzeichnis "struts-2.0.11\docs\xwork-apidocs\index.html", Package "com.opensymphony.xwork2.validator.validators"
Die Fehlermeldungen werden bei den betroffenen Feldern ausgegeben.
Link auf Action
Im Navigations-Menü in "zutatdetail.jsp" wird ein "Öffne aktuellen Kuchen auf der Kuchen-Detail-Seite"-Link implementiert.
Mit JSP-Mitteln (also ohne Zuhilfenahme von Struts):
Der Link darf nicht einfach die JSP-Seite aufrufen (dann wäre die nötige Struts-Action mit den Daten des Kuchens nicht vorhanden), sondern es muss eine Action aufgerufen
werden, die die Daten für die Seite korrekt initialisiert. In meinem Beispiel ist das die Action namens "kuchenedit" (hinter der die Action-Klasse KuchenEditAction
steckt,
die den Kuchen zu einer übergebenen ID einlädt). Wichtig ist, dass der Action-Link auf ".action" endet.
<a href="kuchenedit.action?id=${kuchen.id}">Zum Kuchen</a>
Die Übergabe der ID des Kuchens erfolgt hier über einen EL-Ausdruck: "kuchen.id" greift auf die Methode getKuchen().getId()
der aktuellen ZutatEditAction
zu.
Die Ziel-Action KuchenEditAction
hat eine Methode setId()
, in die der Request-Parameter vom Struts-Framework geschrieben wird.
Mit Struts 2-Tags:
<s:url id="editUrl" action="kuchenedit">
<s:param name="id" value="kuchen.id"></s:param>
</s:url>
<s:a href="%{editUrl}">Zum Kuchen</s:a> <br />
Zuerst wird hier mit dem s:url
-Tag eine Variable namens "editUrl" definiert, die auf die Action "kuchenedit" verweist. Über
das Subtag s:param
werden die Parameter der URL definiert. In diesem Beispiel ist das ein Feld namens "id", das aus dem Wert "kuchen.id" der aktuellen
Action stammt. Beim Link-Klicken wird es in eine Property "id" der Ziel-Action geschrieben.
Genutzt wird diese URL anschließend im s:a
-Tag, wobei im "href"-Attribut eine Struts-Expression steht, die auf die so erzeugte URL verweist.
Zeichensatz-Magie
Wenn man nicht explizit etwas anderes konfiguriert, verwendet Struts als Zeichensatz für die Auswertung der Requests "UTF-8". In meinen JSPs ist allerdings per Default "ISO-8859-1" deklariert:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page session="false" language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Test für Struts</title>
</head>
<body>
Die Deklarationen lassen sich in zwei Gruppen unterteilen:
-Das Attribut "encoding" im XML-Tag sowie das Attribut "pageEncoding" in der "page"-Direktive geben an, in welchem Zeichensatz die JSP-Datei
vorliegt (also: mit welchem Zeichensatz sie gespeichert ist). Steht hier ein falscher Wert, dann werden Umlaute falsch ausgegeben, die direkt in den HTML-Fragmenten der JSP-Seite stecken.
-Das Attribut "charset" im "contentType"-Attribut der @page
-Direktive ist für den Server gedacht und gibt an, mit welchem Zeichensatz die Ausgabe erstellt werden soll.
Es wird in den Response Header gepackt. Der Browser sollte die Seite anhand dieses Encodings darstellen.
-Das HTML-Metatag meta http-equiv="Content-Type"
ist nur für den Browser gedacht, spielt auf Serverseite keine Rolle. Anhand dieses Tags "errät" der Browser,
welchen Zeichensatz er für die Darstellung verwenden soll, und schickt auch Formulareingaben in diesem Zeichensatz ab. Wenn angegeben, sollte die Zeichensatz-Deklaration
im Response Header über diesem Metatag stehen
Struts 2 in der Standardkonfiguration versucht nun diese Daten als UTF-8-Zeichen zu interpretieren und scheitert daran bei Umlauten (das führt zu nicht darstellbaren Zeichen,
wenn sie das nächste Mal zum Browser geschickt werden).
Also wird in "struts.xml" der Zeichensatz auf "ISO-8859-1" umgestellt:
<constant name="struts.i18n.encoding" value="ISO-8859-1"></constant>
Stand 27.01.2008
Historie:
30.12.2007: Erstellt
23.01.2008: Attribute "struts.devMode" und "struts.i18n.encoding" in struts.xml gesetzt und Abschnitt "Zeichensatz-Magie" zugefügt.
24.01.2008: Validators erweitert um "requiredstring"-Validator
27.01.2008: Struts-Links eingebaut, Zeichensatz-Doku erweitert.