如果使用 Random 或 SecureRandom 生成组件 ID,则 JSF 验证器不起作用
JSF Validators do not work if Random or SecureRandom used to generate component ID
当我使用 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 是在运行时生成的任意字符串并符合以下约定(来自 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.
我的开发环境是Mojarra 2.2.6-jbossorg-4 on Wildfly 8.1.0.Final.
编辑:
因此似乎任何在运行时创建组件 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;
}
}
但我很困惑,因为上面的方法似乎没有在 JSF 视图状态中存储新的客户端 ID。它只会增加 lastId 并更新视图状态中的 lastId。
组件 ID 未存储在 JSF 视图状态中。它们本身就像组件,因此基本上是请求范围的。只有存储在 JSF 视图状态中的内容基本上是视图范围的。 IE。通过 getStateHelper()
方法组成 put/get 的东西。 getId()
/setId()
方法不会那样做。
当 JSF 需要处理回发请求时,它将在恢复视图阶段重建视图(即所有组件实例将像 new UIComponent()
等一样重新创建),因此组件将按照您的方式获得不同的客户端 ID。此后,JSF 将使用来自 JSF 视图状态的数据恢复组件树。
然后,当JSF需要处理apply request values阶段时,它会使用client ID作为参数名从HTTP请求参数映射中提取请求参数。但是,由于此客户端 ID 已更改,JSF 无法找到最初提交的值。
这就是这里发生的事情。如何解决是一秒钟。一个好的起点是 UINamingContainer#createUniqueId()
.
当我使用 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 是在运行时生成的任意字符串并符合以下约定(来自 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.
我的开发环境是Mojarra 2.2.6-jbossorg-4 on Wildfly 8.1.0.Final.
编辑:
因此似乎任何在运行时创建组件 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;
}
}
但我很困惑,因为上面的方法似乎没有在 JSF 视图状态中存储新的客户端 ID。它只会增加 lastId 并更新视图状态中的 lastId。
组件 ID 未存储在 JSF 视图状态中。它们本身就像组件,因此基本上是请求范围的。只有存储在 JSF 视图状态中的内容基本上是视图范围的。 IE。通过 getStateHelper()
方法组成 put/get 的东西。 getId()
/setId()
方法不会那样做。
当 JSF 需要处理回发请求时,它将在恢复视图阶段重建视图(即所有组件实例将像 new UIComponent()
等一样重新创建),因此组件将按照您的方式获得不同的客户端 ID。此后,JSF 将使用来自 JSF 视图状态的数据恢复组件树。
然后,当JSF需要处理apply request values阶段时,它会使用client ID作为参数名从HTTP请求参数映射中提取请求参数。但是,由于此客户端 ID 已更改,JSF 无法找到最初提交的值。
这就是这里发生的事情。如何解决是一秒钟。一个好的起点是 UINamingContainer#createUniqueId()
.