How Are You World? with JSF with features like AJAX, Custom Validator, Custom Converter, Resource Bundles

A medium-sized application.


Requirements

  • I want to be able to add Students.
  • A Student must have Firstname and Lastname.
  • Firstname and Lastname can not be empty.
  • If the Student trying to be added exists in the database already, addition should not be allowed.
  • Firstname and Lastname should allow only characters between a-z and A-Z.
  • I want to see all the Students in the Application listed in a tabular format.
  • I want to be able to delete Students using this table.
  • Application must support multiple locales.
So we have the basic requirements, lets see a small demo of the final product:


Demo

Here is a small demo that demonstrates the implemented application:

Also let's see how the application looks if I use a browser that has Language preference:TR 

This is fine, text seems to be in Turkish.
Lets see the code, and then add some discussion at the end:


Code


pom.xml
<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/maven-v4_0_0.xsd">
 
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>biz.tugay</groupId>
    <artifactId>student-list</artifactId>
    <packaging>war</packaging>
 
    <version>1.0-SNAPSHOT</version>
 
    <name>student-list</name>
    <url>http://www.tugay.biz</url>
 
    <dependencies>
        <dependency>
            <groupId>javax.faces</groupId>
            <artifactId>javax.faces-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.2.13</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>2.2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
 
    <build>
        <finalName>student-list</finalName>
        <plugins>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.2.1.v20140609</version>
                <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
                    <webApp>
                        <contextPath>/</contextPath>
                    </webApp>
                </configuration>
            </plugin>
        </plugins>
    </build>
 
</project>

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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-app_3_0.xsd"
         version="3.0">
 
    <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>
 
    <context-param>
        <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
        <param-value>true</param-value>
    </context-param>
 
    <context-param>
        <param-name>javax.faces.VALIDATE_EMPTY_FIELDS</param-name>
        <param-value>true</param-value>
    </context-param>
 
    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>
 
</web-app>

faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<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_2.xsd"
              version="2.2">
 
    <!-- Application wide configurations -->
    <application>
 
        <!-- Resolving resource bundles -->
        <resource-bundle>
            <base-name>biz.tugay.msg</base-name>
            <var>msgs</var>
        </resource-bundle>
 
        <!-- Localisation configuration -->
        <locale-config>
            <default-locale>en</default-locale>
            <supported-locale>tr</supported-locale>
        </locale-config>
 
    </application>
 
</faces-config>

msg.properties
addNewStudent=Add New Student
applicationTitle=Student List Application
canNotBeEmpty=can not be empty!
delete=Delete
duplicateEntryFound=Duplicate Entry Found!
firstname=First Name
goBack=Go Back
lastname=Last Name
operationSuccessful=Operation successful!
providedValueNotAcceptable=Provided value is not acceptable for:
registeredStudents=Registered Students

msg_tr.properties
addNewStudent=Yeni \u00D6\u011Frenci Ekle
applicationTitle=\u00D6\u011Frenci Listesi Uygulamas\u0131
canNotBeEmpty=bo\u015F olamaz!
delete=Sil
duplicateEntryFound=Yinelenen Giri\u015F Bulundu!
firstname=\u0130sim
goBack=Geri Git
lastname=Soyisim
operationSuccessful=\u0130\u015Flem ba\u015Far\u0131l\u0131!
providedValueNotAcceptable=Girilen de\u011Fer bu alan i\u00E7in uygun de\u011Fil:
registeredStudents=Kay\u0131tl\u0131 Kullan\u0131c\u0131lar

Student.java
package biz.tugay.newajax.core;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 8:02 PM
 */
 
public class Student {
 
    private long id;
    private final String firstname;
    private final String lastname;
 
    public Student(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
 
    public String getFirstname() {
        return firstname;
    }
 
    public String getLastname() {
        return lastname;
    }
 
    public long getId() {
        return id;
    }
 
    public void setId(long id) {
        this.id = id;
    }
}

StudentDB.java
package biz.tugay.newajax.core;
 
import java.util.ArrayList;
import java.util.List;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 8:09 PM
 */
public class StudentDB {
 
    private final static List<Student> studentDatabase = new ArrayList<Student>();
    private static long generatedId = 0;
 
    public static List<Student> getStudentDatabase() {
        return studentDatabase;
    }
 
    public static boolean saveStudent(Student student) {
        for (Student s : studentDatabase) {
            if (s.getFirstname().equals(student.getFirstname())
                    && s.getLastname().equals(student.getLastname())) {
                return false;
            }
        }
        generatedId++;
        student.setId(generatedId);
        studentDatabase.add(student);
        return true;
    }
 
    public static void deleteStudent(Student student) {
        studentDatabase.remove(student);
    }
}

StudentForm.java
package biz.tugay.newajax.web.backing;
 
import biz.tugay.newajax.core.Student;
import biz.tugay.newajax.core.StudentDB;
 
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import java.util.ResourceBundle;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 8:03 PM
 */
 
@ManagedBean
@RequestScoped
public class StudentForm {
 
    private String firstname;
    private String lastname;
 
    public String getFirstname() {
        return firstname;
    }
 
    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }
 
    public String getLastname() {
        return lastname;
    }
 
    public void setLastname(String lastname) {
        this.lastname = lastname;
    }
 
    public String addStudent() {
        final Student student = new Student(firstname, lastname);
        final boolean studentSaveSuccessful = StudentDB.saveStudent(student);
 
        final FacesContext facesContext = FacesContext.getCurrentInstance();
        final ExternalContext externalContext = facesContext.getExternalContext();
        final Application application = facesContext.getApplication();
        final ResourceBundle bundle = application.getResourceBundle(facesContext, "msgs");
        if (!studentSaveSuccessful) {
            final String duplicateEntryFound = bundle.getString("duplicateEntryFound");
            final FacesMessage facesMessage = new FacesMessage(duplicateEntryFound, null);
            facesContext.addMessage(null, facesMessage);
            return "";
        }
        final String operationSuccessful = bundle.getString("operationSuccessful");
        externalContext.getFlash().put("message", operationSuccessful);
        return "index?faces-redirect=true";
    }
}

StudentTable.java
package biz.tugay.newajax.web.backing;
 
import biz.tugay.newajax.core.Student;
import biz.tugay.newajax.core.StudentDB;
 
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import java.util.List;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 8:02 PM
 */
 
@ManagedBean
@RequestScoped
public class StudentTable {
    public List<Student> getStudentList() {
        return StudentDB.getStudentDatabase();
    }
    public String deleteStudent(Student s) {
        StudentDB.deleteStudent(s);
        FacesContext.getCurrentInstance().
                getExternalContext().getFlash()
                .put("message", "Student has been deleted successfully!");
        return "index?faces-redirect=true";
    }
}

StudentFormConverter.java
package biz.tugay.newajax.web.converter;
 
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.FacesConverter;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 11:38 PM
 */
 
@FacesConverter("StudentFormConverter")
public class StudentFormConverter implements Converter {
 
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null) {
            return null;
        }
        return value.trim();
    }
 
    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value.toString();
    }
 
}

FieldRequiredValidator.java
package biz.tugay.newajax.web.validator;
 
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.html.HtmlInputText;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import java.util.ResourceBundle;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/5/2016
 * Time: 3:15 PM
 */
 
@FacesValidator("FieldRequiredValidator")
public class FieldRequiredValidator implements Validator {
    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        final String name = (String) value;
        if (name == null || name.equals("")) {
            final HtmlInputText htmlInputText = (HtmlInputText) component;
            final ResourceBundle bundle = context.getApplication().getResourceBundle(context, "msgs");
            final String canNotBeEmpty = bundle.getString("canNotBeEmpty");
            final FacesMessage validationMessage = new FacesMessage(htmlInputText.getLabel() + " " + canNotBeEmpty);
            throw new ValidatorException(validationMessage);
        }
    }
}

StudentFormValidator.java
package biz.tugay.newajax.web.validator;
 
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.html.HtmlInputText;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import java.util.ResourceBundle;
 
/**
 * User: Koray Tugay (koray@tugay.biz)
 * Date: 7/4/2016
 * Time: 8:44 PM
 */
 
@FacesValidator("StudentFormValidator")
public class StudentFormValidator implements Validator {
    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        final String name = (String) value;
        if (name == null) {
            return;
        }
        if (name.matches("^.*[^a-zA-Z ].*$")) {
            final HtmlInputText htmlInputText = (HtmlInputText) component;
            final String label = htmlInputText.getLabel();
            final ResourceBundle bundle = context.getApplication().getResourceBundle(context, "msgs");
            final String providedValueNotAcceptable = bundle.getString("providedValueNotAcceptable");
            final FacesMessage validationMessage =
                    new FacesMessage(providedValueNotAcceptable + " " + label + ".");
            throw new ValidatorException(validationMessage);
        }
    }
}

index.xhtml
<?xml version="1.0" encoding="UTF-8"?>
<!--
    User: Koray Tugay (koray@tugay.biz)
    Date: 7/4/2016
    Time: 8:00 PM
-->
 
<!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><h:outputText value="#{msgs['applicationTitle']}"/></title>
    <style>
        .info {
            border: 1px solid;
            margin: 2px;
            padding: 20px;
            color: #00529B;
            background-color: #BDE5F8;
        }
    </style>
</h:head>
<h:body>
 
    <h:panelGroup id="flashMessages" layout="block" rendered="#{flash.message ne null}" styleClass="info">
        <f:ajax event="click" onevent="remove()"/>
        <h:outputText value="#{flash.message}"/>
    </h:panelGroup>
 
    <h1><h:outputText value="#{msgs['registeredStudents']}"/></h1>
 
    <!--@elvariable id="student" type="biz.tugay.newajax.core.Student"-->
 
    <h:dataTable id="studentTable"
                 rendered="#{not empty studentTable.studentList}"
                 value="#{studentTable.studentList}"
                 var="student">
        <h:column>
            <f:facet name="header">ID</f:facet>
            <h:outputText value="#{student.id}"/>
        </h:column>
        <h:column>
            <f:facet name="header"><h:outputText value="#{msgs['firstname']}"/></f:facet>
            <h:outputText value="#{student.firstname}"/>
        </h:column>
        <h:column>
            <f:facet name="header"><h:outputText value="#{msgs['lastname']}"/></f:facet>
            <h:outputText value="#{student.lastname}"/>
        </h:column>
        <h:column>
            <h:form prependId="false" id="deleteStudentForm">
                <h:commandButton value="#{msgs['delete']}"
                                 action="#{studentTable.deleteStudent(student)}"/>
            </h:form>
        </h:column>
    </h:dataTable>
 
    <br/>
 
    <h:panelGroup layout="block">
        <h:link outcome="addStudent" value="#{msgs['addNewStudent']}"/>
    </h:panelGroup>
 
</h:body>
</html>

addStudent.xhtml
<?xml version="1.0" encoding="UTF-8"?>
<!--
    User: Koray Tugay (koray@tugay.biz)
    Date: 7/4/2016
    Time: 8:18 PM
-->
 
<!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><h:outputText value="#{msgs['applicationTitle']}"/></title>
    <style>
        .info {
            border: 1px solid;
            margin: 2px;
            padding: 20px;
            color: #00529B;
            background-color: #BDE5F8;
        }
    </style>
</h:head>
<h:body>
 
    <h1><h:outputText value="#{msgs['addNewStudent']}"/></h1>
 
    <h:form id="studentForm" prependId="false">
        <h:panelGrid columns="2">
            <h:outputText id="firstnameLabel" value="#{msgs['firstname']}: "/>
            <h:inputText id="firstname"
                         label="#{msgs['firstname']}"
                         value="#{studentForm.firstname}">
                <f:converter converterId="StudentFormConverter"/>
                <f:validator validatorId="FieldRequiredValidator"/>
                <f:validator validatorId="StudentFormValidator"/>
            </h:inputText>
            <h:outputText id="lastnameLabel" value="#{msgs['lastname']}: "/>
            <h:inputText id="lastname"
                         label="#{msgs['lastname']}"
                         value="#{studentForm.lastname}">
                <f:converter converterId="StudentFormConverter"/>
                <f:validator validatorId="FieldRequiredValidator"/>
                <f:validator validatorId="StudentFormValidator"/>
            </h:inputText>
            <h:commandButton id="addStudent"
                             value="#{msgs['addNewStudent']}"
                             action="#{studentForm.addStudent}">
                <f:ajax execute="@form" render="messages requiredMessage"/>
            </h:commandButton>
            <h:outputText value=""/>
        </h:panelGrid>
    </h:form>
    <h:link value="#{msgs['goBack']}" outcome="index"/>
    <br/>
    <br/>
    <h:messages id="messages" styleClass="info" />
</h:body>
</html>

Implementation Decisions

  • I used a simple class called StudentDB because this is an example for various JSF features. StudentDB can be thought as a simple, mock database.
  • The following code is required, else Validators do not work when submitted value is null:
    <context-param>
        <param-name>javax.faces.VALIDATE_EMPTY_FIELDS</param-name>
        <param-value>true</param-value>
    </context-param>
  • Javascript escapes are required for UTF-8 characters in properties files, such as: 
addNewStudent=Yeni \u00D6\u011Frenci Ekle
  • Student and StudentDB are not Managed Beans and they do not have scopes, JSF should not directly interact with instances of these classes.
  • I used AJAX request instead of a regular Post request as seen here:
<f:ajax execute="@form" render="messages requiredMessage"/>
The reason behind this is this: You fill in the fields, but a validation fails and you see an error message. If you do not have an AJAX request, but a regular form post and you hit on refresh button on the browser, you will see a message saying something like "Are you sure you want to.."? etc etc.. However, with AJAX used, when you Refresh the page the GET request you made will be refreshed, so the page will be cleared and no warning message will be seen. (You can see this behavior in the demo.)

Future Work

  • Templating futures of Facelets can be used.
  • <f:ajax event="click" onevent="remove()"/> does not seem to work in Internet Explorer, a solution must be found for this!