jsfomnifaces

<o:validateBean>: turns List<DemoData> into List<String>?


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)

Solution

  • 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" />