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"



Use-Case "Zutat bearbeiten"

Dieser Use-Case ist komplexer, und er zeigt, wie ein Feld mit unterschiedlichen Actions zusammenspielt.


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.


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):
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:
Config Browser


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"