Kuchen-Zutat-Beispiel mit Java Server Faces


Inhalt:

Überblick
faces-config.xml
EJB-Injection
Parameterübergabe
Phase Event
Zugriff auf Request aus ManagedBean-Action-Methode
Validierung
Weitere Design-Tips

Dieses Beispiel stellt das KuchenZutat-Beispiel komplett auf JSF um und kämpft dabei mit einer Reihe von Problemen.

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


Überblick

Die Anwendung enthält vier Seiten:

Es gibt drei Managed Beans:
Die Navigation sieht so aus (alles über parameterlose Action-Methoden abgebildet):


faces-config.xml

"faces-config.xml" sieht so aus nachdem wir damit fertig sind (drei Managed Beans und drei Navigation-Rules sind eingetragen):
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
	version="1.2">
	<managed-bean>
		<managed-bean-name>
		kuchenListeHandler</managed-bean-name>
		<managed-bean-class>
		de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenListeHandler</managed-bean-class>
		<managed-bean-scope>
		request</managed-bean-scope>
	</managed-bean>
	<managed-bean>
		<managed-bean-name>
		kuchenDetailHandler</managed-bean-name>
		<managed-bean-class>
		de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenDetailHandler</managed-bean-class>
		<managed-bean-scope>
		request</managed-bean-scope>
	</managed-bean>
	<managed-bean>
		<managed-bean-name>
		zutatDetailHandler</managed-bean-name>
		<managed-bean-class>
		de.fhw.swtvertiefung.knauf.kuchenzutatjsf.ZutatDetailHandler</managed-bean-class>
		<managed-bean-scope>
		request</managed-bean-scope>
	</managed-bean>

	<navigation-rule>
		<description>"kuchenDetail" führt immer zur Detail-Seite !</description>
		<display-name>KuchenDetail-Seite</display-name>
		<navigation-case>
			<from-outcome>kuchendetail</from-outcome>
			<to-view-id>/kuchendetail</to-view-id>
		</navigation-case>
	</navigation-rule>
	
	<navigation-rule>
		<description>"kuchenliste" führt immer zur Liste-Seite !</description>
		<display-name>KuchenListe-Seite</display-name>
		<navigation-case>
			<from-outcome>kuchenliste</from-outcome>
			<to-view-id>/kuchenliste</to-view-id>
		</navigation-case>
	</navigation-rule>
	
	<navigation-rule>
		<description>"zutatDetail" führt immer zur Detail-Seite !</description>
		<display-name>ZutatDetail-Seite</display-name>
		<navigation-case>
			<from-outcome>zutatdetail</from-outcome>
			<to-view-id>/zutatdetail</to-view-id>
		</navigation-case>
	</navigation-rule>
</faces-config> 
Zu beachten ist dass alle Beans im Request-Scope stecken. Das hat den Nachteil dass bei jedem Request die benötigten Daten neu geladen werden. Der Vorteil ist dass nach einem Speichern eines Kuchens und anschließendem Zurückspringen zur Kuchenliste ein neuer Request erfolgt und dadurch die Kuchenliste mitsamt der Änderung neu eingeladen wird. Wäre die Kuchenliste (KuchenListeHandler) in der Session, dann müsste der geänderte Kuchen mühsam in der Liste aktualisiert werden (was von der Managed Bean aus nicht direkt möglich ist).

Die Navigation Rules sind eher simpel gehalten: jede Action-Methode die als Rückgabe "kuchendetail" hat wird automatisch zur View "kuchendetail" (und damit zur entsprechenden .faces-Seite bzw. zur JSP) weitergeleitet.

EJB-Injection

Alle drei Managed Beans benötigen die Stateless Session Bean de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenZutatWorkerBean. Diese können wir mittels EJB-Injection einlesen.
Das sieht so aus:
  @EJB(name="java:comp/env/ejb/KuchenZutatWorkerLocal")
  private KuchenZutatWorkerLocal kuchenZutatWorkerBean; 
Ich habe eine Injection hier NUR über den Environment Naming Context geschafft. In web.xml muss also folgendes eingetragen sein:
	
	...
	<ejb-local-ref>
		<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<local-home>java.lang.Object</local-home>
		<local>de.fhw.swtvertiefung.knauf.kuchenzutatjsf.KuchenZutatWorkerLocal</local>
	</ejb-local-ref>
	...
"jboss-web.xml" muss dieses enthalten (kompletter Inhalt der Datei):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web PUBLIC
    "-//JBoss//DTD Web Application 4.2//EN"
    "http://www.jboss.org/j2ee/dtd/jboss-web_4_2.dtd">
<jboss-web>

	<context-root>KuchenZutatJSFWeb</context-root>

	<ejb-local-ref>
		<ejb-ref-name>ejb/KuchenZutatWorkerLocal</ejb-ref-name>
		<local-jndi-name>KuchenZutatJSF/KuchenZutatWorkerBean/local</local-jndi-name>
	</ejb-local-ref>

</jboss-web> 


Parameterübergabe

An einigen Stellen muss beim Sprung von einer View zur anderen ein Parameter übergeben werden. Dies kann auf zwei Wegen geschehen (jeweils mit Vor- und Nachteilen).

Weg 1: Hidden Field:
Aus "kuchendetail.jsp" im Formular für das Bearbeiten des aktuellen Kuchens:
<h:inputHidden id="kuchenid" value="#{kuchenDetailHandler.kuchenId}"></h:inputHidden> 

Vorteil: der Wert wird beim Generieren der JSP aus der aktuellen Instanz des KuchenDetail-Handlers gelesen und beim Absenden des Requests dort auch wieder hineingeschrieben.

Nachteil: Falls die Zielseite eine andere Seite ist, dann wird der Wert aus dem Parameter trotzdem in den KuchenDetail-Handler geschrieben. Deshalb nicht allzu geeignet für z.B. die Übergabe einer Kuchen-ID in der Zutatenliste eines Kuchens, wenn das Ziel die Zutatenseite ist.

Fazit: einfaches, aber unflexibles Verfahren, dass nur innerhalb einer Managed Bean funktioniert (zum Beispiel in "kuchendetail.jsp" beim Klick auf "OK").

Weg 2: <f:param>-Tag:
Innerhalb eines <h:commandButton> oder <h:commandLink> können Werte an das Ziel übergeben werden. Dies kann so aussehen (Link zum Bearbeiten einer Zutat auf "kuchendetail.jsp"):
	<h:commandLink id="kuchenedit" action="#{zutatDetailHandler.editZutat}"
			actionListener="#{zutatDetailHandler.selectKuchenZutat}" value="Bearbeiten">
		<f:param id="kuchenId" value="#{kuchenDetailHandler.kuchenId}"></f:param>
		<f:param id="zutatId" value="#{zutatAktuell.id}"></f:param>
	</h:commandLink> 
"zutatAktuell" ist hier eine Variable im PageContext, die beim Befüllen des dataTable aus der Zutatenliste gesetzt wurde.

Vorteil: wird an die Zielseite geschickt.

Nachteil: Keinerlei Automatismus. Die Action-Methode editZutat hat keine Chance auf die Werte zuzugreifen. Deshalb muss der Parameter manuell ausgewertet werden. Dazu wurde das "actionListener"-Attribut gesetzt. Hier wird eine void-Methode angegeben die einen Parameter vom Typ "javax.faces.event.ActionEvent" erwartet.
Diese Methode könnte so aussehen:
	public void selectKuchenZutat(ActionEvent event)
	{
		UIParameter component = (UIParameter) event.getComponent().findComponent("kuchenId");
		this.intKuchenId = Integer.parseInt(component.getValue().toString());

		UIParameter component = (UIParameter) event.getComponent().findComponent("zutatId");
		this.intZutatId = Integer.parseInt(component.getValue().toString());
	}
Es werden zwei Komponenten mit den Namen "kuchenId" und "zutatId" gesucht, die vom Typ javax.faces.component.UIParameter sind. Deren Werte werden ausgewertet und in Membervariablen der Klasse gesetzt. Diese sind zum Zeitpunkt des Aufrufs von editZutat vorhanden.


Phase Event

Mit obigem Vorgehen landet ich in folgender Konstellation in einer Sackgasse !
Lösungsansatz 1 dieses Dilemmas: KuchenDetailHandler kommt in den Session Scope. Nicht schön weil dadurch kein Zurückblättern in der Historie möglich wäre: werden nacheinander zwei Kuchen geöffnet, und wird über den Zurück-Button im Browser vom zweiten zum ersten Kuchen zurückgeblättert, dann würde der KuchenDetailHandler in der Session immer noch den zweiten Kuchen enthalten, mit fatalen Folgen beim Öffnen der Zutat. Außerdem wäre einiges an Verrenkungen nötig um nach dem Speichern einer Zutat den Kuchen dazu zu bewegen seine Zutatenliste zu aktualisieren ;-).

Lösungsansatz 2: Einklinken in die Verarbeitungsphasen von JSF. Hierzu wurde im <f:view>-Tag das beforePhase-Attribut gesetzt:
<f:view beforePhase="#{kuchenDetailHandler.beforePhase}"> 

Die Methode KuchenDetailHandler.beforePhase sucht in den Request-Parametern nach allem was mit ":kuchenId" endet (JSF setzt die komplette Komponentenhierarchie in den Namen des Request-Parameters, in der Zutatenliste sieht das so aus: tablezutaten:0:formzutatedit:kuchenId für die Zutat in Zeile 0, tablezutaten:1:formzutatedit:kuchenId für die Zutat in Zeile 1. Deshalb habe ich hier keine Chance auf den exakten Namen der Komponente zu prüfen)
Der Code der Methode sieht so aus:
  public void beforePhase (javax.faces.event.PhaseEvent e)
  {
    //Nur etwas machen in der Phase "ApplyRequestValues" !
    if (e.getPhaseId().equals( PhaseId.APPLY_REQUEST_VALUES))
    {
      Map<String, String> mapRequest = e.getFacesContext().getExternalContext().getRequestParameterMap();
      
      for (Entry<String, String> entryAktuell : mapRequest.entrySet() )
      {
        String strKey =  entryAktuell.getKey();
        
        if (strKey.endsWith(":kuchenId"))
        {      
          this.intKuchenId = Integer.parseInt(entryAktuell.getValue());
        }
      }
    }
  } 
Das bedingt leider eine Änderung an der Parameterversorung des Zutat-Detail-Links: <f:param>-Tags kann ich nicht als Request-Parameter parsen ! Deshalb verwende ich ein Hidden Field:
	<h:form id="formzutatedit">
		<h:inputHidden id="kuchenId" value="#{kuchenDetailHandler.kuchenId}"></h:inputHidden>
		<h:commandLink id="kuchenedit" action="#{zutatDetailHandler.editZutat}"
				actionListener="#{zutatDetailHandler.selectKuchenZutat}" value="Bearbeiten">
			<f:param id="zutatId" value="#{zutatAktuell.id}"></f:param>
		</h:commandLink>
	</h:form> 
Dieses Hidden Field wird zwar dank Value Binding in den KuchenDetailHandler zurückgeschrieben, das stört uns allerdings nicht weiter. War wir damit erreicht haben ist dass bei dem Aufruf von KuchenDetailHandler.getZutaten die Kuchen-ID bereits gesetzt ist !

Diese schicke Lösung hat einen Nachteil: im ZutatDetailHandler kommt die KuchenId in diesem konkreten Fall nicht mehr als UIParameter an sondern in einem HTML-Hidden Field. Deshalb muss die Logik in ZutatDetailHandler.selectKuchen (diese Methode wird auch aus selectKuchenZutat heraus aufgerufen) erweitert werden:
	public void selectKuchen(ActionEvent event)
	{
		String strValue = null;
		if (event.getComponent().findComponent("kuchenId").getClass().equals(UIParameter.class) )
		{
			UIParameter component = (UIParameter) event.getComponent().findComponent("kuchenId");
			strValue = component.getValue().toString();	
		}
		else if (event.getComponent().findComponent("kuchenId").getClass().equals(HtmlInputHidden.class) )
		{
			HtmlInputHidden component = (HtmlInputHidden) event.getComponent().findComponent("kuchenId");
			strValue = component.getValue().toString();
		}
		else
		{
			throw new FacesException ("Keine Komponente kuchenId gefunden !");
		}
		this.intKuchenId = Integer.parseInt(strValue);
	}
Hier wird also die Kuchen-ID in zwei unterschiedlichen Komponenten-Typen gesucht.


Zugriff auf Request aus ManagedBean-Action-Methode

Oben gezeigte Techniken zur Parameterübergabe lassen sich eventuell vereinfachen: es ist in der Managed Bean möglich auf Request-Parameter direkt zuzugreifen. Dadurch hebelt man natürlich die JSF-Technik ziemlich aus ;-).
Ein Beispiel ist die Methode KuchenDetailHandler.deleteKuchen, die von der Kuchenliste aufgerufen wird und einen Kuchen löscht. Da die Managed Bean KuchenlisteHandler im Request steckt und schon beim Klick auf den Löschen-Link initialisiert wurde steckt der gelöschte Kuchen noch in der Kuchenliste und würde deshalb trotz allem angezeigt. Lösung des Problems: auf den KuchenlisteHandler zugreifen, der als Attribut im Request steckt, und dort den Kuchen aus der Liste entfernen:
    ServletRequest request = (ServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
    KuchenListeHandler kuchenListeHandler = (KuchenListeHandler) request.getAttribute("kuchenListeHandler");
    kuchenListeHandler.clearKuchenliste(); 

Mit einer ähnlichen Vorgehensweise kann man auf die Session zugreifen um z.B. Daten darin zu speichern:
    HttpSession session = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false);
    session.setAttribute("MyAttribute", "MyValue"); 


Validierung

Im Beispiel sind alle Eingabefelder als Pflichtelement deklariert (Attribut "required" setzen). Für den Kuchen ist die minimale Länge auf 5 Zeichen gesetzt:
	<h:inputText id="name" value="#{kuchenDetailHandler.name}" required="true">
		<f:validateLength minimum="5"></f:validateLength>
	</h:inputText> 
Entstehen Fehlermeldungen beim Validieren dieser Komponente, können sie an beliebiger Stelle der Seite ausgegeben werden:
	<h:message for="name"></h:message> 
Durch das Attribut for wird der Name der Komponente angegeben deren Validierungsfehler hier ausgegeben werden sollen. Leider enthält der Meldungstext hier immer auch den Namen der Komponente in einer eher häßlichen Darstellung.


Weitere Design-Tips

Formular sollten möglichst klein gehalten werden, z.B. sollte ein Navigationslink nicht im gleichen form stecken wie die eigentlichen Eingabefelder. Grund: bei einem Klick auf den Link wird das gesamte Formular validiert, und deshalb könnte ein nicht ausgefülltes Eingabefeld einen Klick auf den Link verbieten.

IDs: Alle Formular-Elemente sollten mit IDs versehen werden (h:form, h:inputHidden, h:inputText usw.). Dadurch ist das Element in Fehlermeldungen oder beim Auswerten des Requests einfacher zuzuordnen.


Stand 29.07.2007
Historie:
22.04.2007: Erstellt.
08.06.2007: Löschen von Zutat und Kuchen eingebaut. KuchenDetailHandler war im Session-Scope. Abschnitt "Zugriff auf Request aus ManagedBean-Action-Methode" zugefügt. Fehlermeldungen sind jetzt sauberer in Tabelle integriert.
29.07.2007: Tippfehler beim Zugriff auf Request korrigiert, Zugriff auf HttpSession beschrieben.