Blog-Archiv

Samstag, 24. August 2019

JPA 2.1 with JSF 2.3 Example Application

This is about setting up an Eclipse project featuring JSF 2.3 and JPA 2.1. Generally it is the same procedure as in my latest Blog, but I encountered some Eclipse configuration problems when I tried to integrate JPA, so I will repeat the setup steps before introducing the example sources.

Goal is a web-page with a text-field where you can enter your favourite greetings. On submit (ENTER key or button click) your input will be saved to a database table called GREETING. As response your input will be displayed in a list below the input field. It will look like the following:

The H2 database console shows the persisted inputs:

Tools

I used the newest J2EE Eclipse 2019-06 (4.12.0) as Java IDE, and the newest Tomcat 9 as web-server.

The database is H2, in server-mode, that means I need to start it separately before I run the web-server in Eclipse. H2 has a nice startup script that opens a browser tab where you can run SQL queries and commands as user "sa" with password "".

Setup

You may encounter different Eclipse error messages during the following setup. Menu "Project" - "Clean" often helps, or project context menu "Refresh", or "Maven" - "Update Project". But you could choose to ignore all errors until you created the whole application, that's what I would recommend. Else please refer to the web by entering the error message in some search engine and following the instructions there. I spent a lot of time with that but can't document all here.

Following shows the file structure I am about to generate:

1) Create a "New Maven Project" without archetype

Eclipse menu "File" - "New" - "Project", choose "Maven" - "Maven Project", click "Next".
Activate "Create a simple project" checkbox, click "Next".
Enter your Maven-coordinates, I used "fri.jsf" as group-id and "jsfJpa" as artifact-id, mind that these will be in the generated pom.xml file that will be edited in next step.
Choose "war" for packaging.
Click "Finish".

2) Edit Maven pom.xml

Put following into the generated pom.xml file in project root directory. Adapt groupId and artifactId to the ones you entered in previous step when generating the project.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  
  <groupId>fri.jsf</groupId>
  <artifactId>jsfJpa</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <packaging>war</packaging>
  
  <description>JPA binding for JSF</description>

  <dependencies>
    <!-- START JSF deps -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.faces</artifactId>
      <version>2.3.9</version>
    </dependency>
    
    <dependency>
      <groupId>org.jboss.weld.servlet</groupId>
      <artifactId>weld-servlet-shaded</artifactId>
      <version>3.1.2.Final</version>
    </dependency>
    <!-- END JSF deps -->

    <!-- START JPA deps -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>5.4.4.Final</version>
    </dependency>

    <!-- JDBC driver for some database like H2 -->
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.4.199</version>
      <scope>runtime</scope>
    </dependency>
    <!-- END JPA deps -->
    
    <!-- Test scope -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13-beta-3</version>
      <scope>test</scope>
    </dependency>

  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
      
    </plugins>

  </build>

</project>

This uses Hibernate as JPA provider ("hibernate-entitymanager"). You could choose any other by simply replacing the dependency. Further you should replace the H2 database driver dependency with the one that fits to your local database.

Do not yet run a Maven build!

3) In "Project Properties", click on "Project Facets"

Activate

  • "Dynamic Web Module" with version 4.0,
  • "Java" with 1.8,
  • "JavaServer Faces" with 2.3
  • "JPA" with 2.1

Click "Apply and Close". It may happen that you have to first activate "Dynamic Web Module" 4.0, then "Apply and Close", then open the dialog again and activate JSF 2.3.

In the end you need the state as shown in this screenshot:

If something doesn't work out here, Eclipse writes to .settings/org.eclipse.wst.common.project.facet.core.xml, you can try to edit that file and "Refresh" the project after.

4) Run Maven Build

In Project Explorer, select the project and right-mouse click context menu "Maven" - "Update Project". This will build the project and refresh Eclipse after.

5) Configure Eclipse Project

All of the following happens in project "Properties" dialog that you can launch with context menu on the project in Project Explorer.

Configure JPA to "Discover annotated classes automatically", and "Disable Library Configuration":

Also "Disable Library Configuration" for "Java Server Faces" under "Project Facets":

Configure Maven to NOT "Resolve dependencies from Workspace":

Make sure "Maven Dependencies" are in JavaBuild Path:


6) Configuration Files for JSF

Expand the path src/main/webapp/WEB-INF/ and add following files there, overwriting existing ones:

web.xml

<?xml version="1.0"?>
<web-app 
        xmlns="http://java.sun.com/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/web-app_4_0.xsd" 
        version="4.0">
    
    <servlet>
    <servlet-name>JSF JPA Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    </servlet>
  
    <servlet-mapping>
    <servlet-name>JSF JPA Servlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
  
</web-app>

faces-config.xml

<?xml version="1.0"?>
<faces-config
        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/web-facesconfig_2_3.xsd"
        version="2.3">

</faces-config>

beans.xml

<?xml version="1.0"?>
<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="all">

</beans>

In src/main/java/ create a package fri/jsf/ (or any package you like) and add following Java source (the @FacesConfig annotation is needed by JSF 2.3):

ApplicationFacesConfig.java

package fri.jsf;

import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;

@FacesConfig
@ApplicationScoped
public class ApplicationFacesConfig
{
}

7) Configuration File for JPA

I need to tell the JDBC driver how to connect to my database. In src/main/resources/META-INF/ add following file, again overwriting the existing one (it could happen that an already existing one is in src/main/java/META-INF/, then overwrite this one):

persistence.xml

<?xml version="1.0"?>
<persistence
        xmlns="http://xmlns.jcp.org/xml/ns/persistence"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
             http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
        version="2.1">

    <persistence-unit name="JsfJpaPU">
    
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test" />
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.user" value="jsfjpa" />
            <property name="javax.persistence.jdbc.password" value="jsfjpa" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            
            <!-- Scan for classes annotated by @Entity on startup, instead of hardcoding all classes here -->
            <property name="hibernate.archive.autodetection" value="class" />
            
            <!-- Auto-drop and -recreate the database tables on startup (not the database schema!) -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />
            <!-- Never do this in production mode! -->
            
            <!-- Display all database statements on console log -->
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            
        </properties>
        
    </persistence-unit>

</persistence>

I use JPA property names as much as possible here, but some have to be Hibernate because JPA does not cover their functionality. You would have to change vendor-specific property names when using another JPA-provider.

Application Sources

Now I create the example application. Following files should go to src/main/java/. In package fri/jsf I create a sub-package jpa and put following sources into it:

Greeting.java (the entity-bean, mapped to a database table)

package fri.jsf.jpa;

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(schema = "jsfjpa")
public class Greeting implements Serializable
{
  @Id
  @GeneratedValue
  private long id;
    
  private String text;
    
  public long getId() {
    return id;
  }
  public void setId(long id) {
    this.id = id;
  }

  public String getText() {
    return text;
  }
  public void setText(String text) {
    this.text = text;
  }
}

Mind that no public setter or getter in an entity must be final (see JPA specification).

Mind further that the entity will be saved into an explicitly given database schema "jsfjpa", defined by the @Table annotation. Declaring this here may not be a good idea (because I would have to duplicate the schema name on every entity), but I want to try out if the table actually goes to that schema.

GreetingModel.java (the DAO)

package fri.jsf.jpa;

import java.util.Collection;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class GreetingModel
{
  public static final String JPA_UNIT = "JsfJpaPU"; // from META-INF/persistence.xml
 
  private static final EntityManager em = 
      Persistence.createEntityManagerFactory(JPA_UNIT).createEntityManager();

  public Collection<Greeting> readGreetings() {
    final EntityTransaction tx = em.getTransaction();
    tx.begin();
    Collection<Greeting> greetings = em.createQuery("select g from "+Greeting.class.getName()+" g", Greeting.class).getResultList();
    tx.commit();
    return greetings;
  }
 
  public void saveGreeting(Greeting greeting) {
    final EntityTransaction tx = em.getTransaction();
    tx.begin();
    em.persist(greeting);
    tx.commit();
  }
}

The model encapsulates persistence access. Mind that JPQL (JPA query language) requires the "select" keyword (unlike Hibernate-QL), and it requires an alias name for each entity-type occurring in the query.

The following belongs to JSF, not JPA, so put it into package src/main/java/fri/jsf:

GreetingController.java ("backing bean")

package fri.jsf;

import java.util.Collection;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import fri.jsf.jpa.Greeting;
import fri.jsf.jpa.GreetingModel;

@Named
@RequestScoped
public class GreetingController
{
  private Greeting greeting = new Greeting();
 
  public Greeting getGreeting() {
    return greeting;
  }

  public Collection<Greeting> getGreetings() {
    return getModel().readGreetings();
  }

  public String addGreeting() {
    String followerPage = "";
    if (greeting.getText().trim().length() > 0) {
      followerPage = saveGreeting(greeting);
      greeting = new Greeting();
    }
    return followerPage;
  }
 
  private String saveGreeting(Greeting greeting) {
    getModel().saveGreeting(greeting);
    return "hello-world?faces-redirect=true";
    // POST-redirect-GET to avoid duplicates on browser-refresh by user
  }
 
  private GreetingModel getModel() {
    return new GreetingModel();
  }
}

The controller connects model and view.

I have a model with an entity, I have a controller, now I will create a JSF-view in src/main/webapp:

hello-world.xml

!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
>

  <h:head>
      <title>Hello JSF + JPA</title>
  </h:head>
 
  <h:body>
  
    <h:form>
      <h:outputLabel for="greeting">Your Greeting:</h:outputLabel>
      <h:inputText id="greeting" value="#{greetingController.greeting.text}"/>
   
      <h:commandButton value="Submit" action="#{greetingController.addGreeting()}"/>
      <!--  This will submit every field inside the h:form -->
    </h:form>
  
    <ui:repeat value="#{greetingController.greetings}" var="greeting">
      <li>#{greeting.text}</li>
    </ui:repeat>
  
     
  </h:body>

</html>

This view has a form with one labeled input field for the greeting and a submit-button (h:commandButton). On bottom it has an output "repeater" that renders the list of greetings done so far.

The "value" attribute of the h:inputText field defines the content of the text field (like it is with HTML INPUT). It is bound to the expression value="#{greetingController.greeting.text}" (JSF expression language). This refers to a bean of class "GreetingController" (defaults to "greetingController" with first letter upper-case, or name of the Java-bean). JSF instantiates it (HTTP-request scoped!) and accesses its property "greeting", and from here the property "text", which is represented by the Greeting.getText() Java method. Because the Greeting.text property has no default value, the text field is empty.

On submit of the h:form, the text field's current content is written to GreetingController.greeting.setText(} method through a HTTP-POST. The action="#{greetingController.addGreeting()}" of the submit-button then calls the GreetingController.addGreeting(} method that saves the Greeting built in GreetingController to the according database table.

The response is the return of addGreeting(}, which is the same page but with a new Greeting instance, so the text field appears empty again. Mind that the follower-page is determined by a method call to a Java class, nevertheless it also could be hardcoded in the XHTML view.

The output-repeater on bottom renders all greetings through the binding value="#{greetingController.greetings}". The "var" attribute names a variable that is used to render a Greeting inside the loop body: #{greeting.text} - "greeting" is a variable name in this case.

The POST-Redirect-GET pattern (see GreetingController.saveGreeting) is to avoid duplicate greetings caused by the user clicking browser-"Refresh" after submitting. In JSF you can implement that by appending ?faces-redirect=true to the action response, easy to do as long as the response page needs no parameters. Try to reproduce the duplication by removing the faces-redirect=true, then submitting a greeting, then doing a browser "Refresh". Although the text field is empty, the previous input will be added again. It is in the recent POST used by the browser to redisplay the page. Mind that this "double submit" problem is not solved when the second POST arrives before the browser received the response of the first POST.

Build Application

Finally you must build the project with Maven. Select the project in Project Explorer, right click context menu "Maven" - "Update Project". I hope all Eclipse errors are gone now!

Run Application

First you need to run your H2 database server by executing its bin/h2.sh (UNIX) or bin/h2.bat (WINDOWS) script. This will open a browser tab, use this database view to launch following statements:

CREATE USER jsfjpa PASSWORD 'jsfjpa'

In persistence.xml I configured a database user "jsfjpa" with password "jsfjpa". Thus I must actually create it now. Mind that this is a H2-specific statement!

CREATE SCHEMA jsfjpa AUTHORIZATION jsfjpa

JPA won't create the schema given in Greeting.java (see @Table annotation) automatically. But it is configured to drop and create all tables on startup (property javax.persistence.schema-generation.database.action in persistence.xml), so it will fail and throw an exception when I don't provide the schema now.

Now go to Eclipse and select hello-world.xml in Project Explorer and right click context menu "Run As" - "Run on Server". You probably will have to select Tomcat 9 from a list if you have installed several servers. Finally you should see Eclipse open a browser page with the JSF/JPA application like shown in the screenshot at start of this article.

You can add greetings now, and check that they actually get stored to the database.

Conclusion

JSF is a big and complex thing, so this article got too long. I find the JSF ways quite elegant, but I have a bad feeling when I see that navigation (determining a follower page) can be done in all of Java-controller, XHTML-view, and faces-config.xml.




Keine Kommentare: