Using a "Please select" f:selectItem with null/empty value inside a p:selectOneMenu

Tiny picture Tiny · Jun 11, 2013 · Viewed 54.6k times · Source

I'm populating a <p:selectOneMenu/> from database as follows.

<p:selectOneMenu id="cmbCountry" 
                 value="#{bean.country}"
                 required="true"
                 converter="#{countryConverter}">

    <f:selectItem itemLabel="Select" itemValue="#{null}"/>

    <f:selectItems var="country"
                   value="#{bean.countries}"
                   itemLabel="#{country.countryName}"
                   itemValue="#{country}"/>

    <p:ajax update="anotherMenu" listener=/>
</p:selectOneMenu>

<p:message for="cmbCountry"/>

The default selected option, when this page is loaded is,

<f:selectItem itemLabel="Select" itemValue="#{null}"/>

The converter:

@ManagedBean
@ApplicationScoped
public final class CountryConverter implements Converter {

    @EJB
    private final Service service = null;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        try {
            //Returns the item label of <f:selectItem>
            System.out.println("value = " + value);

            if (!StringUtils.isNotBlank(value)) {
                return null;
            } // Makes no difference, if removed.

            long parsedValue = Long.parseLong(value);

            if (parsedValue <= 0) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Message"));
            }

            Country entity = service.findCountryById(parsedValue);

            if (entity == null) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_WARN, "", "Message"));
            }

            return entity;
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Message"), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value instanceof Country ? ((Country) value).getCountryId().toString() : null;
    }
}

When the first item from the menu represented by <f:selectItem> is selected and the form is submitted then, the value obtained in the getAsObject() method is Select which is the label of <f:selectItem> - the first item in the list which is intuitively not expected at all.

When the itemValue attribute of <f:selectItem> is set to an empty string then, it throws java.lang.NumberFormatException: For input string: "" in the getAsObject() method even though the exception is precisely caught and registered for ConverterException.

This somehow seems to work, when the return statement of the getAsString() is changed from

return value instanceof Country?((Country)value).getCountryId().toString():null;

to

return value instanceof Country?((Country)value).getCountryId().toString():"";

null is replaced by an empty string but returning an empty string when the object in question is null, in turn incurs another problem as demonstrated here.

How to make such converters work properly?

Also tried with org.omnifaces.converter.SelectItemsConverter but it made no difference.

Answer

BalusC picture BalusC · Jun 20, 2014

When the select item value is null, then JSF won't render <option value>, but only <option>. As consequence, browsers will submit the option's label instead. This is clearly specified in HTML specification (emphasis mine):

value = cdata [CS]

This attribute specifies the initial value of the control. If this attribute is not set, the initial value is set to the contents of the OPTION element.

You can also confirm this by looking at HTTP traffic monitor. You should see the option label being submitted.

You need to set the select item value to an empty string instead. JSF will then render a <option value="">. If you're using a converter, then you should actually be returning an empty string "" from the converter when the value is null. This is also clearly specified in Converter#getAsString() javadoc (emphasis mine):

getAsString

...

Returns: a zero-length String if value is null, otherwise the result of the conversion

So if you use <f:selectItem itemValue="#{null}"> in combination with such a converter, then a <option value=""> will be rendered and the browser will submit just an empty string instead of the option label.

As to dealing with the empty string submitted value (or null), you should actually let your converter delegate this responsibility to the required="true" attribute. So, when the incoming value is null or an empty string, then you should return null immediately. Basically your entity converter should be implemented like follows:

@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
    if (value == null) {
        return ""; // Required by spec.
    }

    if (!(value instanceof SomeEntity)) {
        throw new ConverterException("Value is not a valid instance of SomeEntity.");
    }

    Long id = ((SomeEntity) value).getId();
    return (id != null) ? id.toString() : "";
}

@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
    if (value == null || value.isEmpty()) {
        return null; // Let required="true" do its job on this.
    }

    if (!Utils.isNumber(value)) {
        throw new ConverterException("Value is not a valid ID of SomeEntity.");
    }

    Long id = Long.valueOf(value);
    return someService.find(id);
}

As to your particular problem with this,

but returning an empty string when the object in question is null, in turn incurs another problem as demonstrated here.

As answered over there, this is a bug in Mojarra and bypassed in <o:viewParam> since OmniFaces 1.8. So if you upgrade to at least OmniFaces 1.8.3 and use its <o:viewParam> instead of <f:viewParam>, then you shouldn't be affected anymore by this bug.

The OmniFaces SelectItemsConverter should also work as good in this circumstance. It returns an empty string for null.