Kuchen-Zutat-Beispiel mit Struts 7


Inhalt:

Unterschiede zu JSF
Action-Basisklasse
Action in JSP aufrufen
StrutsParameter, next level
"struts.xml": struts.allowlist.classes
Use-Case "Neuer Kuchen"
Use-Case "Zutat bearbeiten"
Validators
Link auf Action
ValueStack/OGNL
Zeichensatz-Magie
Config Browser Plugin

Für WildFly 33, JakartaEE10 und Struts 7: Dieses Beispiel verwendet exakt die gleiche EJB-Schicht wie das Kuchen-Zutat-Beispiel mit Jakarta Faces, nur die Webschicht ist in Struts 7 gebaut.

Zum Aufbau eines Struts-Projekts sei auf das StrutsBasics-Beispiel verwiesen. In diesem Beispiel wird Version 7.0.M9 verwendet.

Hier gibt es das Projekt als EAR-Export-Datei: KuchenZutatStruts.ear.


Startseite der Anwendung: http://localhost:8080/KuchenZutatStrutsWeb/


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

In Struts 2 war es möglich, eine JSP direkt aufzurufen. D.h. ich konnte die Seite "kuchenliste.jsp" direkt aufrufen, und die JSP hätte dann selbst die benötigte Action erzeugt. Eigentlich ist dies aber nicht empfohlen bzw. eventuell sogar "verboten": https://struts.apache.org/security/#never-expose-jsp-files-directly.

In Struts 7.0M9 führt dieses Konstrukt zu einem Fehler:
Caused by: java.lang.NullPointerException: Cannot invoke "com.opensymphony.xwork2.ActionInvocation.getProxy()" because "invocation" is null
	at deployment.KuchenZutatStruts.ear.KuchenZutatStrutsWeb.war//org.apache.struts2.components.Component.getNamespace(Component.java:448)
	at deployment.KuchenZutatStruts.ear.KuchenZutatStrutsWeb.war//org.apache.struts2.components.ActionComponent.executeAction(ActionComponent.java:252)
	at deployment.KuchenZutatStruts.ear.KuchenZutatStrutsWeb.war//org.apache.struts2.components.ActionComponent.end(ActionComponent.java:166)
	at deployment.KuchenZutatStruts.ear.KuchenZutatStrutsWeb.war//org.apache.struts2.views.jsp.ComponentTagSupport.doEndTag(ComponentTagSupport.java:39)
	at org.apache.jsp.kuchenliste_jsp._jspx_meth_s_005faction_005f0(kuchenliste_jsp.java:199)
	at org.apache.jsp.kuchenliste_jsp._jspService(kuchenliste_jsp.java:134)
	at io.undertow.jsp@2.2.7.Final//org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
	at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
	at io.undertow.jsp@2.2.7.Final//org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:422)
	... 54 more
D.h. ich kann die Startseite der Anwendung ("kuchenliste.jsp") nicht mehr direkt aufrufen, sondern muss "kuchenliste.action" aufrufen.
Zur Verbesserung dieser nichtssagenden Fehlermeldung: https://issues.apache.org/jira/browse/WW-5475

Allerdings ist es trotzdem noch nötig, in der JSP explizit eine Action aufzurufen, siehe unten.

Die Seite "kuchenliste.jsp" rendert eine Tabelle auf Basis der Variablen kuchenListe, die in der aktuellen Action zu finden sein sollte. Es gibt zwei Wege zu dieser Seite:
Deshalb wird in "kuchenliste.jsp" explizit eine Action ausgeführt:
<s:action name="kuchenliste" var="kuchenlisteAction"></s:action>
Dies ruft execute der Action "kuchenliste" auf, die in struts.xml so definiert ist:
<action name="kuchenliste" class="de.hsrm.jakartaee.knauf.kuchenzutatstruts.actions.KuchenlisteAction">
    <result>/kuchenliste.jsp</result>
</action>
Obwohl diese Action innerhalb "kuchenliste.jsp" aufgerufen wird, muss eine Result-Seite in "struts.xml" definiert werden (in diesem Fall "kuchenliste.jsp").
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 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).

Doku des "action"-Tags: https://struts.apache.org/tag-developers/action-tag

StrutsParameter, next level

In "kuchendetail.jsp" wird in der Zutatenliste neben jeder Zutat ein "Bearbeiten"-Button angezeigt. Die Id des Kuchen wird als URL-Parameter weitergegeben. Da wir gerade die Details eines KuchenBean-Objekts anzeigen, wird die Kuchen-ID aus einer Property "id" eines Objekts "kuchen" aus der KuchenEditAction geholt.

<s:url var="editUrl" action="zutatedit">
  <s:param name="kuchen.id" value="kuchen.id"></s:param>
  <s:param name="id" value="id"></s:param>
</s:url>

Dieser Parameter wird an die ZutatEditAction weitergegeben. D.h. dort muss es die gleiche Struktur geben und damit eine Property "kuchen", die ein KuchenBean-Objekt enthält:
public class ZutatEditAction extends BaseAction 
{
  ...
  public KuchenBean getKuchen()
  {
    return this.kuchen;
  }
  ...
}
Die ID aus dem URL-Parameter soll in dieses KuchenBean-Objekt geschrieben werden. Dies funktioniert nur, wenn ein @StrutsParameter definiert wird (oder diese Anforderung in "struts.xml" abgeschaltet wurde). Allerdings ist dies nicht ausreichend, da wir hier eine Property innerhalb des Objekts setzen wollen. Das Definieren der Annotation auf getKuchen würde nur ein Setzen des gesamten KuchenBean-Objekts zulassen, aber nicht das Setzen von Properties dieses Objekts. Wir können den @StrutsParameter auch nicht auf KuchenBean.getId() definieren, da diese Bean im EJB-Projekt liegt, das die Struts-JARs eventuell (je nach Architektur) nicht kennt.

Aber es gibt eine Lösung: mit dem Attribut depth können wir definieren, dass auch geschachtelte Properties gesetzt werden sollen:
  @StrutsParameter(depth = 1)
  public KuchenBean getKuchen()
  {
    return this.kuchen;
  }


"struts.xml": struts.allowlist.classes


Wie auch im Struts-Basics-Beispiel müssen wir unsere Entity Beans für die Verwendung in Struts-Seiten/Ausdrücken zulassen:
    <constant name="struts.allowlist.classes" value="de.hsrm.jakartaee.knauf.kuchenzutatstruts.KuchenBean, de.hsrm.jakartaee.knauf.kuchenzutatstruts.ZutatBean"/>


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:
Siehe
https://struts.apache.org/core-developers/validation
Es wird im Package der Action (also in meinen Beispielen im Verzeichnis "de\hsrm\jakartaee\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.3//EN" 
  		"http://struts.apache.org/dtds/xwork-validator-1.0.3.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.

Die Fehlermeldungen werden bei den betroffenen Feldern ausgegeben.


Validation per Annotation:
https://struts.apache.org/core-developers/validation-annotation
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/release/2.3.x/docs/url.html

ValueStack/OGNL

Siehe https://struts.apache.org/tag-developers/ognl und https://github.com/orphan-oss/ognl/tree/main

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: struts.apache.org/release/2.3.x/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>
Die Quelle ist leider nicht mehr auffindbar :-(.


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: https://struts.apache.org/plugins/config-browser/
Um ihn zu aktivieren: die Datei "struts2-config-browser-plugin-7.0.0-M9.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.

Der Punkt "Beans" löst eine Exception aus, die wir lösen können, indem wir folgenden JARs der Anwendung zufügen:
Unter "Namespaces" => "default" finden wir unsere Actions, deren URLs und die Rückgabewerte:
Config Browser


Stand 19.10.2024
Historie:
19.10.2024: Erstellt aus 2014er-Beispiel. Umgestellt auf Struts 7.0-M9, JakartaEE10 und WildFly 34