如果Random或SecureRandom用于生成组件ID

时间:2015-06-07 17:37:24

标签: jsf jsf-2.2 mojarra programmatically-created

当我使用UUID#randomUUID()(使用SecureRandom)或RandomStringUtils#randomAlphabetic(int)(使用Random)生成HtmlInputText的组件ID时,验证将停止工作。相反,如果我使用任意硬编码字符串(例如" C5d682a6f")设置组件ID,则验证按预期工作。这是代码:

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);
    }
}

有关为何如此的任何想法?我只需要将componentId作为在运行时生成并符合以下约定的任意String(来自UIComponent#setId(String) JavaDoc):

  

组件标识符必须遵守以下语法限制:

 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 ('-').
     

组件标识符还必须遵守以下语义限制(请注意,setId()实现不强制执行此限制):

 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.

我的开发环境是Wildfly 8.1.0.Final上的Mojarra 2.2.6-jbossorg-4。

修改

因此,似乎任何在运行时创建组件ID的尝试都会导致验证失败。

    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();

如果在编译时知道组件ID,则验证发生就好了。

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

我真的很想明白为什么会这样。

编辑#2:

以下工作正常(谢谢你,BalusC),在运行时生成的componentId,这正是我需要的:

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

我遵循了BalusC的建议,看了UIViewRoot#createUniqueId(),看起来就像这样:

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;
    }
}

但我感到困惑,因为上述方法似乎没有将新客户端ID存储在JSF视图状态中。它只增加lastId并在视图状态中更新lastId。

1 个答案:

答案 0 :(得分:1)

组件ID未存储在JSF视图状态中。它们就像组件本身,因此基本上是请求作用域。只有存储在JSF视图状态的东西基本上是视图范围的。即通过getStateHelper()方法放置/获取组件的东西。 getId() / setId()方法无法做到这一点。

当JSF需要处理回发请求时,它将在恢复视图阶段重建视图(即所有组件实例将重新创建,如new UIComponent()等),因此组件将按照您的方式获得不同的客户端ID。此后,JSF将使用JSF视图状态的数据恢复组件树。

然后,当JSF需要处理应用请求值阶段时,它将使用客户端ID作为参数名称从HTTP请求参数映射中提取请求参数。但是,由于此客户端ID已更改,JSF无法找到最初提交的值。

发生在这里的事情。如何解决它是第二个。一个很好的起点是UINamingContainer#createUniqueId()