JSF Bootstrap style validation

Standard

In this example, I provide a solution to the problem of needing to display errors specific to a form on a page. This solution was developed due to the need to have multiple forms on the same page, while maintaining a simple way to display errors belonging to there ID’s. The point of this article is to show you how to make a simple profile page for submitting a first and last name that is validated.

The most important class is the FacesUtil.java which provides methods for queuing / retrieving facesMessages based on each inputs DOM id. I have defined 3 types for displaying errors based on the bootstrap 3 message classes (success, warning, and danger).

public class FacesUtil {

    /**
     * Options allowed for errors
     */
    private static List errorCodes = new ArrayList() {
        {
            add("danger");
            add("warning");
            add("success");
        }
    };

   /**
     *
     * @param str
     */
    public static void queueMessage(String str) {
        queueMessage(str, "warning");
    }

    /**
     *
     * @param str
     * @param severity
     */
    public static void queueMessage(String str, String severity) {

        // Did they enter a valid severity
        if (!errorCodes.contains(severity)) {
            Logger.getLogger(FacesUtil.class.getName()).log(Level.SEVERE, null, "Improper api usage");
            return;
        }

        FacesMessage message = new FacesMessage(str);

        switch (severity) {
            case "danger":
                message.setSeverity(FacesMessage.SEVERITY_ERROR);
                break;
            case "warning":
                message.setSeverity(FacesMessage.SEVERITY_WARN);
                break;
            default:
                message.setSeverity(FacesMessage.SEVERITY_INFO);
                break;
        }

        FacesContext.getCurrentInstance().addMessage(UIComponent.getCurrentComponent(FacesContext.getCurrentInstance()).getClientId(), message);
    }

    /**
     *
     * @param severity
     * @return
     */
    public static Collection getMessages(String severity) {
        return getMessages("", severity);
    }

    /**
     * Gets all messages in a array list of a given severity type. Specify
     * severity as "error", "warn", or "info"
     *
     * @param formId
     * @param severity
     * @return
     */
    public static Collection getMessages(String formId, String severity) {

        // Get the Faces Context
        FacesContext facesContext = FacesContext.getCurrentInstance();

        // Restore other messages
        restoreMessages(facesContext);

        // Get the root of the UIComponent
        UIViewRoot root = facesContext.getViewRoot();

        // Iterator to iterate through the client ID's
        Iterator iterator = facesContext.getClientIdsWithMessages();

        // Map all the messages by error level
        Map<String, Collection> map = new HashMap<>();

        // Did they enter a valid severity
        if (!errorCodes.contains(severity)) {
            Logger.getLogger(FacesUtil.class.getName()).log(Level.SEVERE, null, "Improper api usage");
            return map.get(severity);
        }

        // Determine error categories
        String errorLevel = "";
        switch (severity) {
            case "danger":
                errorLevel = "ERROR 2";
                break;
            case "warning":
                errorLevel = "WARN 1";
                break;
            default:
                errorLevel = "INFO 0";
                break;
        }

        // Iterate through the messages
        while (iterator.hasNext()) {

            // Retrieve the client ID
            String clientId = iterator.next();

            // Retrieve the item UI component
            UIComponent baseComponent = root.findComponent(clientId);

            // Find the the form UI component
            UIComponent parentForm = findParentForm(baseComponent);

            // If the form ID was specified and its not the true parent, we skip this iteration
            if (!formId.equals("") && (parentForm == null || (!formId.equals("") && !parentForm.getId().equals(formId)))) {
                continue;
            }

            // If the formId is not specified we always display it as a global message
            if (formId.equals("") || baseComponent != null) {

                // Return an Iterator over the FacesMessages
                Iterator facesIterator = facesContext.getMessages(clientId);

                // Build a list to return based on type specified
                while (facesIterator.hasNext()) {

                    FacesMessage facesMessage = facesIterator.next();
                    Collection values = map.get(errorLevel);
                    if (values == null) {
                        values = new ArrayList<>();
                        map.put(errorLevel, values);
                    }

                    // Only add if we don't have that value already
                    if (facesMessage.getSeverity().toString().equals(errorLevel) && !values.contains(facesMessage.getDetail())) {
                        values.add(facesMessage.getDetail());
                    }
                }
            }
        }

        return map.get(errorLevel);
    }

    /**
     *
     * @param component
     * @return
     */
    public static UIComponent findParentForm(UIComponent component) {
        if (component instanceof UIForm) {
            return component;
        }
        if (component == null) {
            return null;
        }
        return findParentForm(component.getParent());
    }

    /**
     * @return the errorCodes
     */
    public static List getErrorCodes() {
        return errorCodes;
    }

    /**
     * @param aErrorCodes the errorCodes to set
     */
    public static void setErrorCodes(List aErrorCodes) {
        errorCodes = aErrorCodes;
    }
}

Interfacing with the FacesUtil.java class is a viewScoped bean called viewProfile.java. This bean extends the methods of the FacesUtil class and is where our submit action lives. It also defines the first and last name that will be submitted from our profile page.

/**
 * Sample profile page
 *
 * @author sixthpoint
 */
@ManagedBean
@ViewScoped
public class ViewProfile implements Serializable {

    private String firstName;
    private String lastName;

    /**
     * Basic constructor
     */
    public ViewProfile() {

    }

    /**
     * On submit, we queue a success message
     */
    public void submit() {

        FacesUtil.queueMessage("we have success", "success");
    }

    /**
     * Returns a array of validation options
     *
     * @return
     */
    public List validationOptions() {
        return FacesUtil.getErrorCodes();
    }

    /**
     * Ask for validation messages based on a formId in the component tree
     * (success, warning, danger)
     *
     * @param formId
     * @param level
     * @return
     */
    public Collection validation(String formId, String level) {
        return FacesUtil.getMessages(formId, level);
    }

    /**
     * Omitted getters and setters
     */

The final page is the actual JSF view. It is simple to display all errors from the faces util class through the viewProfile bean. On inputText I have defined a validateLength and a requireMessage. These have been set to required. If those validation requirements are not met, the error will be displayed in the panelGroup block below. The FacesUtil class has the logic to grab all manually queued messages as well as messages queue from validation inputs.

<h:form id="profileForm">
    <h:inputText value="#{viewProfile.firstName}" p:placeholder="First Name" required="true" requiredMessage="Please enter a first name">
        <f:validateLength minimum="3"/>
    </h:inputText>
    <h:inputText value="#{viewProfile.lastName}" p:placeholder="Last Name" required="true" requiredMessage="Please enter a last name">
        <f:validateLength minimum="3"/>
    </h:inputText>
    <h:commandButton type="submit" value="Submit" action="#{viewProfile.submit}">
        <f:ajax execute="@form" render="@form :messages"/>
    </h:commandButton>
</h:form>

<h:panelGroup layout="block" id="messages">
    <c:forEach var="errorCode" items="#{viewProfile.validationOptions()}">
        <h:panelGroup layout="block" rendered="#{viewProfile.validation('profileForm', errorCode).size() gt 0}">
            <h:panelGroup layout="block" styleClass="alert alert-#{errorCode} alert-dismissable">
                <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#xD7;</button>
                <ul>
                    <c:forEach var="m" items="#{viewProfile.validation('profileForm', errorCode)}">
                        <li>#{m}</li>
                    </c:forEach>
                </ul>
            </h:panelGroup>
        </h:panelGroup>
    </c:forEach>
</h:panelGroup>

 

Omitted are the bootstrap CDN’s libraries. A full working version of this code can be found on Github.