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".
Neue JSF-Library
Das Ergebnis sieht so aus:
Neue JSF-Library

Projekt erstellen

Beim Erstellen des Projekts ist es wichtig die Facet "Java Server Faces" Version 1.1 zuzufügen.
Neues Projekt (1)
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.
Neues Projekt (2)
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".
Managed Bean zufügen (1)
Package und Klassenname werden eingegeben.
Managed Bean zufügen (2)
Als "scope" wählen wir "session" da in dieser Bean unsere letzten Berechnungen gespeichert werden sollen.
Managed Bean zufügen (3)
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:
Managed Bean zufügen (4)
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:


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...".
Facet zufügen (1)
Man wählt die Facet "JavaServer Faces" aus:
Facet zufügen (2)
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.
Facet zufügen (3)
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.