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"



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. 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.