Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSF converter causes validator(s) to be ignored

Tags:

java

jsf-2

Here's the field:

<h:inputText id="mobilePhoneNo"
             value="#{newPatientBean.phoneNo}"
             required="true"
             requiredMessage="Required"
             validator="#{mobilePhoneNumberValidator}"
             validatorMessage="Not valid (validator)"
             converter="#{mobilePhoneNumberConverter}"
             converterMessage="Not valid (converter)"
             styleClass="newPatientFormField"/>

And the validator:

@Named
@ApplicationScoped
public class MobilePhoneNumberValidator implements Validator, Serializable
{
    @Override
    public void validate(FacesContext fc, UIComponent uic, Object o) throws ValidatorException
    {
        // This will appear in the log if/when this method is called.
        System.out.println("mobilePhoneNumberValidator.validate()");

        UIInput in = (UIInput) uic;
        String value = in.getSubmittedValue() != null ? in.getSubmittedValue().toString().replace("-", "").replace(" ", "") : "";

        if (!value.matches("04\\d{8}"))
        {
            throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Please enter a valid mobile phone number.", null));
        }
    }
}

When I press the command button within the form, I get the following behaviour:

  • When the field is blank, the message is "Not valid (converter)".
  • When the field has a valid entry, the message is "Not valid (validator)".
  • When the field has an invalid entry, the message is "Not valid (converter)".

In all three cases, MobilePhoneNumberConverter.getAsObject() is called. MobilePhoneNumberValidator.validate() is never called. And when the field is blank, it ignores the required="true" attribute and proceeds straight to conversion.

I would have thought the proper behaviour would be:

  • When the field is blank, the message should be "Required".
  • When the field has a valid entry, there should be no message at all.
  • When the field has an invalid entry, the message should be "Not valid (validator)".
  • If, by some chance, validation passed by conversion didn't, the message should be "Not valid (converter)".

Note: The backing bean is request scoped, so there's no fancy AJAX business going on here.

Update:

Might it have something to do with javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL being set to true?

like image 687
Steve Avatar asked Jan 03 '12 22:01

Steve


2 Answers

Conversion happens before validation. Converters will also be called when the value is null or empty. If you want to delegate the null value to the validators, then you need to design your converters that it just returns null when the supplied value is null or empty.

@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
    if (value == null || value.trim().isEmpty()) {
        return null;
    }

    // ...
}

Unrelated to the concrete problem, your validator has a flaw. You should not extract the submitted value from the component. It's not the same value as returned by the converter. The right submitted and converted value is available as the 3rd method argument already.

@Override
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
    if (value == null) {
        return; // This should normally not be hit when required="true" is set.
    }

    String phoneNumber = (String) value; // You need to cast it to the same type as returned by Converter, if any.

    if (!phoneNumber.matches("04\\d{8}")) {
        throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Please enter a valid mobile phone number.", null));
    }
}
like image 83
BalusC Avatar answered Nov 14 '22 17:11

BalusC


After reading BalusC's comment I am updating this post again.

I created a small demo application and to see the phases and when the conversion and validation occur.

View:

<h:form>
    <h:inputText value="#{demoBean.field}">
        <f:converter converterId="demoConverter"/>
        <f:validator validatorId="demoValidator"/>
    </h:inputText>
    <h:commandButton value="Submit" action="#{demoBean.demoAxn()}"/>
</h:form>

Managed bean:

@ManagedBean
@SessionScoped
public class DemoBean implements Serializable {
    private String field;

    public DemoBean() {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
    }

    public String getField() {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
        return field;
    }

    public void setField(String field) {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
        this.field = field;
    }

    public String demoAxn() {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
        return null;
    }
}

Converter:

@FacesConverter(value="demoConverter")
public class DemoConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        System.out.println(Thread.currentThread().getStackTrace()[1]);            
        return value;
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
        return (String) value;
    }    
}

Validator:

@FacesValidator(value="demoValidator")
public class DemoValidator implements Validator {

    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
    }

}

Phase listener:

public class DemoPhaseListener implements PhaseListener {
    @Override
    public void afterPhase(PhaseEvent event) {
        System.out.println(Thread.currentThread().getStackTrace()[1]);
        System.out.println("PhaseId: " + event.getPhaseId() + "  ===============================\n\n");        
    }

    @Override
    public void beforePhase(PhaseEvent event) {
        System.out.println("\n\nPhaseId: " + event.getPhaseId() + "  ===============================");
        System.out.println(Thread.currentThread().getStackTrace()[1]);        
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.ANY_PHASE;
    }    
}

Registered the phase listener:

<lifecycle>
    <phase-listener>pkg.DemoPhaseListener</phase-listener>
</lifecycle>

With that setting when the "Submit" button is clicked, the output is:

INFO: PhaseId: RESTORE_VIEW 1  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: RESTORE_VIEW 1  ===============================

INFO: PhaseId: APPLY_REQUEST_VALUES 2  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: APPLY_REQUEST_VALUES 2  ===============================

INFO: PhaseId: PROCESS_VALIDATIONS 3  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoConverter.getAsObject(DemoConverter.java:13)
INFO: pkg.DemoValidator.validate(DemoValidator.java:14)
INFO: pkg.DemoBean.getField(DemoBean.java:17)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: PROCESS_VALIDATIONS 3  ===============================

INFO: PhaseId: UPDATE_MODEL_VALUES 4  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoBean.setField(DemoBean.java:22)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: UPDATE_MODEL_VALUES 4  ===============================

INFO: PhaseId: INVOKE_APPLICATION 5  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoBean.demoAxn(DemoBean.java:27)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: INVOKE_APPLICATION 5  ===============================

INFO: PhaseId: RENDER_RESPONSE 6  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoBean.getField(DemoBean.java:17)
INFO: pkg.DemoConverter.getAsString(DemoConverter.java:20)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: RENDER_RESPONSE 6  ===============================

But when make change to throw a NPE in the converter as follows:

@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
    System.out.println(Thread.currentThread().getStackTrace()[1]);            
    throw new NullPointerException();
}

the output is:

INFO: PhaseId: RESTORE_VIEW 1  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: RESTORE_VIEW 1  ===============================

INFO: PhaseId: APPLY_REQUEST_VALUES 2  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: APPLY_REQUEST_VALUES 2  ===============================

INFO: PhaseId: PROCESS_VALIDATIONS 3  ===============================
INFO: pkg.DemoPhaseListener.beforePhase(DemoPhaseListener.java:17)
INFO: pkg.DemoConverter.getAsObject(DemoConverter.java:13)
INFO: pkg.DemoPhaseListener.afterPhase(DemoPhaseListener.java:10)
INFO: PhaseId: PROCESS_VALIDATIONS 3  ===============================

INFO: pkg.DemoBean.getField(DemoBean.java:17)

But the stacktrace is displayed on the resulting view.

like image 20
Bhesh Gurung Avatar answered Nov 14 '22 18:11

Bhesh Gurung