Java Server Faces (Basics mit Facelet) (JavaEE 8)
Inhalt:
Projekt erstellen
ManagedBean "GeometricModelHandler"
Hilfsklasse "History"
Facelet
"beans.xml"
JSF 2.3 aktivieren
Startseite
JSF-Facet zu bestehendem Projekt zufügen
Dieses Beispiel baut auf der gleichen Logik auf wie die JSP-Beispiele, es existiert also nur eine Web-Anwendung mit minimaler Programmlogik.
Bereits durchgeführte Berechnungen werden in der Session gespeichert.
Das Beispiel baut auf JavaEE8 und WildFly 24.0 auf.
Hier gibt es das Projekt als WAR-Export-Datei: JSF.war.
Nach dem Import muss man die JSF-Facet dem Projekt zufügen, siehe JSF-Facet zu bestehendem Projekt zufügen
Für mehr Informationen zu JSF sei auf das JavaEE6-Tutorial, Part II, ab Kapitel 4 verwiesen:
http://download.oracle.com/javaee/6/tutorial/doc/index.html
Hier finden sich die Spezifikationen für JSF 2.2 (JavaEE7) und JSF 2.3 (JavaEE8):
https://javaee.github.io/javaserverfaces-spec/
Facelets vs. JSP
In den JSF-Spezifikationen 1.x wurden JSP-Seiten für die Abbildung der Oberfläche verwendet. Ab JSF 2 werden stattdessen Facelets verwendet, über als XHTML-Seiten
umgesetzt werden. Die Deklaration der JSF-Oberfläche erfolgt über eine "View Declaration Language", von der Facelets und JSP Implementationen sind. Ab JSF 4.0 (JakartaEE10)
wurde die JSP-Unterstützung entfernt.
Dieses Beispiel verwendet entsprechend eine XHTML-Seite. Das Vorgängerbeispiel für eine JSP-Seite findet sich hier.
Managed Bean vs. CDI
In früheren JSF-Spezifikationen wurde eine Bean als "Managed Bean" deklariert.
In JSF 4 wurde diese Möglichkeit entfernt. Jetzt wird CDI verwendet.
Projekt erstellen
Beim Erstellen des "Dynamic Web Project" wird die "Dynamic web module version" auf "4.0" gestellt.
Außerdem ist es wichtig die Facet "JavaServer Faces 2.3" zuzufügen. Diese ist leider nicht in der "Configuration"-Combobox enthalten,
sondern man muss zuerst auf "Modify..." klicken.
In dem erscheinenden Dialog steht bei "Configuration" vermutlich "Custom".
In der darunter liegenden Tabelle der zu installierenden Facets wird jetzt ein Haken bei "JavaServer Faces" gesetzt und die Version 2.3 gewählt.
Anschließend wird diese Configuration unter einem sinnvollen Namen (in meinem Fall: "WildFly 24 + JSF 2.3") gespeichert.
Jetzt auf "OK" und weiter im Assistenten.
Im nächsten Schritt ("Java") bleibt alles bei den Defaults.
Im nächsten Schritt "Web Module" lassen wir uns einen Deploymentdescriptor "web.xml" genrieren:
Im Schritt "JSF Capabilities" wählen wir "Libary provided by target runtime" aus, da WildFly die JSF-Implementierung bereitstellt.
Unter "URL Mapping Patterns" löschen wir die Vorgabe "/faces/*" und ersetzen sie durch "*.xhtml":
Im Verzeichnis "WEB-INF" befindet sich jetzt eine neue Datei "faces-config.xml".
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>*.xhtml</url-pattern>
</servlet-mapping>
Da wir die JSTL-Library verwenden wollen (Version 1.2), wird die Datei "jstl-1.2.jar" von https://repo.maven.apache.org/maven2/jstl/jstl/1.2/
heruntergeladen und nach "WEB-INF\lib" kopiert. Alternativ könnte man auch die in WildFly enthaltene Datei in "%WILDFLY_HOME%\modules\system\layers\base\javax\servlet\jstl\api\main\taglibs-standard-impl-1.2.6-RC1.jar" verwenden.
verwenden. Aber auch diese muss man wohl in "WEB-INF\lib" kopieren - tut man das nicht, kann Eclipse zumindest die Tags in der JSP-Seite nicht auflösen.
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, Eclipse 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"
Es wird eine Java-Klasse "GeometricModelHandler" zugefügt.
Sie benötigt folgende Definition:
import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
@Named(value="geometricModelHandler")
@SessionScoped
public class GeometricModelHandler implements Serializable
{
Das Interface
java.io.Serializable
muss implementiert werden, da die Bean im Session Scope liegen soll. Soll sie im Request Scope liegen, ist dies nicht nötig.
Tut man das nicht, führt es zu folgender Fehlermeldung beim Deploy:
...
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-000072: Bean declaring a passivating scope must be passivation capable. Bean: Managed Bean [class de.fhw.komponentenarchitekturen.knauf.jsf.GeometricModelHandler] with qualifiers [@Default @Any @Named]
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.ManagedBean.checkType(ManagedBean.java:220)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.AbstractBean.initializeAfterBeanDiscovery(AbstractBean.java:108)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.ManagedBean.initializeAfterBeanDiscovery(ManagedBean.java:124)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:111)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:102)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:62)
at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:55)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
Rückblick auf die Deklaration von Managed Beans in früheren JSF-Versionen: Dafür gab es zwei Möglichkeiten:
Variante 1:
Dies konnte über die Annotation
javax.faces.bean.ManagedBean
erfolgen. Der in diesem Beispiel verwendete Session Scope wurde mittels
javax.faces.bean.SessionScoped
definiert:
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean(name="geometricModelHandler")
@SessionScoped()
public class GeometricModelHandler
{
Variante 2:
Es konnte über einen Eintrag in "faces-config.xml" erfolgen:
<managed-bean>
<description>
This Managed Bean performs the calculation of surface and volume of the cube.
It stores a list of all user inputs for the current session.
</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 History history = new History();
public History getHistory()
{
return this.history;
}
public double getVolume()
{
return this.dblVolume;
}
public double getSurface()
{
return this.dblSurface;
}
Schließlich wird die Methode calculate
zugefügt, die beim Klick auf "Submit" aufgerufen wird und
die Berechnung durchführt sowie die aktuelle Berechnung der Historie zufügt.
public String calculate()
{
//Calculate the values and store in member variables:
this.dblVolume = this.dblA * this.dblB * this.dblC;
this.dblSurface = 2 * (this.dblA * this.dblB) + 2 * (this.dblA * this.dblC) + 2 * (this.dblB * this.dblC);
//Add to history
SideLengths sideLengthCurrent = new SideLengths();
sideLengthCurrent.setA(this.dblA);
sideLengthCurrent.setB(this.dblB);
sideLengthCurrent.setC(this.dblC);
this.history.addSideLength(sideLengthCurrent );
//Return value does not matter, because this simple sample has no navigation rules.
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 "History"
In der Klasse "History", in der die bereits durchgeführten Berechnungen abgelegt werden, ergeben sich zwei kleine Änderungen:
public List<SideLengths> getSideLengths()
{
return this.vectorSideLengths;
}
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).
getSideLengths
liefert die SideLengths
-Objekte als generic Liste zurück. Diese wird später über das JSF-Tag
<h:dataTable>
durchlaufen.
Facelet
Wir fügen ein Facelet "index.xhtml" zu.
Die Seite verwendet die JSTL-Core-Library.
Sie sieht so aus:
<?xml version="1.0" encoding="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"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Simple JSF sample</title>
</head>
<body>
<f:view>
<h:form id="formGeometricModelInput">
<h:panelGrid columns="3">
<c:if test="#{geometricModelHandler.volume > 0.0}">
<h:outputText value="Volume:"/>
<h:outputText id="volume" value="#{geometricModelHandler.volume}"></h:outputText>
<h:outputText value=""></h:outputText>
<h:outputText value="Surface:"/>
<h:outputText id="surface" value="#{geometricModelHandler.surface}"></h:outputText>
<h:outputText value=""></h:outputText>
</c:if>
<h:outputText value="Side a:"></h:outputText>
<h:inputText label="Side A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:message for="a"></h:message>
<h:outputText value="Side b:"></h:outputText>
<h:inputText label="Side B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:message for="b"></h:message>
<h:outputText value="Side c:"></h:outputText>
<h:inputText label="Side C" id="c" value="#{geometricModelHandler.c}"></h:inputText>
<h:message for="c"></h:message>
<h:commandButton id="calculate" value="Calculate" action="#{geometricModelHandler.calculate}"></h:commandButton>
<h:outputText value=""></h:outputText>
<h:outputText value=""></h:outputText>
<ui:remove>Print history:</ui:remove>
<c:if test="#{geometricModelHandler.history.size > 0}">
<ui:remove>A DataTable with three columns will contain all calculations of the current session.
The current iteration element will be put in a variable named "sideLengthCurrent"</ui:remove>
<h:dataTable value="#{geometricModelHandler.history.sideLengths}" var="sideLengthCurrent">
<h:column>
<f:facet name="header">
<h:outputText value="A"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.a}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="B"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.b}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="C"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.c}"></h:outputText>
</h:column>
</h:dataTable>
</c:if>
</h:panelGrid>
</h:form>
</f:view>
</body>
</html>
Die Elemente im Einzelnen:
- In der ersten Zeile wird das Encoding der XML-Datei definiert, das wohl wichtig ist, falls die Datei nicht in UTF-8 gespeichert wird (in meinem Beispiel wird ISO8859-1 verwendet):
<?xml version="1.0" encoding="ISO-8859-1"?>
Ohne diese Encoding-Definition wird es zu einer Fehlermeldung beim Aufruf der Seiten kommen, sobald z.B. Umlaute verwendet werden (hier wird nur die eigentliche innere Exception dargestellt):
Caused by: javax.faces.view.facelets.FaceletException: Error Parsing /index.xhtml: Error Traced[line: 26] Ungültiges Byte 2 von 3-Byte-UTF-8-Sequenz.
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.compiler.SAXCompiler.doCompile(SAXCompiler.java:445)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.compiler.SAXCompiler.doMetadataCompile(SAXCompiler.java:428)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.compiler.Compiler.metadataCompile(Compiler.java:88)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory.createMetadataFacelet(DefaultFaceletFactory.java:470)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory.access$200(DefaultFaceletFactory.java:68)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory$2.newInstance(DefaultFaceletFactory.java:177)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory$2.newInstance(DefaultFaceletFactory.java:174)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletCache$2.newInstance(DefaultFaceletCache.java:76)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletCache$2.newInstance(DefaultFaceletCache.java:69)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.util.ExpiringConcurrentCache$1.call(ExpiringConcurrentCache.java:76)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.util.ExpiringConcurrentCache.get(ExpiringConcurrentCache.java:91)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletCache.getViewMetadataFacelet(DefaultFaceletCache.java:124)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletCache.getViewMetadataFacelet(DefaultFaceletCache.java:39)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory.getMetadataFacelet(DefaultFaceletFactory.java:279)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.facelets.impl.DefaultFaceletFactory.getMetadataFacelet(DefaultFaceletFactory.java:209)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.application.view.ViewMetadataImpl.createMetadataView(ViewMetadataImpl.java:122)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:224)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.lifecycle.Phase.doPhase(Phase.java:76)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:110)
at com.sun.jsf-impl@2.3.17.SP01//com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:177)
at javax.faces.api@3.1.0.SP01//javax.faces.webapp.FacesServlet.executeLifecyle(FacesServlet.java:707)
... 50 more
Der Grund dürfte wohl sein, dass beim Parsen der XHTML-Datei das Default-Encoding "UTF-8" verwendet wird, und hier würden Zeichen über ASCII-Index 128 anders interpretiert.
Das XML-Encoding muss natürlich mit dem Encoding der Ergebnis-HTML-Seite übereinstimmen.
- Es werden drei Taglibraries eingebunden: JSF-HTML, JSF-Core, JSF-UI und JSTL-Core. Für diese werden XML-Namespace-Aliasse definiert:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core">
- Es wird eine JSF-View deklariert (hiervon darf es pro Seite nur eine geben), die ein JSF-Formular enthält:
<f:view>
<h:form id="formGeometricModelInput">
...
</h:form>
</f:view>
Das "id"-Attribut des "h:form"-Tags ist nötig, weil dieses Beispiel auch für einen JSFUnit-Test verwendet wird.
- 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 der @Named
-Annotation
deklarierten Namen vorliegt.
<c:if test="#{geometricModelHandler.volume > 0.0}">
...
</c:if>
Hier gibt es einen Unterschied zur vorherigen JSF 1-Variante, die die Bean mittels der veralteten
@ManagedBean
bzw. über "faces-config.xml" definierte:
Dort hätte ich im EL-Ausdruck explizit auf den Session Scope zugreifen können:
<c:if test="#{sessionScope.geometricModelHandler.volume > 0.0}">
...
</c:if>
Durch die CDI Annotations findet sich die Bean nicht in der impliziten Variable "sessionScope", aber irgendwie funktioniert der Lookup trotzdem und sie wird auch korrekt abgerufen.
- Innerhalb dieser If-Prüfung werden die aktuellen Werte der ManagedBean "geometricModelHandler" ausgegeben:
<h:outputText value="Volume:"/>
<h:outputText id="volume" value="#{geometricModelHandler.volume}"></h:outputText>
<h:outputText value=""></h:outputText>
<h:outputText value="Surface:"/>
<h:outputText id="surface" value="#{geometricModelHandler.surface}"></h:outputText>
<h:outputText value=""></h:outputText>
Die "id"-Attribute der Ausgabetexte erklären sich, weil dieses Beispiel in einem späteren Beispiel für einen JSFUnit-Test genutzt wird.
Kombination von JSTL und JSF
Man beachte hier die Kombination eines JSTL-Tags (ein Ausdruck beginnend mit "$") und der JSF-Komponenten, deren Ausdrücke mit "#" beginnen.
Der wichtige Unterschied ist der Verarbeitungszeitpunkt der Tags. Eine JSF-Seite hat zwei "life cycles": den Compilierungs-Zyklus
und den JSF-Zyklus. Im Compilierungs-Zyklus wird der Komponenten-Baum erstellt, auf dem die Verarbeitung der JSF-Seite bei einem
konkreten Request erfolgt. Dies erfolgt beim Aufruf der JSF-Seite (also nicht wie bei einer JSP-Seite einmalig beim ersten Aufruf).
Danach erfolgt der restliche JSF-Lifecycle.
Es zwei unterschiedliche Typen von (JSF-) Tags: "TagHandler" und "ComponentHandler". Erstere werden im Compile-Zyklus
ausgeführt, während letztere im JSF-Lifecycle ausgeführt werden. Zur Kategorie der "TagHandler" gehören die JSTL-Tags (vermutlich
alle Tags aus JSP-Taglibraries), aber auch einige wenige JSF-Tags, z.B. "ui:include". Zur Kategorie der "ComponentHandler" gehören
die meisten JSF-Tags.
Daraus ergibt sich: JSTL-Tags werden zur Compilierungszeit der JSF-Seite verarbeitet. Sind in ihnen JSF-Komponenten enthalten, kann
durch das JSTL-Tag gesteuert werden, ob die JSF-Komponente überhaupt dem Komponenten-Baum zugefügt wird, oder ob sie sogar mehrfach
zugefügt wird.
Das kann zu verwirrenden Seiteneffekten führen. Ich denke gültig ist meist die Kombination "ein JSTL-Tag klammert eine JSF-Komponente",
denn hier wird der JSTL-Ausdruck für dem Ausdruck der JSF-Komponente ausgeführt (siehe mein obiges Beispiel, das die JSF-Ausgabefelder
komplett unterdrückt, wenn eine Bedingung nicht vorhanden ist.
Umgekehrt könnte z.B. ein "c:if"-Tag in eine Zeile eines "h:dataTable" gesetzt sein. Hier wird die Bedingung des "c:if" zuerst ausgeführt,
und der Inhalt der Datatable-Zeile wird anhand dieses einmaligen Ausführens des "c:if" bestimmt und dem Komponentenbaum zugefügt oder nicht.
Erst später, im JSF-Lebenszyklus, wird der "h:dataTable" ausgeführt und n Tabellenzeilen je nach zu durchlaufender Datenquelle erzeugen.
Der Inhalt aller Zeilen ist aber schon durch das initiale "c:if" bestimmt!
Quelle 1:
http://www.znetdevelopment.com/blogs/2008/10/18/jstl-with-jsffacelets/
Quelle 2:
https://stackoverflow.com/questions/3342984/jstl-in-jsf2-facelets-makes-sense/
Von dort zitiere ich folgende Aussage:
JSTL runs from top to bottom first, producing the JSF component tree, then it's JSF's turn to run from top to bottom again, producing the HTML output.
In meinem Beispiel wäre eine alternative Möglichkeit (unter Vermeidung des JSTL-Tags "c:if") das "rendered"-Attribut:
<h:outputText value="Volume:" rendered="#{geometricModelHandler.volume > 0.0}"/>
<h:outputText id="volume" value="#{geometricModelHandler.volume}" rendered="#{geometricModelHandler.volume > 0.0}"></h:outputText>
<h:outputText value="" rendered="#{geometricModelHandler.volume > 0.0}"></h:outputText>
<h:outputText value="Surface:" rendered="#{geometricModelHandler.volume > 0.0}"/>
<h:outputText id="surface" value="#{geometricModelHandler.surface}" rendered="#{geometricModelHandler.volume > 0.0}"></h:outputText>
<h:outputText value="" rendered="#{geometricModelHandler.volume > 0.0}"></h:outputText>
Geschickter wäre natürlich, diesen Bereich in ein Panel zu setzen und dieses als ganzes ein-/auszuschalten.
- 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="Side a:"></h:outputText>
<h:inputText label="Side A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
<h:message for="a"></h:message>
<h:outputText value="Side b:"></h:outputText>
<h:inputText label="Side B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
<h:message for="b"></h:message>
<h:outputText value="Side c:"></h:outputText>
<h:inputText label="Side 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: "Side 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 "index.xhtml" 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=formGeometricModelInput:a[severity=(ERROR 2), summary=(Side A: 'a' must be a number consisting of one or more digits.), detail=(Side A: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:
"Side 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="calculate" value="Calculate" action="#{geometricModelHandler.calculate}"></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.sideLengths}" var="sideLengthCurrent">
...
</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 SideLengths
-Objekt landet.
- Jetzt werden die Tabellen-Inhalte ausgegeben:
<h:column>
<f:facet name="header">
<h:outputText value="A"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.a}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="B"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.b}"></h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="C"/>
</f:facet>
<h:outputText value="#{sideLengthCurrent.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 "sideLengthCurrent", es werden in den drei Spalten die Properties
"a", "b" und "c" abgerufen.
- Hinweis - Kommentare:
In JSP-Seiten können Kommentare mit der Syntax "<%-- --%>
" verwendet werden. In Facelets funktioniert dies nicht - führt zu Syntax-Fehlern.
Auch HTML-Kommentare (<!-- -->
) funktionieren nicht. Es gibt zwar keine Fehlermeldung, aber die Inhalte der Kommentare werden als JSF-Komponenten gerendert,
und dies kann z.B. zu leeren Tabellenzellen führen, die das Layout des "panelGrid" stören würden.
Es gibt Lösungen:
Variante 1: das Tag "ui:remove" verwenden - dies erzeugt eine JSF-Komponente, die nicht gerendert wird:
<ui:remove>Print history:</ui:remove>
Variante 2: In "web.xml" den Parameter "facelets.SKIP_COMMENTS" auf "true" setzen:
<context-param>
<param-name>facelets.SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
Beim Deploy wird die Anwendung folgende Fehlermeldung anzeigen:
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) javax.faces.FacesException: Unable to find CDI BeanManager
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) at com.sun.jsf-impl@2.3.14.SP04//com.sun.faces.application.applicationimpl.Version.isJsf23(Version.java:62)
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) at com.sun.jsf-impl@2.3.14.SP04//com.sun.faces.application.applicationimpl.ExpressionLanguage.addELResolver(ExpressionLanguage.java:140)
...
Lösung:
Es muss eine Datei "WEB-INF\beans.xml" mit folgendem Inhalt angelegt werden:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
version="2.0" bean-discovery-mode="annotated">
</beans>
Siehe https://github.com/eclipse-ee4j/mojarra/blob/2.3/README.md#user-content-activating-cdi-in-jakarta-faces-23:
Auch wenn in "faces-config.xml" die Version 2.3 eingetragen ist, läuft JSF trotzdem teilweise im 2.2-Modus. Um dies zu umgehen, muss irgendwo
bei einer Klasse die entsprechende JSF-Version per Annotation deklariert werden.
Der Übersichtlichkeit halber habe ich das auf einer eigenen Klasse "Jsf23Activator" getan:
package de.fhw.komponentenarchitekturen.knauf.jsf;
import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;
@ApplicationScoped
@FacesConfig(version = FacesConfig.Version.JSF_2_3)
public class Jsf23Activator {
}
Meine Beispiel nutzen zwar keine Features, für die dies nötig wäre, aber ich erkläre diese Lösung trotzdem.
Startseite
Die Anwendung liegt in einer Datei "index.xhtml". Diese ist nicht als "Welcome file" registriert, deshalb kann man die Anwendung nicht ohne Angabe einer Unterseite aufrufen.
Deshalb wird sie in "web.xml" zugefügt:
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
Die Anwendung findet sich unter dieser URL: http://localhost:8080/JSF/index.xhtml.
JSF-Facet zu bestehendem Projekt zufügen
Falls man bereits ein Webprojekt ohne JSF-Unterstützung hat (z.B. nach dem Import der WAR-Datei dieses Beispiels), lässt 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 "2.3". Außerdem
prüft man, ob die Facet "Dynamic Web Project" die richtige Version hat.
Wie beim Erzeugen des Projekts wird die Variante "Library Provided by Target Runtime" gewählt.
Falls es bereits eine "faces-config.xml" im Projekt gibt, sollte man
außerdem die Checkbox "Configure JSF servlet in deployent descriptor" zurücksetzen.
Stand 06.01.2023
Historie:
25.12.2022: Erstellt aus vorherigem Beispiel, das den veralteten Ansatz einer JSP-Seite verwendete.
06.01.2023: @ManagedBean
durch CDI ersetzt.