I want to use <o:validateBean> to validate my bean at the class level. The bean is:
@Named
@ConversationScoped
public class ValidateClassLevelBean implements Serializable {
...
@Inject
private Conversation con;
@Min(value = 2)
@Max(value = 5)
private int objectCount;
@Valid
private List<DemoData> listData;
public String reInit() {
if (!con.isTransient()) {con.end();}
con.begin();
listData = new ArrayList<>(objectCount);
for (int i = 0; i < objectCount; i++) {
listData.add(new DemoData("bbb", 3));
}
return "validProduct_2";
}
public String action() {
for (DemoData d : listData) {
logger.info("[listData: <" + d.toString() + ">]");
}
con.end();
return "index";
}
...
}
The Facelet looks like this:
<h:form id="classLevelFormCustomCopier">
...
<table>
<ui:repeat value="#{validateClassLevelBean.listData}" var="data" varStatus="loop">
<tr>
<td>
<h:inputText id="dataInput"
value="#{validateClassLevelBean.listData[loop.index]}"/>
</td>
</tr>
</ui:repeat>
</table>
<h:commandButton value="submit" action="#{validateClassLevelBean.action}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<o:validateBean value="#{validateClassLevelBean}"
validationGroups="de.test.wholebeanvalidation.validProduct.DemoDataGroup" />
The DemoData
is nothing special (a forClass
-Converter exists):
@ValidDemoData(groups = DemoDataGroup.class)
public class DemoData implements Serializable {
...
private String strData;
private int intData;
public DemoData() {
this.strData ="";
this.intData=0;
}
public DemoData(DemoData d) {
this.strData =d.getStrData();
this.intData = d.getIntData();
}
...
}
The validation constraint annotation:
@Constraint(validatedBy = { DemoDataValidator.class })
@Documented
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface ValidDemoData {
String message() default "Invalid product";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The Validator:
public class DemoDataValidator implements ConstraintValidator<ValidDemoData, ValidateClassLevelBean> {
...
@Override
public void initialize(ValidDemoData constraintAnnotation) {
logger.info("[ init ]");
}
@Override
public boolean isValid(ValidateClassLevelBean value, ConstraintValidatorContext context) {
...
return true;
}
}
Droped the copier="..."
and the Exception is:
[2024-07-27T18:02:30.870+0200] [Payara 6.2024.1] [SCHWERWIEGEND] [] [org.omnifaces.taghandler.ValidateBean] [tid: _ThreadID=158 _ThreadName=http-thread-pool::http-listener-2(3)] [timeMillis: 1722096150870] [levelValue: 1000] [[
Exception occured while doing validation.
java.lang.ClassCastException: class java.lang.Class cannot be cast to class java.lang.reflect.ParameterizedType (java.lang.Class and java.lang.reflect.ParameterizedType are in module java.base of loader 'bootstrap')
at org.omnifaces.util.Reflection.setBeanPropertyWithDefaultValue(Reflection.java:410)
at org.omnifaces.util.Reflection.getBeanProperty(Reflection.java:397)
at org.omnifaces.util.Reflection.getBase(Reflection.java:336)
at org.omnifaces.util.Reflection.setBeanProperties(Reflection.java:314)
at org.omnifaces.taghandler.ValidateBean$3.invoke(ValidateBean.java:456)
at org.omnifaces.taghandler.ValidateBean$ValidateBeanCallback.run(ValidateBean.java:751)
at org.omnifaces.util.Events.lambda$wrap$1(Events.java:334)
at org.omnifaces.util.Events$4.afterPhase(Events.java:370)
at org.omnifaces.eventlistener.CallbackPhaseListener.afterPhase(CallbackPhaseListener.java:69)
at com.sun.faces.lifecycle.Phase.handleAfterPhase(Phase.java:148)
at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:78)
at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:159)
at jakarta.faces.webapp.FacesServlet.executeLifecyle(FacesServlet.java:691)
at jakarta.faces.webapp.FacesServlet.service(FacesServlet.java:449)
at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1554)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:331)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:211)
at org.glassfish.tyrus.servlet.TyrusServletFilter.doFilter(TyrusServletFilter.java:83)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:253)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:211)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:257)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:166)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:757)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:577)
at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:99)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:158)
at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:372)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:239)
at com.sun.enterprise.v3.services.impl.ContainerMapper$HttpHandlerCallable.call(ContainerMapper.java:520)
at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:217)
at org.glassfish.grizzly.http.server.HttpHandler$1.run(HttpHandler.java:190)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:535)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:515)
at java.base/java.lang.Thread.run(Thread.java:1583)
a forClass-Converter exists
The observable problem is in the end not caused by <o:validateBean>
. It's caused by erasure of generic type and EL (those #{..}
things) in its current version being unable to deal with it. While setting a new item in a List
, EL doesn't know what the generic type of the list is and therefore assumes the default of String
.
You can solve this by explicitly registering the converter on the input component, or, as you discovered, by using a plain array instead of a List
.
<h:inputText id="dataInput"
value="#{validateClassLevelBean.listData[loop.index]}"
converter="yourDemoDataConverter" />