Jakarta Faces (Basics) (JakartaEE 10)


Inhalt:

Projekt erstellen
ManagedBean "GeometricModelHandler"
Hilfsklasse "History"
Facelet
"beans.xml"
Startseite
JSF-Facet zu bestehendem Projekt zufügen


Für WildFly 33 und JakartaEE 10: Dieses Beispiel zeigt eine Verwendung von "Jakarta Faces". Es 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.

"Jakarta Faces" hieß vor Version 4.0 des Standards "Java Server Faces" (abgekürzt "JSF"), deshalb kann dieser alte Begriff hier noch auftauchen.

Hier gibt es das Projekt als WAR-Export-Datei: FacesBasics.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 JakartaEE10-Tutorial ("Jakarta EE Web Profile" -> "Jakarta Faces" -> "Jakarta Faces Technology") verwiesen: https://jakarta.ee/learn/docs/jakartaee-tutorial/current/web/faces-intro/faces-intro.html
Hier finden sich die Spezifikationen für Jakarta Faces 4.0 (unter "Details" sind HTML und PDF verlinkt): https://jakarta.ee/specifications/faces/4.0/



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, die 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.

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 "6.0" gestellt.
Neues Projekt (1)

Dieser Abschnitt ist der JakartaEE8-Version des Beispiels entnommen, Eclipse 2024-06 unterstützt noch keine Facet "JavaServer Faces" in Version 4.0 (https://bugs.eclipse.org/bugs/show_bug.cgi?id=577597), deshalb können diese Schritte aktuell ignoriert werden.


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.
JavaServer Faces Configuration
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:
Neues Projekt (2)

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":
Neues Projekt (3)

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>

Hier müssen wir die "servlet-class" ändern auf die JakartaEE10-Version:
		<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
Da wir die JSTL-Library verwenden wollen (Version 3.0), werden die Dateien "jakarta.servlet.jsp.jstl-api-3.0.0.jar" von https://repo.maven.apache.org/maven2/jakarta/servlet/jsp/jstl/jakarta.servlet.jsp.jstl-api/3.0.1/ und "jakarta.servlet.jsp.jstl-3.0.1.jar" von https://repo.maven.apache.org/maven2/org/glassfish/web/jakarta.servlet.jsp.jstl/3.0.1/ heruntergeladen und nach "WEB-INF\lib" kopiert, siehe JSP3-Beispiel. Alternativ könnte man auch die in WildFly enthaltenen Datei verwenden und diesen Schritt überspringen.


ManagedBean "GeometricModelHandler"

Es wird eine Java-Klasse "GeometricModelHandler" zugefügt.
Sie benötigt folgende Definition:
import java.io.Serializable;
import jakarta.enterprise.context.SessionScoped;
import jakarta.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.hsrm.jakartaee.knauf.jsf.GeometricModelHandler] with qualifiers [@Default @Any @Named]
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.bean.ManagedBean.checkType(ManagedBean.java:224)
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.bean.AbstractBean.initializeAfterBeanDiscovery(AbstractBean.java:108)
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.bean.ManagedBean.initializeAfterBeanDiscovery(ManagedBean.java:127)
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:113)
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:104)
	at org.jboss.weld.core@5.1.2.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:62)
	at org.jboss.weld.core@5.1.2.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.hsrm.jakartaee.knauf.facesbasics.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="jakarta.faces.facelets"
      xmlns:f="jakarta.faces.core"
      xmlns:h="jakarta.faces.html"
      xmlns:c="jakarta.tags.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:


"beans.xml"

In früheren JSF-Versionen kam beim Deploy der Anwendung folgende Fehlermeldung, dies scheint in meinem JakartaEE10-Beispiel nicht mehr nötig zu sein. Trotzdem belasse ich die Datei "beans.xml", da sie an anderer Stelle sinnvoll sein kann.


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 version="4.0"
   xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
   bean-discovery-mode="annotated">
</beans>


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/FacesBasics/index.xhtml.


JSF-Facet zu bestehendem Projekt zufügen

Dieser Abschnitt ist der JakartaEE8-Version des Beispiels entnommen, Eclipse 2024-06 unterstützt noch keine Facet "JavaServer Faces" in Version 4.0, deshalb können diese Schritte aktuell ignoriert werden.

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.
Facet zufügen (1)
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.
Facet zufügen (2)



Stand 01.09.2024
Historie:
01.09.2024: Erstellt aus JavaEE8-Beispiel.