When developing complex applications, JavaServer Faces (JSF) works best when used “statefully”. In practice, this means using @ViewScoped backing beans. A consequence is that client-specific view state is persistent on the server.

JavaServer Faces can also be used in a stateless mode.

Unfortunately, stateless operation presents problems when using validation. In particular, validation failures prevent model update. This means that model state cannot be used to render a view in the event of a validation error.

Input components can use posted-back form data to re-render the view during a validation error. However, output components (e.g., <h:outputText>) have no posted-back data. Their state needs to be maintained using hidden fields in a form.

In JSF, hidden state can be maintained using <h:inputHidden>. However, validation interferes with <h:inputHidden>. Like any other input component, validation errors prevent all model updates.

My solution to this problem is to use a custom component. The component works almost the same as <h:inputHidden>. Instead of following the usual JSF lifecycle, it performs model update before validation.

Read on for the implementation and an example of its use.

Implementation

Here's the source code for the unvalidatedInputHidden tag:

import java.io.*;
import javax.faces.component.*;
import javax.faces.context.*;
import javax.faces.convert.*;

/**
 * A JSF component that works like inputHidden but performs model update 
 * during the decode stage of the JSF lifecycle. This ensures that model
 * update is performed even if a validation error occurs.
 */
@FacesComponent(createTag = true)
public class UnvalidatedInputHidden extends UIInput {

    @Override
    public String getFamily() {
        return "custom.unvalidated";
    }

    /**
     * Handle the decode phase of the JSF lifecyle.
     * Extracts the parameter from the posted-back form data.
     * Uses the declared converter (if present).
     * Saves directly to the model.
     * @param context the JSF context
     */
    @Override
    public void processDecodes(FacesContext context) {
        String clientId = getClientId(context);
        String param = context.getExternalContext().getRequestParameterMap().get(clientId);
        Converter converter = getConverter();
        Object value;
        if (converter == null)
            value = param;
        else
            value = getConverter().getAsObject(context, this, param);
        setValue(value);

        // Jump ahead to update model: use the default implementation in the **superclass**
        super.updateModel(context);
    }

    /**
     * Normally this method would handle the model update phase of the JSF 
     * lifecycle. This method does nothing, as the model update has already been
     * performed during the decode phase.
     * @param context the JSF context
     */
    @Override
    public void updateModel(FacesContext context) {
        // Do nothing
        // model is already updated in the decode phase
        // (super.updateModel is called from processDecodes)
    }

    /**
     * Output a HTML input type="hidden" element to save the state.
     * Uses the declared converter (if present).
     * @param context the JSF context
     * @throws IOException 
     */
    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        String clientId = getClientId(context);
        ResponseWriter out = context.getResponseWriter();
        out.startElement("input", this);
        out.writeAttribute("name", clientId, "id");
        out.writeAttribute("type", "hidden", null);
        Converter converter = getConverter();
        String value;
        if (converter == null)
            value = String.valueOf(getValue());
        else
            value = getConverter().getAsString(context, this, getValue());
        out.writeAttribute("value", value, "value");
        out.endElement("input");
    }

}

Usage

To use a custom component, you need to add the JSF namespace for custom components at the top of your Facelet:

xmlns:custom="http://xmlns.jcp.org/jsf/component"

Once you've done that, you can use the component directly:

<custom:unvalidatedInputHidden value="#{xxxxxx}"/>

Here's a sample stateless Facelets file that uses the component:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<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:custom="http://xmlns.jcp.org/jsf/component">
    <f:view transient="true">
        <h:head>
            <title>Unvalidated Input Hidden Demo</title>
        </h:head>
        <h:body>
            <h:form>
                <p>The current counter value is: #{demoController.count}</p>
                <p>Clear this text to cause a validation error: <h:inputText value="#{demoController.validated}"/></p>
                <h:messages/>
                <p><h:commandButton value="Increment Counter" action="#{demoController.increment}"/></p>
                <custom:unvalidatedInputHidden value="#{demoController.count}"/>
            </h:form>
        </h:body>
    </f:view>
</html>

The above view requires the following backing bean:

import javax.enterprise.context.*;
import javax.inject.*;
import javax.validation.constraints.*;

/**
 * A simple backing bean to demonstrate the custom unvalidatedInputHidden
 * component in JSF. The DemoController maintains a counter and has an additional
 * property (validated) that must be a non-empty string.
 */
@Named
@RequestScoped
public class DemoController {

    private int count;
    private String validated = "Initial value";

    //----------------------------------------------------------
    // Count property: the "counter" logic

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public void increment() {
        count++;
    }

    //----------------------------------------------------------
    // Validated property: used only to create validation errors

    @NotNull
    @Size(min = 1)
    public String getValidated() {
        return validated;
    }

    public void setValidated(String validated) {
        this.validated = validated;
    }

}

Note that the increment action will only be called by the view if there are no validation errors. However, in the event of a validation failure, the unvalidatedInputHidden will ensure that current count value will be remembered. If an ordinary inputHidden is used (instead of an unvalidatedInputHidden) then the counter will reset to zero whenever there is a validation error.

Published 6 September 2015 by Benjamin Johnston.