1

In JSF 2.3, I have an h:inputText to edit a Double value, which has in addition Bean-Validation constraints. The h:inputText has a f:convertNumber. When submitting the form, this leads to a ClassCastException (see below).

So, it seems, that f:convertNumber produces a Long which then, could not be converted to Double to validate the @DecimalMin constraint, right?

In JSF 2.2 this worked as expected, the problem occured after upgrading to JSF 2.3.

Does anybody has any ideas what could be the problem?

I could reproduce this in WildFly 15.0.1 with the following minimal example with just one facelet an just lombok as dependency:

@Data
public class Building {

    @DecimalMin("0.0")
    private Double area;
}

@Named
@ViewScoped
public class BuildingEditBean implements Serializable {

    @Getter
    @Setter
    private Building building;

    @PostConstruct
    public void init() {
        this.building = new Building();
    }

    public void submit() {
        System.out.println("Building: " + this.building.getArea());
    }
}

Facelet:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:p="http://xmlns.jcp.org/jsf/passthrough">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        <h:form>
            <div class="form-group">
                <label>Area</label>
                <h:inputText id="area" value="#{buildingEditBean.building.area}">
                    <f:convertNumber />
                </h:inputText>
                <h:messages id="areaMessage" for="area" />
            </div>
            <h:commandButton value="Save" action="#{buildingEditBean.submit()}" />
        </h:form>
    </h:body>
</html>

Stacktrace after form submit with valid value:

10:57:07,649 WARNING [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-1) HV000028: Unexpected exception during isValid call.: javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:177)
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:68)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateValueInContext(ValidatorImpl.java:781)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateValue(ValidatorImpl.java:210)
    at javax.faces.validator.BeanValidator.validate(BeanValidator.java:349)
    at javax.faces.component.UIInput.validateValue(UIInput.java:1248)
    at javax.faces.component.UIInput.validate(UIInput.java:1037)
    at javax.faces.component.UIInput.executeValidate(UIInput.java:1334)
    at javax.faces.component.UIInput.processValidators(UIInput.java:757)
    at javax.faces.component.UIForm.processValidators(UIForm.java:269)
    at javax.faces.component.UIComponentBase.processValidators(UIComponentBase.java:1298)
    at javax.faces.component.UIComponentBase.processValidators(UIComponentBase.java:1298)
    at javax.faces.component.UIViewRoot.processValidators(UIViewRoot.java:1332)
    at com.sun.faces.lifecycle.ProcessValidationsPhase.execute(ProcessValidationsPhase.java:77)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:100)
    at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:201)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:670)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
    at io.opentracing.contrib.jaxrs2.server.SpanFinishingFilter.doFilter(SpanFinishingFilter.java:55)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:292)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:138)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:272)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:104)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:360)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1985)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1487)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1378)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Double
    at org.hibernate.validator.internal.constraintvalidators.bv.number.bound.decimal.DecimalMinValidatorForDouble.compare(DecimalMinValidatorForDouble.java:17)
    at org.hibernate.validator.internal.constraintvalidators.bv.number.bound.decimal.AbstractDecimalMinValidator.isValid(AbstractDecimalMinValidator.java:51)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:171)
    ... 69 more

UPDATE: the error depends on the user input. In case, a number with fraction digits is entered, everthing works as expected, but as soon, as a number without fraction digits is entered, the described error occurs

jheider
  • 111
  • 8
  • Please provide the full stack trace. And your `h:inputText` is wired mixing html, jsf and passthrough attributes. – Selaron Feb 06 '19 at 14:29
  • added the full stack trace. Whats the problem with the `h:inputText`? It always worked that way and I assume this is what passthrough was made for? – jheider Feb 06 '19 at 14:40
  • found this in the meantime were it was mentioned as bug in older JSF versions: https://stackoverflow.com/questions/2919249/jsf-hinputtext-and-double-value – jheider Feb 07 '19 at 10:12
  • Does that answer solve your problem? – Selaron Feb 07 '19 at 10:20
  • not really. Yes, the error is gone with that workaround. But then, it does not allow the localized input (or output when loading an existing entity in form). So it is not really a solution. – jheider Feb 07 '19 at 10:34
  • UPDATE: the error depends on the user input. In case, a number with fraction digits is entered, everthing works as expected, but as soon, as a number without fraction digits is entered, the described error occurs. – jheider Feb 12 '19 at 09:26
  • Out of curiosity, why do you have this convertNumber anyway? If the area variable is of type Double, JSF should automatically convert it. And since you don't have a pattern or type on the converter (for formatting e.g.) I see no need for the converter anyway. – Kukeltje Feb 12 '19 at 09:46
  • And what is your specific JSF version (2.3.x) and implementation (Mojarra/MyFaces) – Kukeltje Feb 12 '19 at 09:49
  • And I found _"When binding the NumberConverter to a component, ensure that the managed bean property to which the component is bound is of a primitive type or has a type of java.lang.Number. In the preceding example, cart.total is of type double."_ in the 'specs https://docs.oracle.com/javaee/7/tutorial/jsf-page-core001.htm – Kukeltje Feb 12 '19 at 09:56
  • I'm using it because of of localized output/input of the double value, otherwise the values is converted correctly, but when the form is loaded, the value is not localized (comma instead of dot as decimal separator in that case). at least I don't know any other solution to do this. I'am using Mojarra 2.3.5.SP2 which is bundled with Wildfly 15.0.1. – jheider Feb 12 '19 at 10:01
  • but I don't see a locale attribute being used on it. And if you want to have decimals to, you could (should) use a fracionDigits maybe? Now it all seems a bit 'undefined'' I'll try to see if I can find the time to (try to) reproduce – Kukeltje Feb 12 '19 at 10:03
  • the "global" local is set in faces-config. using a primitive double makes no difference. I tried to set the fraction digits value with min=0, as well as a pattern. The user should be able to not always add ",00" in case there are no fraction digits. As I already said, that was working fine in JSF 2.2, so I assume the error is somewhere else or I miss something else which is new to 2.3. – jheider Feb 12 '19 at 10:11
  • The source of both [2.3.x](https://github.com/javaserverfaces/mojarra/blob/master/impl/src/main/java/javax/faces/convert/NumberConverter.java ) 2.2.x is open. You can compare their sources... (like I'm doing now) – Kukeltje Feb 12 '19 at 11:18
  • Are you btw sure you just upgraded from JSF 2.2 to 2.3? Not by accident an upgrade from an earlier JDK to a more recent one? – Kukeltje Feb 13 '19 at 08:35
  • No, we switched from Java EE 7 (wildFly 10) to Java EE 8 (wildFly 15), JDK is the same. So maybe it could also be at bean validation side?! Btw: we tested with JDK 8 and 11, same issue – jheider Feb 13 '19 at 09:14
  • I'l create an answer, not that it explains the difference, but it does have relevant info and is to extensive for a comment. – Kukeltje Feb 13 '19 at 11:59

2 Answers2

1

I've looked at the sourcecode of the NumberConverter of both JSF 2.2 and 2.3 and noticed no serious differences (just comments, copyrights etc). I isolated the code that does the actual conversion and isolated this in some very small plain java code.

NumberFormat nf = NumberFormat.getNumberInstance(Locale.getDefault());

System.out.println(nf.getClass());
System.out.println(Double.class.isAssignableFrom(BigDecimal.class));
System.out.println(nf.parse("10,0")+ " : " + nf.parse("10,0").getClass());
System.out.println(nf.parse("10,1")+ " : " + nf.parse("10,1").getClass());
System.out.println(nf.parse("10.0")+ " : " + nf.parse("10.0").getClass());
System.out.println(nf.parse("10.1")+ " : " + nf.parse("10.1").getClass());

Which results in the folowing output.

class java.text.DecimalFormat
false
100 : class java.lang.Long
101 : class java.lang.Long
10 : class java.lang.Long
10.1 : class java.lang.Double

I initially ran this on JDK 8, but also tried 7 later, both with the same results.

So I'd be inclined to draw the conclusion that it is not JSF that causing problems (although I'd say that since the converter uses the expected type to do some checking it would not be strange to return the expected type and not the type returned by the NumberFormat.

But when setting the parseBigDecimal to true

((DecimalFormat)nf).setParseBigDecimal(true);

And running the same 'tests', there is no loss in presision like mentioned in the converter and the output is

100 : class java.math.BigDecimal
101 : class java.math.BigDecimal
10.0 : class java.math.BigDecimal
10.1 : class java.math.BigDecimal

But this cannot be cast to a Double either. So I'd personally start looking into the BeanValidator code.

Kukeltje
  • 12,223
  • 4
  • 24
  • 47
  • Yeah, but the thing with th expected type is only done for BigDecimal. Maybe there should be a conversion to the expected type at the end? I also had a look in Bean Validation code, but didn't get it all, yet. Seems, that they changed a lot and call the required validator based on expected type? – jheider Feb 28 '19 at 10:05
0

This is still relevant with Mojarra 4. Now, when using f:convertNumber on a field of type Short, a ClassCastException Long to Short is thrown.

jheider
  • 111
  • 8