Java Server Faces (Basics)
Inhalt:
Projekt erstellen
ManagedBean "GeometricModelHandler"
Hilfsklasse "Historie"
JSP
Startseite
Troubleshooting
JSF-Facet zu bestehendem Projekt zufügen
ACHTUNG:
Der Web Tools Platform-Plugin (bis einschließlich Version 3.0.3) hat einen Bug, durch den in einer Config-Datei die JSF-Libraries nicht eingebunden werden. Das führt in meinem Beispiel
zu Validierungsfehlern bei der Einbindung der JSF-Taglibraries.
Bugreport und Workaround siehe
https://bugs.eclipse.org/bugs/show_bug.cgi?id=258181.
Mein Eclipse-Paket enthält den Workaround bereits.
Dieses Beispiel baut auf der gleichen Logik auf wie die JSP-Beispiel, es existiert also nur eine Web-Anwendung mit minimaler Programmlogik.
Bereits durchgeführte Berechnungen werden in der Session gespeichert.
Hier gibt es das Projekt als WAR-Export-Datei: JSF.war.
Die offizielle Sun-Seite zu JSF: http://java.sun.com/javaee/javaserverfaces/reference/docs/index.html
Weitere Informationen findet man im JavaEE5-Tutorial, Part II, Kapitel 10-15.
Projekt erstellen
Beim Erstellen des "Dynamic Web Project" ist es wichtig die Facet "Java Server Faces 1.2" zuzufügen. Dies geschieht, indem die Configuration
"Java Server Faces v1.2 Project" gewählt wird.
Im Schritt "JSF Capabilities" wählen wir "Server supplied JSF implementation" (da die Jars bereits im JBoss enthalten sind).
Unter "URL Mapping Patterns" löschen wir die Vorgabe "/faces/*" und ersetzen sie durch "*.faces" (zumindest für mein Beispiel, ihr könnt das natürlich
im Projekt anders halten).
Im Verzeichnis "WEB-INF" befindet sich jetzt eine neue Datei "faces-config.xml", für die es einen schicken Editor gibt.
In "web.xml" wurde das Faces-Servlet eingebunden:
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
Da wir die JSTL-Library verwenden wollen (Version 1.2), wird "%JBOSS_HOME%\server\default\deploy\jbossweb.sar\jstl.jar" nach
"WEB-INF\lib" kopiert.
Anmerkung: Die im Beispiel "JSP3" beschriebenen zusätzlichen Schritte (c.tld aus der JAR-Datei in ein Unterverzeichnis
von WEB-INF legen und ein Element <jsp-config>
mit der Verbindung von URI zu TLD-Datei in "web.xml" einfügen)
sind normalerweise nicht nötig, WTP und JBoss sind intelligent genug die zur URI passende TLD-Datei selbst zu finden.
Nur wenn diese nicht eindeutig wäre oder (aus welchen Gründen auch immer nicht identisch ist mit der in der TLD deklarierten)
müssen diese zwei Schritte erfolgen.
ManagedBean "GeometricModelHandler"
Zuallererst fügen wir die ManagedBean "GeometricModelHandler" zu die die Anwendungslogik enthält.
Dazu öffnen wir "faces-config.xml" und gehen auf der Karteikarte "Managed Beans" auf "Add".
Wir wählen die Option "Create a new Java class".
Package und Klassenname werden eingegeben.
Als "scope" wählen wir "session" da in dieser Bean unsere letzten Berechnungen gespeichert werden sollen.
Im letzten Schritt werden die Eingaben nochmals zusammengefaßt angezeigt, hier gibt es nichts weiter für uns zu tun.
Das Ergebnis sieht so aus:
In "faces-config.xml" wurde folgendes zugefügt:
<managed-bean>
<description>
Diese Managed Bean übernimmt die Berechnung der Eingaben, außerdem wird hier die Liste der letzten Berechnungen gespeichert.</description>
<managed-bean-name>geometricModelHandler</managed-bean-name>
<managed-bean-class>de.fhw.komponentenarchitekturen.knauf.jsf.GeometricModelHandler</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
Jetzt werden der Managed Bean Properties zugefügt:
private double dblA = 0;
private double dblB = 0;
private double dblC = 0;
public double getA()
{
return dblA;
}
public void setA(double dblA)
{
this.dblA = dblA;
}
public double getB()
{
return dblB;
}
public void setB(double dblB)
{
this.dblB = dblB;
}
public double getC()
{
return dblC;
}
public void setC(double dblC)
{
this.dblC = dblC;
}
Es gibt get-Properties für die aktuell berechneten Werte sowie die Historie:
private double dblOberflaeche = 0;
private double dblVolumen = 0;
private Historie historieGesamt = new Historie();
public Historie getHistorieGesamt()
{
return historieGesamt;
}
public double getVolumen()
{
return this.dblVolumen;
}
public double getOberflaeche()
{
return this.dblOberflaeche;
}
Schließlich wird die Methode berechnen
zugefügt, die beim Klick auf "Submit" aufgerufen wird und
die Berechnung durchführt sowie die aktuelle Berechnung der Historie zufügt.
public String berechnen()
{
//Ausrechnen:
this.dblVolumen = this.dblA * this.dblB * this.dblC;
this.dblOberflaeche = 2 * (this.dblA * this.dblB) + 2 * (this.dblA * this.dblC) + 2 * (this.dblB * this.dblC);
//Zufügen zur Historie !
Seitenlaengen seitenlaengeAktuell = new Seitenlaengen();
seitenlaengeAktuell.setA(this.dblA);
seitenlaengeAktuell.setB(this.dblB);
seitenlaengeAktuell.setC(this.dblC);
this.historieGesamt.addSeitenlaenge( seitenlaengeAktuell );
//Rückgabe ist egal da es hier keine Navigation gibt:
return null;
}
Die Rückgabe einer solchen Submit-Methode ist normalerweise eine Navigationsregel die die Zielseite zurückgibt.
Kommt dabei null
zurück so gelangt man automatisch wieder zu der Seite die das Formular abgeschickt hat.
Hilfsklasse "Historie"
In der Klasse "Historie", in der die bereits durchgeführten Berechnungen abgelegt werden, ergeben sich zwei kleine Änderungen:
public Seitenlaengen[] getSeitenlaengen()
{
Seitenlaengen[] seitenlaengeGesamt = new Seitenlaengen[this.vectorBerechnungen.size()];
for (int intIndex = 0; intIndex < this.vectorBerechnungen.size(); intIndex++)
{
seitenlaengeGesamt[intIndex] = this.vectorBerechnungen.get(intIndex);
}
return seitenlaengeGesamt;
}
public int getSize()
{
return this.vectorBerechnungen.size();
}
Die Methode getSize
ist nötig um mittels <c:if>
-Tag auf das Vorhandensein von Elementen zu prüfen (nur dann
wird die Tabelle der bereits durchgeführten Berechnungen angezeigt).
getSeitenlaengen
liefert die Seitenlaengen
-Objekte als Array zurück. Grund hierfür ist dass das JSF-Tag <h:dataTable>
scheinbar nicht über einen Generic-Iterator laufen kann, wohl aber über ein Array.
JSP
Wir fügen eine JSP-Seite "geometricmodel.jsp" zu. Warum ich sie nicht "index.jsp" nenne wird im nächsten Abschnitt erklärt !
Die Seite verwendet die JSTL-Core-Library.
Sie sieht so aus:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Test für JSF</title>
</head>
<body>
<f:view>
<h:form>
<h:panelGrid columns="3">
<c:if test="${sessionScope.geometricModelHandler.volumen > 0.0}">
<h:outputText value="Volumen:"/>
<h:outputText value="#{geometricModelHandler.volumen}"></h:outputText>
<h:outputText value=""></h:outputText>
<h:outputText value="Oberfläche:"/>
<h:outputText value="#{geometricModelHandler.oberflaeche}"></h:outputText>
<h:outputText value=""></h:outputText>
</c:if>
<h:outputText value="Kante a:"></h:outputText>
<h:inputText label="Kante A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:message for="a"></h:message>
<h:outputText value="Kante b:"></h:outputText>
<h:inputText label="Kante B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:message for="b"></h:message>
<h:outputText value="Kante c:"></h:outputText>
<h:inputText label="Kante C" id="c" value="#{geometricModelHandler.c}"></h:inputText>
<h:message for="c"></h:message>
<h:commandButton id="berechnen" value="Berechnen" action="#{geometricModelHandler.berechnen}"></h:commandButton>
<h:outputText value=""></h:outputText>
<%--Historie ausgeben: --%>
<c:if test="${geometricModelHandler.historieGesamt.size > 0}">
<%--In einem DataTable mit drei Spalten die letzten Berechnungen ausgeben. Aktuelles Element in Variable "seitenlaengeAktuell" packen --%>
<h:dataTable value="#{geometricModelHandler.historieGesamt.seitenlaengen}" var="seitenlaengeAktuell">
<h:column>
<f:facet name="header">
<h:outputText value="A"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.a}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="B"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.b}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="C"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.c}"></h:outputText>
</h:column>
</h:dataTable>
</c:if>
</h:panelGrid>
</h:form>
</f:view>
</body>
</html>
Die Elemente im Einzelnen:
- Es werden drei Taglibraries eingebunden: JSF-HTML, JSF-Core und JSTL-Core:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
- Es wird eine JSTL-View deklariert (hiervon darf es pro JSP nur eine geben), die ein JSTL-Formular enthält:
<f:view>
<h:form>
...
</h:form>
</f:view>
- Es wird ein Tabellenlayout mit 3 Spalten initialisiert (wobei Spalte 3 nur bei Fehlermeldungen der Felder sichtbar ist).
<h:panelGrid columns="3">
...
</h:panelGrid>
Im folgenden beginnt jeweils nach drei Ausdrücken eine neue Zeile.
- Zuallererst wird geprüft ob beim aktuellen Aufruf der Seite eine Berechnung durchgeführt wurde. Hierzu wird das
JSTL-Tag
c:if
genutzt. In der Prüfbedingung kann man auf die Managed Bean zugreifen, da diese unter dem in faces-config.xml
deklarierten managed-bean-name
im entsprechenden Scope (hier: Session) liegt.
<c:if test="${sessionScope.geometricModelHandler.volumen > 0.0}">
...
</c:if>
Da der JBoss aktuell noch nicht die Unified EL unterstützt müssen die EL-Ausdrücke hier mit "$" angegeben werden.
- Innerhalb dieser If-Prüfung werden die aktuellen Werte der ManagedBean "geometricModelHandler" ausgegeben:
<h:outputText value="Volumen:"/>
<h:outputText value="#{geometricModelHandler.volumen}"></h:outputText>
<h:outputText value=""></h:outputText>
<h:outputText value="Oberfläche:"/>
<h:outputText value="#{geometricModelHandler.oberflaeche}"></h:outputText>
<h:outputText value=""></h:outputText>
Zu beachten: in JSF-Tags beginnen Expression-Language-Ausdrücke mit "#".
- Jetzt folgt das eigentliche Formular. Zuerst drei Ein-/Ausgabefelder. Diese werden mit den Properties a, b und c der Managed Bean
verdrahtet, d.h. beim Erzeugen der HTML-Seite werden die aktuellen Werte in die Felder geschrieben, und beim Auswerten des Requests
werden die Request-Parameter "a", "b" und "c" (durch das Attribut
id
definiert) in die entsprechenden Felder geschrieben.
<h:outputText value="Kante a:"></h:outputText>
<h:inputText label="Kante A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:message for="a"></h:message>
<h:outputText value="Kante b:"></h:outputText>
<h:inputText label="Kante B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:message for="b"></h:message>
<h:outputText value="Kante c:"></h:outputText>
<h:inputText label="Kante C" id="c" value="#{geometricModelHandler.c}"></h:inputText>
<h:message for="c"></h:message>
Falls beim Parsen der Eingaben ein interner Fehler auftritt, so wird die entsprechende Meldung hinter dem Feld mittels des Elements h:message
ausgeben
(z.B. beim Eingeben von Buchstaben).
Das Attribut for="..."
gibt dabei die ID des Eingabefelds an, dessen Fehler hier ausgegeben werden. Diese Fehlerausgaben sind der Grund,
warum wir im h:panelGrid
drei Spalten haben.
Das Attribut label="..."
des h:inputText
definiert einen Text, der in den Fehlermeldungen verwendet wird.
Das führt zu solchen Ausgaben: "Kante A: '0.0a' must be a number between 4.9E-324 and 1.7976931348623157E308 Example: 1999999"
Würden wir das label
-Attribut nicht setzen, würde die Fehlermeldung z.B. so aussehen: "j_id_jsp_1992281669_1:a: 'a' must be a number between 4.9E-324 and 1.7976931348623157E308 Example: 1999999"
(man würde also die häßliche interne ID in ihrer ganzen Pracht sehen).
Hätten wir kein h:message
-Element deklariert, so würde wieder die Seite "geometricmodel.jsp" ohne weitere Fehlermeldungen aufgerufen werden, nur in der Server-Konsole
fänden wir die Meldung "21:55:38,890 INFO [lifecycle] WARNING: FacesMessage(s) have been enqueued, but may not have been displayed.
sourceId=j_id_jsp_1992281669_1:a[severity=(ERROR 2), summary=(j_id_jsp_1992281669_1:a: 'a' must be a number consisting of one or more digits.), detail=(j_id_jsp_1992281669_1:a: 'a' must be a number between 4.9E-324 and 1.7976931348623157E308 Example: 1999999)]"
Anmerkung: wenn wir unser h:message
-Element so definieren:
<h:message showSummary="true" showDetail="false" for="b"></h:message>
sehen wir eine zusammenfassende Nachricht:
"Kante B: 'B' must be a number consisting of one or more digits."
showSummary
ist per Default auf "false", showDetail
ist per Default "true". Ich habe den Default also gerade umgedreht.
Der Submit-Button wird durch ein Element h:commandButton
abgebildet. Im Attribut action
wird angegeben
welche Methode beim Klick/Abschicken des Formulars aufgerufen werden soll.
<h:commandButton id="berechnen" value="Berechnen" action="#{geometricModelHandler.berechnen}"></h:commandButton>
Das folgende h:outputText
-Element erklärt sich daher dass hier eine Tabellenzeile des panelGrid
abgeschlossen wird.
- Mittels
<c:if>
-Tag wird geprüft ob Einträge in der Historie vorhanden sind, nur dann werden diese ausgegeben.
- Die Historie wird in einem
<h:dataTable>
ausgegeben.
<h:dataTable value="#{geometricModelHandler.historieGesamt.seitenlaengen}" var="seitenlaengeAktuell">
...
</h:dataTable>
Das Attribut value
gibt an über welche Liste (hier: ein Array aus der Managed Bean) gelaufen werden soll.
Das Attribut var
ist ein PageContext-Attribut, in dem das aktuelle Seitenlaengen
-Objekt landet.
- Jetzt werden die Tabellen-Inhalte ausgegeben:
<h:column>
<f:facet name="header">
<h:outputText value="A"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.a}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="B"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.b}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="C"/>
</f:facet>
<h:outputText value="#{seitenlaengeAktuell.b}"></h:outputText>
</h:column>
Es werden drei Spalten deklariert (<h:column>
). Jede Spalte enthält einen Header, dieser wird durch <f:facet name="header"> deklariert.
Das name
-Attribut erzeugt durch den konstanten Wert header
einen Header, der Überschrift-Text steckt im Inhalt des Tags.
Der Spalteninhalt pro Zeile wird anschließend als Textausgabe deklariert. Dieser stammt aus dem PageContext-Attribut "seitenlaengeAktuell", es werden in den drei Spalten die Properties
"a", "b" und "c" abgerufen. Letzterer Ausdruck führt leider beim WTP-Plugin zu einer Validierungs-Warnung.
Startseite
Eine Besonderheit gibt es bei der Initialisierung der Session: bevor die erste JSP aufgerufen wird, die JSF-Tags enthält, MUSS
das FacesServlet initialisiert werden.
Was dies bedeutet erkennt man, wenn oben angelegte JSP direkt über die URL http://localhost:8080/JSF/geometricmodel.jsp
aufgerufen wird (hier nur der RootCause der Exception da dieser aussagekräftiger ist als die eigentliche Exception):
java.lang.RuntimeException: Cannot find FacesContext
javax.faces.webapp.UIComponentClassicTagBase.getFacesContext(UIComponentClassicTagBase.java:1763)
javax.faces.webapp.UIComponentClassicTagBase.isIncluded(UIComponentClassicTagBase.java:1647)
javax.faces.webapp.UIComponentClassicTagBase.setJspId(UIComponentClassicTagBase.java:1575)
org.apache.jsp.geometricmodel_jsp._jspx_meth_f_005fview_005f0(geometricmodel_jsp.java:109)
org.apache.jsp.geometricmodel_jsp._jspService(geometricmodel_jsp.java:83)
org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:384)
org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:320)
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:266)
javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
Die einfachste Lösung wäre, nicht die JSP-Seite aufzurufen, sondern einen Alias der über das Faces-Servlet führt. Das Faces-Servlet behandelt
(siehe obiger Auszug aus "web.xml") alle URLs mit der Endung ".faces", und leitet diese auf die gleichnamige JSP-Seite weiter.
Wir könnten also mittels http://localhost:8080/JSF/geometricmodel.faces
auf die Seite zugreifen.
Da dies nicht wirklich komfortabel ist habe ich eine Startseite "index.jsp" vorgeschaltet die einen Link zur Faces-Seite bietet.
Wichtig bei dieser Seite ist, dass sie nicht an einer Session teilnimmt:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page session="false" language="java" contentType="text/html; charset=ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Test für JSF</title>
</head>
<body>
<h1>JSF-Basics</h1>
<a href="geometricmodel.faces">Zur Startseite</a>
</body>
</html>
Eine Alternative wäre ein direktes Redirect auf die Faces-Seite (kompletter Code der Seite):
<%@ page session="false" language="java" contentType="text/html; charset=ISO-8859-1"%>
<% response.sendRedirect("geometricmodel.faces"); %>
Die Anwendung findet sich unter dieser URL: http://localhost:8080/JSF/index.jsp.
Troubleshooting
Nach dem Redeploy einer JSF-Anwendung kann es im Browser zu folgendem Fehler kommen:
javax.servlet.ServletException: viewId:/geometricmodel.faces - View /geometricmodel.faces could not be restored.
javax.faces.webapp.FacesServlet.service(FacesServlet.java:249)
org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
root cause
javax.faces.application.ViewExpiredException: viewId:/geometricmodel.faces - View /geometricmodel.faces could not be restored.
com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:180)
com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:248)
com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:117)
javax.faces.webapp.FacesServlet.service(FacesServlet.java:244)
org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
Dies tritt immer dann auf wenn man vorher eine Seite offen hatte die über JSF erzeugt wurde, und diese Seite nach dem Reploy direkt wieder
an den Server abschickt (also z.B. im obigen Beispiel erneut auf den Submit-Button klickt).
Lösung: nach jedem Redeploy über die Indexseite in die Anwendung einsteigen.
JSF-Facet zu bestehendem Projekt zufügen
Falls man bereits ein Webprojekt ohne JSF-Unterstützung hat, läßt sich dies leicht nachtragen.
Man geht in die Properties des Projekts in den Punkt "Project Facets". Dort aktiviert man die Facet "JavaServer Faces" und setzt die Version auf "1.2".
Der OK-Button ist erst klickbar, wenn wir einmal auf "Further configuration required..." hauen.
Wenn man die JSF-Einstellungen beim Projekt-Erstellen bereits einmal eingestellt hat, muss man hier nichts ändern, ansonsten
muss die Option "Server supplied runtime" gewählt werden sowie die "URL Mapping Patterns" angepaßt werden.
Stand 20.01.2009
Historie:
09.12.2008: Erstellt aus Vorjahresbeispiel, angepaßt an Eclipse 3.4 / WebTools 3.0. Doctype geändert, da Eclipse bei "XHTML" falsche Warnungen ausspuckt.
04.01.2009: Kleine Korrekturen auf Webseite
20.01.2009: Package im WAR korrigiert.