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
ValueStack/OGNL
Zeichensatz-Magie
Config Browser Plugin
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 aus dem JSF-Beispiel auf mehrere Klassen aufteilen. Dies führt meiner Meinung nach 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 SessionBean-Zugriff für Subklassen ermöglicht.
EJB-Injection ist leider im Struts-Framework nicht von Haus aus möglich. Wenn gewünscht, müsste sie handgebaut werden. Für einen Vorschlag dazu:
http://blogs.cuetech.eu/roller/psartini/entry/in_struts2_auf_ejb3_session.
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" var="kuchenlisteAction"></s:action>
Dies ruft execute
der Action "kuchenliste" aus, die in struts.xml so definiert ist:
<action name="kuchenliste" class="de.fhw.komponentenarchitekturen.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 (über das Attribut "var"), 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="#kuchenlisteAction.kuchenListe">
...
</s:iterator>
Der Ausdruck "#kuchenliste" ist übrigens kein Ausdruck der Unified EL (aus JSP2.1), sondern ein OGNL-Ausdruck, der auf eine vorher definierte Variable zugreift
(siehe Abschnitt ValueStack/OGNL).
Falls dieser Ausdruck als EL-Ausdruck interpretiert wird und Probleme bereitet, hier ein Workaround: http://struts.apache.org/2.0.14/docs/ognl.html#OGNL-JSP2.1.
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.komponentenarchitekturen.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.komponentenarchitekturen.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.komponentenarchitekturen.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.komponentenarchitekturen.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.komponentenarchitekturen.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. Dies kann durch eine Konfigurationsdatei oder durch Annotations eingestellt werden.
Validation per Konfigurationsdatei:
Es wird im Package der Action (also in meinen Beispielen im Verzeichnis "de\fhw\komponentenarchitekturen\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.1.6\docs\xwork-apidocs\index.html", Package "com.opensymphony.xwork2.validator.validators"
Die Fehlermeldungen werden bei den betroffenen Feldern ausgegeben.
Validation per Annotation:
http://struts.apache.org/2.1.6/docs/validations-annotation.html
Auf den Settern der einzelnen Felder kann man die Validierungsregeln definieren. In meinem Beispiel gestaltet sich das komplizierter, da der Kuchename über
KuchenSaveAction.getKuchen().setName(...)
setzbar ist, es gibt also keinen Setter in der Action-Klasse.
Deshalb werden die Validierungsregeln auf der execute
-Methode definiert:
import com.opensymphony.xwork2.validator.annotations.*;
public class KuchenSaveAction extends BaseAction
{
@Validations
(
requiredStrings =
{
@RequiredStringValidator(type = ValidatorType.SIMPLE, fieldName = "kuchen.name", message = "Kuchenname muss angegeben werden!")
},
stringLengthFields =
{
@StringLengthFieldValidator(type = ValidatorType.SIMPLE, fieldName="kuchen.name", minLength = "5", trim = true, message = "Kuchenname muss 5 oder mehr Zeichen lang sein!")
}
)
public String execute() throws Exception
{
}
}
Das Attribut "fieldName" gibt an, auf welches Feld der Action sich die Validierung bezieht (im Beispiel also die Property "name" der Property "kuchen"). Wäre die Regel direkt
auf einem Setter definiert, wäre diese Angabe nicht nötig. Der "type" ist hier nicht der Default "FIELD", da wir den Validator nicht auf einem Feld/Setter definiert haben.
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.
Alle Infos zum "s:url"-Tag:
http://struts.apache.org/2.1.6/docs/url.html
ValueStack/OGNL
Siehe: http://cwiki.apache.org/WW/ognl-basics.html
Und die Homepage des OGNL-Projekts: http://www.ognl.org
ValueStack
Der ValueStack besteht aus vier Ebenen. Beim Zugriff auf den Stack erfolgt die Suche von oben nach unten,
bis ein Objekt mit einer passenden Property gefunden wird.
- Oberste Ebene: temporäre Objekte, z.B. das aktuelle Element beim Iterieren über eine Collection.
- Zweite Ebene: Modellobjekte (Actions, die das Interface "ModelDriven" implementieren, in meinen Beispielen nicht genutzt.
- Dritte Ebene: die aktuell aufgerufene Action.
- Unterste Ebene: Benannte Objekte ("Named Objects"). Jedem Objekt kann ein Name gegeben werden, wodurch es zu einem "Benannten Objekt" wird.
Es gibt einige Standard-Objekte wie "#application", "#session", "#request", "#attr", "#parameters"
Der Zugriff darauf erfolgt z.B. so:
- Auf einen Request-Parameter "foo":
#parameters['foo']
oder #parameters.foo
- Auf ein Request-Attribut "foo":
#request['foo']
oder #request.foo
- Auf ein Session-Attribut "foo":
#session['foo']
oder #session.foo
- Auf das erste passende Attribut, ausgehend vom PageContext über Request und Session bis zum ApplicationContext:
#attr['foo']
oder #attr.foo
Temporäre Objekte und Benannte Variablen
Bei einem <s:iterator>
-Tag wird das aktuelle Item auf den valueStack gepackt.
Beispiel aus "kuchenliste.jsp":
<s:iterator value="#kuchenlisteAction.kuchenListe">
Hier wird die aktuelle KuchenBean auf den ValueStack in die Ebene der temporären Objekte gepackt.
Der Zugriff auf die ID des aktuellen Kuchens erfolgt implizit (also Suchen des obersten Objekts mit einer Mehode getId()
auf dem ValueStack) so:
<s:property value="id"/>
Alternativ könnte man dem Objekt eine ID geben (dadurch wird es auf die Variablen-Ebene geschoben):
<s:iterator value="#kuchenlisteAction.kuchenListe" var="kuchenAktuell">
Dadurch könnte man den aktuellen Kuchen wie bisher implizit verwenden (wobei hier zu beachten wäre, dass benannte Objekte weiter unten im Stack stehen und deshalb
z.B. eine Action mit einer Property "id" sich in den Weg stellen könnte), oder aber explizit:
<s:property value="#kuchenAktuell.id"/>
Unerklärlich ist mir bisher: der in kuchenliste.jsp aufgerufene Action "kuchenliste" gebe ich eine ID, damit sie als Variable zugreifbar ist. Aber sie
ist nicht über Standard-ValueStack-Aufrufe ansteuerbar.
Hier die Doku zum <s:action>:
-Tag: http://struts.apache.org/2.1.6/docs/action.html
Expliziter Ebenzugriff
Auf "kuchendetail.jsp" könnte man sich ein Problem basteln (im aktuellen Code stellt es sich nicht):
- Die Action (z.B. "KuchenEditAction") enthält einen Getter für das Feld "id" (hier müsste zum bereits vorhandenen Setter noch ein Getter gebaut werden)
- Beim Aufbau der Zutatenliste (
<s:iterate>
-Tag) wird die aktuelle Zutat auf den ValueStack geschrieben und enthält ebenfalls ein Attribut "id"
(ID der Zutat).
Angenommen, man möchte auf das Attribut "id" der "KuchenEditAction" zugreifen. Beim Defaultverhalten würde die Property "id" schon auf dem temporären Objekt, also der
aktuellen Schleifenvariablen, gefunden, statt in der "KuchenEditAction". OGNL bietet die Möglichkeit, mit einer Array-Syntax
eine Ebene im ValueStack anzuspringen: "[0].property" etc, wobei Index 0 das oberste Objekt ist, "[1]" greift auf eine Ebene tiefer zu etc.
In meinem Beispiel liegt zuoberst im ValueStack das temporäre Objekt der Zutat, darunter die KuchenEditAction.
Man muss also beim Aufbau der Zutatentabelle auf Ebene 1 zugreifen, um "getId" der "KuchenEditAction" abzurufen:
<s:property value="[1].id"/>
Mit dieser Syntax könnte kann man z.B. bei geschachtelten <s:iterate>
-Tags auf Properties äußerer Objekte zugreifen.
Besonderheit: %{...}
(z.B. bei URLs):
In manchen Tags werden Attributwerte per Default nicht als OGNL-Ausdruck ausgewertet. Ein Beispiel ist <s:url>
: hier
würde der Wert des Attributs "href" direkt in die Ergebnisseite gerendert.
Deshalb kann hier eine OGNL-Validierung eines Attributs durch diese Struts-spezifische Erweiterung erzwungen werden:
<s:url var="editUrl" includeParams="none" action="kuchenedit">
<s:param name="id" value="id"></s:param>
</s:url>
<s:a href="%{editUrl}">Bearbeiten</s:a>
Die Klammerung %{...}
kann man übrigens auch für alle Attribute verwenden, die sowieso als OGNL-Ausdruck interpretiert
werden. In diesen Fällen entfernt Struts2 den Zusatz automatisch.
Anmerkung: die so erzeugte URL könnte man in anderen Tags so ausgeben (es wird auf eine Variable namens "editUrl" zugegriffen):
<s:property value="#editUrl"/>
Oder auch (völliges Vertrauen in den ValueStack):
<s:property value="editUrl"/>
JSP EL und der ValueStack:
Die JSP-Expression Language bietet die Möglichkeit, sogenannte ELResolver zu deklarieren,
die in EL-Ausdrücken Variablen auflösen. Struts2 bietet einen solchen, um Variablen auf dem ValueStack zu finden, und dadurch ist es möglich, mittels JSP-EL-Ausdrücken
auf z.B. Struts-Actions zuzugreifen. Ich könnte das <s:if>
-Tag aus "kuchendetail.jsp" (das auf eine positive ID in der Action prüft) ersetzen durch:
<c:if test="${kuchen.id != 0}">
...
</c:if>
Quelle: http://www.nabble.com/S2.1---struts-tags-vs-jstl-expression-language-td21507296.html
Alle Struts2-Tags verbieten übrigens über ihre TLD die Verwendung von JSP-EL (aufgrund von Sicherheitsproblemen im Zusammenspiel von JSP-EL und OGNL)
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 HTML 4.01 Transitional//EN">
<html>
<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>
Config Browser Plugin
Struts 2 bietet einen Plugin, um sich die Config der aktuellen Webanwendung anzuschauen.
Siehe: http://struts.apache.org/2.x/docs/config-browser-plugin.html
Um ihn zu aktivieren: die Datei "struts2-config-browser-plugin-2.1.6.jar" in "WEB-INF\lib" kopieren (danach in Eclipse "Refresh" auf das Projekt wählen).
Jetzt ist der Config Browser unter dieser URL aufrufbar: http://localhost:8080/KuchenZutatStrutsWeb/config-browser/index.action
Er zeigt z.B. unter "Constants" die Parameter der aktuellen Anwendung, wie sie in "struts.xml" bzw. in Defaultwerten definiert sind. "Beans" löst leider eine Exception aus.
Unter "Namespaces" => "default" finden wir unsere Actions, deren URLs und die Rückgabewerte:
Stand 20.02.2009
Historie:
04.01.2009: Erstellt aus Vorjahresbeispiel. Anpassungen an JBoss 5 und Struts 2.0.14. Validators per Annotation.
20.01.2009: Abschnitt "ValueStack/OGNL"
21.01.2009: Struts 2.1.6 + Anpassungen ("id"-Attribute durch "var" ersetzt, deprecated Annotation @Validation
entfernt), Aufräumen in Webseite.
20.02.2009: Abschnitt "Config Browser Plugin"