JSF Validators do not work if Random or SecureRandom used to generate component ID

214 views Asked by At

When I use UUID#randomUUID() (which uses SecureRandom) or RandomStringUtils#randomAlphabetic(int) (which uses Random) to generate component ID for HtmlInputText validation stops working. If instead I set the component ID using an arbitrary hard-coded String (e.g. "C5d682a6f") the validation works as expected. Here's the code:

import org.apache.commons.lang3.RandomStringUtils;
import java.util.UUID;
import javax.faces.component.html.HtmlInputText;
import javax.faces.component.html.HtmlMessage;
import javax.faces.component.html.HtmlPanelGrid;

@Model
public class LoginBean
{
    private HtmlPanelGrid panelGrid;
    private String email;
    @PostConstruct void initialize()
    {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        String componentId;
        componentId = "C" + UUID.randomUUID().toString().substring(0, 8);   // Yields something like "C5d682a6f", which should be fine, yet breaks validation.
        //componentId = RandomStringUtils.randomAlphabetic(8);              // Yields something like "zxYBcUYM", which should be fine, yet breaks validation.
        //componentId = "C5d682a6f";                                        // Hard-coding the same exact kind of string generated by UUID#randomUUID() works fine.
        //componentId = "zxYBcUYM";                                         // Hard-coding the same exact kind of string generated by RandomStringUtils#randomAlphabetic(int) 

        HtmlInputText emailFieldComponent = (HtmlInputText)facesContext.getApplication().createComponent(
            facesContext,
            HtmlInputText.COMPONENT_TYPE,
            "javax.faces.Text"
        );
        emailFieldComponent.setId(componentId);
        emailFieldComponent.setValueExpression(
            "value",
            facesContext.getApplication().getExpressionFactory().createValueExpression(
                facesContext.getELContext(),
                "#{loginBean.email}",
                String.class
            )
        );

        // The following validators stop working if UUID#randomUUID() or
        // RandomStringUtils#randomAlphabetic(int) are used to generate componentId.
        emailFieldComponent.setRequired(true);
        emailFieldComponent.addValidator(new EmailValidator());

        HtmlMessage message = (HtmlMessage)facesContext.getApplication().createComponent(
            facesContext,
            HtmlMessage.COMPONENT_TYPE,
            "javax.faces.Message"
        );
        message.setFor(componentId);

        panelGrid = (HtmlPanelGrid)facesContext.getApplication().createComponent(
            facesContext,
            HtmlPanelGrid.COMPONENT_TYPE,
            "javax.faces.Grid"
        );
        panelGrid.setColumns(2);
        panelGrid.getChildren().add(emailFieldComponent);
        panelGrid.getChildren().add(message);
    }
}

Any ideas on why this is so? I just need the componentId to be an arbitrary String generated at runtime and conforming to the following conventions (from UIComponent#setId(String) JavaDoc):

Component identifiers must obey the following syntax restrictions:

 Must not be a zero-length String.
 First character must be a letter or an underscore ('_').
 Subsequent characters must be a letter, a digit, an underscore ('_'), or a dash ('-').

Component identifiers must also obey the following semantic restrictions (note that this restriction is NOT enforced by the setId() implementation):

 The specified identifier must be unique among all the components (including facets) that are descendents of the nearest ancestor UIComponent that is a NamingContainer, or within the scope of the entire component tree if there is no such ancestor that is a NamingContainer.

My development environment is Mojarra 2.2.6-jbossorg-4 on Wildfly 8.1.0.Final.

EDIT:

So it seems that any attempt to create a component ID at runtime causes validation to not happen.

    componentId = "C" + Long.toHexString(Double.doubleToLongBits(Math.random()));
    componentId = "C" + Long.toHexString(System.currentTimeMillis());
    componentId = "C" + Long.toHexString(new Date().getTime());
    componentId = "C" + new Date().hashCode();

Whereas if component ID is known at compile time validation happens just fine.

    componentId = "C" + Long.toHexString(Double.doubleToLongBits(Double.MAX_VALUE));

I'd really like to understand why this is so.

EDIT #2:

The following works just fine (thank you, BalusC), componentId being generated at runtime, which is exactly what I need:

    setId(facesContext.getViewRoot().createUniqueId());

I followed BalusC's advice and looked at UIViewRoot#createUniqueId(), which looks like this under the hood:

public String createUniqueId() {
    return createUniqueId(getFacesContext(), null);
}

public String createUniqueId(FacesContext context, String seed) {
    if (seed != null) {
        return UIViewRoot.UNIQUE_ID_PREFIX + seed;
    } else {
        Integer i = (Integer) getStateHelper().get(PropertyKeys.lastId);
        int lastId = ((i != null) ? i : 0);
        getStateHelper().put(PropertyKeys.lastId,  ++lastId);
        return UIViewRoot.UNIQUE_ID_PREFIX + lastId;
    }
}

But I'm confused because it doesn't appear that the above method stores the new client ID in JSF view state. It only increments lastId and updates lastId in the view state.

1

There are 1 answers

2
BalusC On BEST ANSWER

Component IDs are not stored in JSF view state. They are like components themselves thus basically request scoped. Only stuff which is stored in JSF view state is basically view scoped. I.e. the stuff which components put/get via getStateHelper() method. The getId()/setId() methods doesn't do that.

When JSF needs to process the postback request, it will during restore view phase rebuild the view (i.e. all component instances will be recreated like new UIComponent() etc) and hereby the components will with your way thus get a different client ID. Hereafter JSF will restore the component tree with data from JSF view state.

Then, when JSF needs to process the apply request values phase, it will extract the request parameters from the HTTP request parameter map using client ID as parameter name. However, as this client ID has changed, JSF can't find the initially submitted values.

That what's happening here. How to solve it is a second. A good starting point is UINamingContainer#createUniqueId().