Java Server Faces (Basics)
Inhalt:
Konfiguration
Projekt erstellen
ManagedBean "GeometricModelHandler"
Hilfsklasse "Historie"
JSP
Startseite
Troubleshooting
JSF-Facet zu bestehendem Projekt zufügen
Import
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, Kapitel 9.14.
Konfiguration
In den "Preferences" stellen wir unter "Web and XML" -> "JSF Libraries" die JSF-Runtime ein.
Im JBoss 4.2 wird die Version 1.2 der Sun-Referenzimplementation mitgeliefert, sie liegt im Verzeichnis "server\default\deploy\jboss-web.deployer\jsf-libs".
Wir fügen also eine neue JSF-Library zu, dabei wählen die die Dateien "jsf-api.jar" und "jsf-impl.jar" aus.
Wir geben dem Baby außerdem einen aussagekräftigen Namen (im Beispiel: "Sun JSF 1.2"), wählen als Version "v1_2" und setzen den Haken "Is JSF implementation".
Das Ergebnis sieht so aus:
Projekt erstellen
Beim Erstellen des Projekts ist es wichtig die Facet "Java Server Faces" Version 1.1 zuzufügen.
Im Schritt "JSF Capabilities" entfernen wir den Haken bei "Deploy JARs to WEB-INF/lib" (da die Jars bereits im JBoss enthalten sind) und wählen
unsere JSF-Library aus. Den Rest belassen wir beim Default.
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>
WTP legt uns die JSF-Konfigurationsdatei gemäß Version 1.1 an. Da wir 1.2 verwenden wollen passen wir faces-config.xml an:
<?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">
</faces-config>
Da wir die JSTL-Library verwenden wollen (Version 1.2), wird "%JBOSS_HOME%\server\default\deploy\jboss-web.deployer\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.swtvertiefung.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 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 JSF</title>
</head>
<body>
<f:view>
<h:form>
<h:panelGrid columns="2">
<c:if test="${sessionScope.geometricModelHandler.volumen > 0.0}">
<h:outputText value="Volumen:"/>
<h:outputText value="#{geometricModelHandler.volumen}"></h:outputText>
<h:outputText value="Oberfläche:"/>
<h:outputText value="#{geometricModelHandler.oberflaeche}"></h:outputText>
</c:if>
<h:outputText value="Kante a:"></h:outputText>
<h:inputText id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:outputText value="Kante b:"></h:outputText>
<h:inputText id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:outputText value="Kante c:"></h:outputText>
<h:inputText id="c" value="#{geometricModelHandler.c}"></h:inputText>
<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 2 Spalten initialisiert.
<h:panelGrid columns="2">
...
</h:panelGrid>
Im folgenden beginnt jeweils nach zwei 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="Oberfläche:"/>
<h:outputText value="#{geometricModelHandler.oberflaeche}"></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 id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:outputText value="Kante b:"></h:outputText>
<h:inputText id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:outputText value="Kante c:"></h:outputText>
<h:inputText id="c" value="#{geometricModelHandler.c}"></h:inputText>
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 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 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 klickt man auf "Add/Remove Project Facets...".
Man wählt die Facet "JavaServer Faces" aus:
Nach dem Klick auf "Next" gelangt man in den Dialog zur Konfiguration von "faces-config.xml", den wir auch schon beim Erstellen des Projekts
gesehen haben. Wichtig ist hier dass der Haken "Deploy jars to WEB-INF/lib" entfernt wird.
Bleibt der Haken "Deploy jars to WEB-INF/lib" gesetzt führt dies beim Deploy zu einer ganzen Reihe von Exceptions, von denen diese die erste ist:
21:52:18,968 ERROR [[/JSF]] Exception sending context initialized event to listener instance of class org.jboss.web.jsf.integration.config.JBossJSFConfigureListener
java.lang.UnsupportedOperationException
at com.sun.faces.config.ConfigureListener$InitFacesContext.getViewRoot(ConfigureListener.java:1690)
at com.sun.faces.util.MessageFactory.getMessage(MessageFactory.java:113)
at com.sun.faces.util.MessageUtils.getExceptionMessageString(MessageUtils.java:277)
at com.sun.faces.config.ConfigureListener.digester(ConfigureListener.java:1207)
at com.sun.faces.config.ConfigureListener.contextInitialized(ConfigureListener.java:318)
at org.jboss.web.jsf.integration.config.JBossJSFConfigureListener.contextInitialized(JBossJSFConfigureListener.java:69)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3854)
at org.apache.catalina.core.StandardContext.start(StandardContext.java:4359)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:761)
...
Anmerkung: das Zufügen der Facet führt beim ersten Aufrufen zu einer Fehlermeldung "ConcurrentModificationException". "faces-config.xml" wurde
erzeugt, allerdings wurde die Facet nicht zugefügt. Erst ein zweiter Durchlauf brachte den gewünschten Effekt (siehe auch folgender Abschnitt).
Import
Nach dem Import des Projekts muss die Projekt-Facet zugefügt werden (dabei die Einstellungen aus dem vorherigen Abschnitt beachten !).
Dies führt leider zu der Meldung "Failed while installing JavaServer Faces 1.1 - java.util.ConcurrentModificationException". Es gibt zwei Lösungen:
Lösung 1: einfach nochmal probieren, im zweiten Anlauf klappt es.
Lösung 2: Nach dem Import der WAR-Datei wird in "web.xml" die Registrierung des JSF-Servlets gelöscht (also folgendes entfernen):
<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>
Diese Einträge werden beim Zufügen der Facet wieder erstellt.
Stand 08.06.2007
Historie:
12.04.2007: Erstellt.
22.04.2007: Probleme beim Import beschrieben
08.06.2007: In Historie wurde "b" statt "c" ausgegeben. Hinweis auf Fehlermeldung wenn JSF-Jars auf den Server deployed werden.