Blog-Archiv

Dienstag, 3. September 2019

JSF Master Detail Example with JPA Database

This is a small example application implemented with JSF and JPA database integration. The "master-details" view is an application archetype. Master is a paged read-only list of a possibly big number of items, selecting an item shows the details view of the item, mostly editable. The list is stateless, the detail editor can be committed or cancelled. This example "navigates" to the detail-view, meaning master and detail are not shown side-by-side at the same time.

The application is pure JSF without PrimeFaces or BootsFaces or OmniFaces or ..., with a minimum of CSS styling, without internationalization, keeping the sources short but deep. It features a standard web application architecture comprising presentation layer, service layer, and model layer.

Screenshots

This is how the app will look like. First the read-only "master" view:

When you trigger "Add", or click "Edit" in some row, the "details" editor shows:

You can cancel or save inputs, both going back to the master view.
When you click "Delete" in some row on the master view, a JavaScript confirm-dialog shows:

Setup

Please read about a JSF 2.3 + JPA 2.1 setup with Eclipse 4.12.0 in my latest Blog.

Project Structure


Sources

The Maven artifactId in pom.xml of this project is jsfJpaCrud. The WEB-INF/beans.xml, WEB-INF/faces-config.xml and ApplicationFacesConfig.java configuration files are unchanged and empty, also META-INF/persistence.xml is unchanged. You can find these files on my latest Blog. All others follow now.

Servlet Configuration

WEB-INF/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">
  
  <welcome-file-list>
    <welcome-file>todos.xhtml</welcome-file>
  </welcome-file-list>
   
  <servlet>
    <servlet-name>JSF JPA Servlet CRUD Todolist</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>JSF JPA Servlet CRUD Todolist</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>

  <!-- avoid ViewExpiredException -->
  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>

  <!-- Force JSF to use local time when converting -->
  <context-param>
    <param-name>javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name>
    <param-value>true</param-value>
  </context-param>
   
</web-app>

I switched to STATE_SAVING_METHOD = client (non-default!) to avoid ViewExpiredException.

The DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE = true parameter was necessary to get local time from JSF date/time converters.

1) Model Layer

Here is the entity representing a "To Do" item, that's what this application is all about.

Todo.java

package fri.jsf.todos.model;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Todo implements Serializable
{
    public enum State
    {
        NEW,
        RUNNING,
        DONE,
        CANCELED,
    }
    
    @Id @GeneratedValue
    private Long id;
    private String description;
    private String outcome;
    private Date createdAt = new Date();
    private State state = State.NEW;
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }

    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    
    public String getOutcome() {
        return outcome;
    }
    public void setOutcome(String outcome) {
        this.outcome = outcome;
    }

    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public State getState() {
        return state;
    }
    public void setState(State state) {
        this.state = state;
    }
}

UI challenges will be the enum and the date/time fields. Mind that field default values can be set directly here.

What is missing here are @NotNull annotations and other validation rules on the fields. For that I would have to add validation-libraries to the Maven project pom.xml, there is an API for bean-validation, and Hibernate provides an implementation for it.

2) Presentation Layer

Master Page

todos.xhtml (MVC-view, the chooser list)

<!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"
>
    <h:head>
        <title>Create and Delete ToDos</title>
        <h:outputStylesheet library="css" name="todos.css"/>
    </h:head>
    
    <h:body>
        <h1>To Do List</h1>
        
        <h:button value="Add" outcome="editTodo"/>
            
        <h:dataTable value="#{todosController.todos}" var="todoItem"
                headerClass="tableHeader" rowClasses="tableRowOdd,tableRowEven" cellpadding="6">
                
            <h:column><f:facet name="header">Description</f:facet>
                <h:inputTextarea value="#{todoItem.description}" title="#{todoItem.description}"
                    readonly="true" rows="1"/>
            </h:column>
            
            <h:column><f:facet name="header">State</f:facet>
                #{todoItem.state}
            </h:column>
            
            <h:column><f:facet name="header">Outcome</f:facet>
                <h:inputTextarea value="#{todoItem.outcome}" title="#{todoItem.outcome}"
                    readonly="true" rows="1"/>
            </h:column>
            
            <h:column><f:facet name="header">Created At</f:facet>
                <h:outputText value="#{todoItem.createdAt}">
                    <f:convertDateTime pattern="yyyy-MM-dd HH:mm"/>
                </h:outputText>
            </h:column>
            
            <h:column>
                <h:link outcome="editTodo?id=#{todoItem.id}">Edit</h:link>
            </h:column>
            
            <h:column>
                <h:form>  <!-- command* needs to be inside form, required for method call -->
                    <h:commandLink value="Delete"
                        action="#{todosController.deleteTodo(todoItem.id)}"
                        onclick="return confirm('Really delete this ToDo?')"/>
                </h:form>
            </h:column>
        </h:dataTable>
        
    </h:body>

</html>

todos.css (skinning)

.tableHeader { /* inverse table headline */
    background-color: black;
    color: white;
}
.tableRowOdd { /* zebra look for table rows */
    background-color: lightGray;
}
.tableRowEven { /* zebra look for table rows */
    background-color: white;
}
.error { /* error messages color */
    color: red;
}

TodosController.java

package fri.jsf.todos;

import java.util.Collection;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import fri.jsf.todos.model.Todo;
import fri.jsf.todos.service.TodoService;

@Named
@RequestScoped
public class TodosController
{
    private Collection<Todo> todos;
    private final TodoService service = new TodoService();

    public Collection<Todo> getTodos() {
        if (todos == null)
            todos = getService().findAll();
        return todos;
    }

    public String deleteTodo(Object id)    {
        getService().delete(id);
        return "todos?faces-redirect=true";
        // without redirect page would not update and item would still be visible
    }
    
    private TodoService getService()    {
        return service;
    }
}

On top of the todos.xhtml view we see an import of a CSS file. This is also used in the details-view (see below). Mind that any CSS file must be in directory src/main/webapp/resources/css/. The "css" folder-name seems to be what is in the library attribute in <h:outputStylesheet library="css" name="todos.css"/>.

The "Add" button directly navigates to the editTodo.xhtml page, not passing any parameter. This causes that page to provide a new Todo item (see EditTodoController below). The <h:dataTable> displays the Todo items, getting the list from the value attribute that calls the TodosController. The "Created At" property has a converter to display date and time in ISO-format.

The "Edit" <h:link> also calls editTodo.xhtml page, but it passes the id of the selected Todo item as parameter.

The JavaScript on "Delete" onclick="return confirm('Really delete this ToDo?')" drives the confirm-dialog. The "Delete" <h:commandLink> calls the controller and then navigates to whatever that returned. The controller deletes the Todo item with given id and then simply refreshes the todos.xhtml page through a redirect, which causes the list of Todo items to be built newly. Effect is that the deleted item disappears.

Mind that any kind of <h:commandXXX> MUST be enclosed into a <h:form> element.

Details Page

editTodo.xhtml (MVC-view, the input form)

<!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"
      xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
>
    <f:metadata>
        <f:viewParam name="id" />
        <f:event type="preRenderView" listener="#{editTodoController.load(id)}"/>
        <!-- See https://stackoverflow.com/questions/6377798/what-can-fmetadata-fviewparam-and-fviewaction-be-used-for -->
    </f:metadata>
    
    <h:head>
        <title>Edit ToDo</title>
        <h:outputStylesheet library="css" name="todos.css"/>
    </h:head>
    
    <h:body>
        <h1>To Do</h1>
        
        <h:form>
            <h:commandButton value="Cancel" action="#{editTodoController.cancel()}" immediate="true"/>
            <h:commandButton value="Save" action="#{editTodoController.save()}"/>
            
            <h:messages styleClass="error" />
                
            <h:panelGrid columns="2">
                <h:outputLabel for="description">Description:</h:outputLabel>
                <h:inputTextarea id="description" value="#{editTodoController.todo.description}" required="true"/>
            
                <h:outputLabel for="state">State:</h:outputLabel>
                <h:selectOneMenu id="state" value="#{editTodoController.todo.state}">
                    <f:selectItems value="#{editTodoController.getStates()}"/>
                </h:selectOneMenu>
                
                <h:outputLabel for="outcome">Outcome:</h:outputLabel>
                <h:inputTextarea id="outcome" value="#{editTodoController.todo.outcome}"/>
            
                <h:outputLabel for="createdAt">Created At:</h:outputLabel>
                <h:inputText id="createdAt"
                        value="#{editTodoController.todo.createdAt}"
                        p:placeholder="yyyy-MM-dd HH:mm">
                    <f:convertDateTime pattern="yyyy-MM-dd HH:mm" />
                </h:inputText>
            </h:panelGrid>
        </h:form>
        
    </h:body>

</html>

EditTodoController.java

package fri.jsf.todos;

import java.io.Serializable;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
import fri.jsf.todos.model.Todo;
import fri.jsf.todos.service.TodoService;

/** Controller (backing bean) for <code>editTodo.xhtml</code>. */
@Named
@ViewScoped
public class EditTodoController implements Serializable
{
    private transient Todo todo;
    private final TodoService service = new TodoService();

    /** Receive JSF view-parameter before rendering, see &lt;f:metadata&gt;. */
    public void load(String id) {
        if (id == null || id.length() <= 0)
            todo = new Todo();
        else
            todo = getService().find(Long.valueOf(id));
    }
    
    /** JSF accesses this editing object for read/write operations. */
    public Todo getTodo() {
        return todo;
    }

    /** Provide menu-items for ToDo-states. */
    public Todo.State[] getStates() {
        return Todo.State.values();
    }
    
    /** Callback for "Save" button. */
    public String save()    {
        if (todo.getId() == null)
            getService().create(todo);
        else
            getService().update(todo);    // not needed due to transparent persistence?
        
        return followerPage();
    }
        
    /** Callback for "Cancel" button. */
    public String cancel()    {
        return followerPage();  // old values will survive because "Cancel" is "immediate=true"
    }
    
    private String followerPage()    {
        return "todos?faces-redirect=true";
    }
    
    private TodoService getService()    {
        return service;
    }
}

The editTodo.xhtml view receives an optional parameter, represented by the <f:viewParam/> inside the <f:metadata/> element. The name "id" is arbitrary (needs not to be the property name), but it must match the name inside the #{editTodoController.load(id)} expression in <f:event/> element. This causes the EditTodoController.load() method to be called pefore the page is rendered. That way the controller can decide whether it is a new or an existing Todo item.

The "Cancel" button, ergonomically left of "Save", works through a JSF trick. The immediate=true attribute causes the server lifecycle to break in "Apply Request Values" phase. Effect is that the view values do not get written to the bean properties. If the button wasn't be immediate, the view values would be written to the Todo bean, and upon termination of the HTTP-request the ORM layer would detect any modification and save it automatically. That trick is also the reason for the EditTodoController.cancel() method containing no implementation.

The "Save" button calls the controller to immediately save the input. It must not be immediate. The controller method updates an existing Todo, or saves a new one. Both "Cancel" and "Save" navigate back to the list of Todo items.

Now although I didn't explicitly implement a validator, JSF checks the required=true attribute on "Description", and tries to execute the converter in "Created At". When one of them fails, the server lifecycle stops and stays on the page. Assumed you created a new item, did not enter the required "Description", wrote rubbish into the "Created At" field, and then pressed "Save", you would see following validation messages (managed through the presence of the h:messages element):

3) Service Layer

A service layer is stateless and transactional, and it should cache data. It encapsulates business logic and is the operational/functional part of the more structural domain model. It is use-case oriented, and thus also user-interface oriented.

The following service layer consists of a DAO and a Service, both minimalistic, both in a generic and a typed form.

Generic

Dao.java

package fri.jsf.todos.service.generic;

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

/** Encapsulates persistence logic, provides CRUD functionality. */
public abstract class Dao<T>
{
    protected abstract EntityManager getEntityManager();
    
    public Collection<T> findAll(Class<T> type)    {
        return getEntityManager().createQuery("select t from "+type.getName()+" t", type).getResultList();
    }
    
    public T find(Class<T> type, Object id) {
        return getEntityManager().find(type, id);
    }
    
    public void create(T entity) {
        getEntityManager().persist(entity);
    }
    
    public void update(T entity) {
        getEntityManager().merge(entity);
    }
    
    public void delete(Class<T> type, Object id) {
        final Object entity = find(type, id);  // gets entity into persistence context
        getEntityManager().remove(entity);
        // https://stackoverflow.com/questions/16086822/when-using-jpa-entitymanager-why-do-you-have-to-merge-before-you-remove
    }

    public EntityTransaction getTransaction() {
        return getEntityManager().getTransaction();
    }
}

This DAO abstraction can read and write any type of entity. The EntityManager (database, JPA persistence unit) must be provided by a concrete sub-class.

Maybe the findAll() method should not be here, because on database tables with millions of records it would cause an OutOfMemoryError when called.

Service.java

package fri.jsf.todos.service.generic;

import java.util.Collection;
import java.util.function.Function;
import javax.persistence.EntityTransaction;

/** CRUD functionality, wrapping each call in a transaction. */
public abstract class Service<T>
{
    /** Sub-classes must define their type. */
    protected abstract Class<T> getType();
    
    /** Sub-classes must define their DAO. */
    protected abstract Dao<T> getDao();
    
    public Collection<T> findAll() {
        return transactional(parameters -> getDao().findAll(getType()), null);
    }
    
    public T find(Object identity) {
        return transactional(id -> getDao().find(getType(), id), identity);
    }
    
    public void create(T entity) {
        transactional(e -> { getDao().create((T) e); return null; }, entity);
    }
    
    public void update(T entity) {
        transactional(e -> { getDao().update((T) e); return null; }, entity);
    }
    
    public void delete(Object identity) {
        transactional(id -> { getDao().delete(getType(), id); return null; }, identity);
    }

    /**
     * P = parameter type, R = return type.
     * @param daoFunction the DAO lambda to wrap into a transaction.
     * @param parameters the sole parameter to pass to the DAO function.
     * @return the return object of the DAO function.
     */
    protected <P,R> R transactional(Function<P,R> daoFunction, P parameters) {
        final EntityTransaction tx = getDao().getTransaction();
        try    {
            tx.begin();
            final R result = daoFunction.apply(parameters);
            tx.commit();
            return result;
        }
        catch (Exception e)    {
            tx.rollback();
            throw e;
        }
    }
}

The only thing this service does is wrapping every DAO call into a transaction. (I will explain this functional transaction-wrapping in my next Blog.)

Typed

Because most things have been solved in the generic layer, the concrete implementations are short. Mind that the service would grow when introducing business logic (not present in this example).

DaoDatabaseBinding.java

package fri.jsf.todos.service;

import javax.persistence.EntityManager;
import javax.persistence.Persistence;
import fri.jsf.todos.service.generic.Dao;

public abstract class DaoDatabaseBinding<T> extends Dao<T>
{
    public static final String JPA_UNIT = "JsfJpaPU";    // from META-INF/persistence.xml
    
    private static final EntityManager em = 
            Persistence.createEntityManagerFactory(JPA_UNIT).createEntityManager();

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }
}

This DAO binds to a concrete database (→ JPA persistence unit).

TodoDao.java

package fri.jsf.todos.service;

import fri.jsf.todos.model.Todo;

public class TodoDao extends DaoDatabaseBinding<Todo>
{
}

This DAO is dedicated to the Todo entity type.

TodoService.java

package fri.jsf.todos.service;

import fri.jsf.todos.model.Todo;
import fri.jsf.todos.service.generic.Service;

public class TodoService extends Service<Todo>
{
    private final TodoDao dao = new TodoDao();
    
    @Override
    protected Class<Todo> getType() {
        return Todo.class;
    }
    
    @Override
    protected TodoDao getDao() {
        return dao;
    }
}

This explicitly binds to the Todo class, so the name is fine. Nevertheless a service is not bound to a single entity type, it serves use cases and business operations that involve also other entity types. All this logic is missing here in this example, because it is just about the Todo entity type.

Conclusion

Here is a summary of what I was fighting with while I implemented this example.

  • Navigation is a big question, POST or GET or POST-Redirect-GET, which XHTML element can do what; how to pass parameters to a view, in URL or through JSF tricks; should I update a view by redirecting to the same page, how can I do it through AJAX; should I navigate in view or in controller or in faces-config.xml, are there best practices for this.

  • I found several tutorials about style-sheet loading on the web, but most of them forgot to show where in file system the .css file needs to be, according to the <h:outputStylesheet library="...."/> element which doesn't make this clear.

  • Renaming Java classes or methods is a mess, Eclipse doesn't help on this refactoring, JSF silently fails when some controller from XHTML is not found by name any more.

  • The tags <h:commandLink> and <h:commandButton> are the only ones that can call Java methods. <h:link>, <h:button> and <h:outputLink> can not.

  • The tags <h:commandLink> and <h:commandButton> silently fail when not contained in <h:form>, even when being AJAX'ed. Was not easy to find out.

  • The time conversion uses a hardcoded time zone, you must switch this explicitly off in web.xml(!) to get the local time.

  • Is the "immediate" attribute in "Cancel" button actually a good technique to roll back changes? Moreover it took some time to find out this trick.

  • A good JSF date chooser is not available in base library.

Lots of questions about JSF are answered on stackoverflow, but most of these answers are long and special, and you need quite a time to understand them before you find out that you're on the wrong page. It looks like JSF is such a big and old world that beginner problems are not known any more. Moreover lots of stuff about JSF on the web is outdated.




Keine Kommentare: