6. Validation in the View Layer

After we have defined the validations in the Java model layer and the database layer we need to take care of validation in the user interface. In particular, we need to make sure that human readable error messages are displayed.

6.1. Form validation for the Create Object use case

The WebContent/views/books/create.xhtml file contains the JSF code (see below) used to define the view which allows to create a new Book record. We like to improve the code so we can see the validation errors as well as enforce the uniqueness validation check.

<ui:composition template="/WEB-INF/templates/page.xhtml">
  <ui:define name="content">
    <h:form id="createBookForm" styleClass="pure-form pure-form-aligned">
      <h:panelGrid columns="3">
        <h:outputLabel for="isbn" value="ISBN: " />
        <h:inputText id="isbn" value="#{book.isbn}" 
            validator="#{bookController.checkIsbnAsId}"/>
        <h:message id="isbnMessages" for="isbn" errorClass="error" />
        <h:outputLabel for="title" value="Title: " />
        <h:inputText id="title" value="#{book.title}" />
        <h:message id="titleMessages" for="title" errorClass="error" />
        <h:outputLabel for="year" value="Year: " />
        <h:inputText id="year" p:type="number" value="#{book.year}" />
        <h:message id="yearMessages" for="year" errorClass="error" />
      </h:panelGrid>
      <h:commandButton value="Create" 
          action="#{bookController.add( book.isbn, book.title, book.year)}" />
    </h:form>
    <h:button value="Main menu" outcome="index" />
  </ui:define>
</ui:composition>

There are only a few changes comparing with the same view used for the minimal application, when no validation was performed. The first change is the new h:message element which is bound to a specific form element by using the h:message/@for attribute. We create a such an element for each of our form input elements. Please notice that we don't have to do nothing else for seeing the validation errors for all integrity constraint checks which are performed by using the Validation API annotations (builtin and custom defined ones). As soon as a constraint validation fails, the message set by using the message property of the integrity constraint annotation (e.g. @Pattern, @NotNull, etc) is displayed in the JSF generated HTML5 span element, created as a result of using the h:message element.

For all the integrity constraints we have used Validation API annotations (builtin and the UpToNextYear custom one we've created), but for the uniqueness constraint we have used custom code, therefore no error message will be shown for it. In the view code we can see that a new attribute, h:inputText/@validator was used for the isbn input field. It specifies which custom method is used to perform validation of the provided value in this form field. In our case, we use the checkIsbnAsId method defined in the BookController controller as shown below:

public void checkIsbnAsId( FacesContext context, UIComponent component, 
    Object value) throws ValidatorException {
  String isbn = (String) value;
  try {
    Book.checkIsbnAsId( em, isbn);
  } catch ( UniquenessConstraintViolation e) {
    throw new ValidatorException( new FacesMessage(
        FacesMessage.SEVERITY_ERROR, e.getMessage(), e.getMessage()));
  }
}

The controller check method throws a ValidatorException which is also used to deliver the error message (the third parameter of the ValidatorException constructor) to the corresponding JSF facelet for being displayed in the UI. Methods used as validators must have a specific syntax. The first two parameters of type FacesContext, respectively UIComponent are used by the container to invoke the method with references to the right view component and context, and they can be used in more complex validation methods. The last one, of type Object is mostly used by the method validation code and it contains the value which was provided in the form field. This value has to be casted to the expected value type (i.e. String in our case). It is important to know that, if the a cast to a non compatible type is requested, the validation method fails and an exception is thrown.

6.2. Form validation for the Update Object use case

In the update object test case, the WebContent/views/books/update.xhtml file was updated so it uses the h:message elements for being able to display validation errors:

<ui:composition template="/WEB-INF/templates/page.xhtml">
  <ui:define name="content">
    <h:form id="updateBookForm" styleClass="pure-form pure-form-aligned">
      <h:panelGrid columns="3">
        <h:outputLabel for="selectBook" value="Select book: " />
        <h:selectOneMenu id="selectBook" value="#{book.isbn}">
          <f:selectItem itemValue="" itemLabel="---" />
          <f:selectItems value="#{bookController.books}" var="b" 
             itemValue="#{b.isbn}" itemLabel="#{b.title}" />
          <f:ajax listener="#{bookController.refreshObject( book)}" 
              render="isbn title year"/>
        </h:selectOneMenu>
        <h:outputLabel for="isbn" value="ISBN: " />
        <h:outputText id="isbn" value="#{book.isbn}" />
        <h:outputLabel for="title" value="Title: " />
        <h:inputText id="title" value="#{book.title}" />
        <h:message id="titleMessages" for="title" errorClass="error" />
        <h:outputLabel for="year" value="Year: " />
        <h:inputText id="year" p:type="number" value="#{book.year}" />
        <h:message id="yearMessages" for="year" errorClass="error" />
      </h:panelGrid>
      <h:commandButton value="Update" 
          action="#{bookController.update( book.isbn, 
          book.title, book.year)}"/>
    </h:form>
    <h:button value="Main menu" outcome="index" />
  </ui:define>
</ui:composition>

Since we do not allow to change the isbn value, it is created by using h:outputText, it does not require to be validated in the view, so a validator is not defined as in the create object use case.

The result of an h:outputText JSF element is a HTML5 span element. This means that the form submission which occurs on pressing the "Update" h:commandButton contains no information about that specific element. If the validation fail, we expect to see the form content together with the error messages. To get the expected result, we need to use @ViewScoped annotation for the bean class (i.e. pl.model.Book) instead of @RequestScoped, otherwise our bean instance referenced by the book variable is initialized with a new value on every request (this is what @RequestScoped annotation is for), and then #{book.isbn}" is equal with null. As a result, our ISBN value is not shown. The @ViewScoped annotation specifies that the bean instance is alive as long as the associated view is alive, so the ISBN value stored by the book is available for all this time and it can be displayed in the view.

By contrast, the h:inputText JSF elements results in input HTML5 elements which are part of the form submission content, so the response contains the already existing values because these values are known in this case. From this we learn that choosing a bean scope is important, and it can create logical errors if we do not pay attention to it.