27

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.

Community
  • 1
  • 1
Tiny
  • 27,221
  • 105
  • 339
  • 599

6 Answers6

34

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.

Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Along with this converter, I have upgraded OmniFaces to [1.8.1](http://central.maven.org/maven2/org/omnifaces/omnifaces/1.8.1/) in which `` also works intuitively. I however always afraid of one thing in general - Is it not harmful to inject an EJB in an application scoped bean as shown in the question? – Tiny Jun 20 '14 at 19:40
  • Nope. EJBs (and CDI managed beans, but **not** JSF managed beans) use the proxy pattern. The actual instance being injected is just a proxy. It does nothing else than locating the currently available instance on a per method call basis. Given that you're familiar with JSF, you can imagine it roughly as if internally using `FacesContext.getCurrentInstance()` and then grabbing the bean from the desired scope map, invoking the method on it and finally returning its result, if any. Such a proxy instance can perfectly be application scoped. It's is autogenerated by the container using reflection. – BalusC Jun 20 '14 at 21:29
  • Note that EJB and CDI actually doesn't use `FacesContext` this way, but have their own EJB and CDI context things (`EJBContext` and `BeanManager` respectively). Anyway, you should get the picture now. – BalusC Jun 20 '14 at 21:34
  • Thank you for this clarification. In this converter, I personally prefer to catch the `NumberFormatException` instead of relying upon the given regex because it doesn't guard against, when a given number is outside the range of a given type - `Long` in this case which might be supplied by a malicious user :) – Tiny Jun 21 '14 at 22:53
  • It was a kickoff example using standard APIs, but you're right. I updated the answer to utilize OmniFaces `Utils#isNumber()`. – BalusC Jun 24 '14 at 07:27
  • Returning null used to work with Mojarra 2.1.28. When did this change? – Demonblack Jul 03 '18 at 13:40
5
  • If you want to avoid null values for your select component, the most elegant way is to use the noSelectionOption.

When noSelectionOption="true", the converter will not even try to process the value.

Plus, when you combine that with <p:selectOneMenu required="true"> you will get a validation error, when user tries to select that option.

One final touch, you can use the itemDisabled attribute to make it clear to the user that he can't use this option.

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

    <f:selectItem itemLabel="Select"
                  noSelectionOption="true"
                  itemDisabled="true"/>

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

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

<p:message for="cmbCountry"/>
  • Now if you do want to be able to set a null value, you can 'cheat' the converter to return a null value, by using

    <f:selectItem itemLabel="Select" itemValue="" />
    

More reading here, here, or here

Community
  • 1
  • 1
yannicuLar
  • 3,083
  • 3
  • 32
  • 50
2

You're mixing a few things, and it's not fully clear to me what you want to achieve, but let's try

This obviously causes the java.lang.NumberFormatException to be thrown in its converter.

It's nothing obvious in it. You don't check in converter if value is empty or null String, and you should. In that case the converter should return null.

Why does it render Select (itemLabel) as its value and not an empty string (itemValue)?

The select must have something selected. If you don't provide empty value, the first element from list would be selected, which is not something that you would expect.

Just fix the converter to work with empty/null strings and let the JSF react to returned null as not allowed value. The conversion is called first, then comes the validation.

I hope that answers your questions.

Danubian Sailor
  • 1
  • 38
  • 145
  • 223
  • Inside the `getAsObject()` method, I have changed the `return` statement like, `return StringUtils.isNotBlank(value)&&StringUtils.isNumeric(value)?sharableService.find(Long.parseLong(value)):null;` where the class `org.apache.commons.lang.StringUtils` is held by an external library - `Apache Common Lang`. It worked fine with this change, the `required` constraint also worked but why does `` render `Select` as its value instead of rendering an empty string? Does it render `itemLabel` instead of rendering `itemValue`? – Tiny Jun 11 '13 at 21:27
  • No I'm getting `itemLabel` (not `itemValue`) in the converter which I don't expect. I put a statement inside the `getAsObject()` method, `System.out.println("value = "+value)`. It displays, `value = Select` (i.e. `itemLabel`). Is this right? I can't to come to believe this. This happens only with an embedded `` and not with ``. – Tiny Jun 13 '13 at 18:27
  • OK I've not written it clear, you convert between itemLabel and itemValue in converter. So in one direction you become itemValue, and must give itemLabel for it, and in the other direction you must find itemValue for itemLabel. – Danubian Sailor Jun 14 '13 at 14:10
1

In addition to incompleteness, this answer was deprecated, since I was using Spring at the time of this post :

I have modified the converter's getAsString() method to return an empty string instead of returning null, when no Country object is found like (in addition to some other changes),

@Controller
@Scope("request")
public final class CountryConverter implements Converter {

    @Autowired
    private final transient Service service = null;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        try {
            long parsedValue = Long.parseLong(value);

            if (parsedValue <= 0) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "The id cannot be zero or negative."));
            }

            Country country = service.findCountryById(parsedValue);

            if (country == null) {
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_WARN, "", "The supplied id doesn't exist."));
            }

            return country;
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "", "Conversion error : Incorrect id."), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value instanceof Country ? ((Country) value).getCountryId().toString() : ""; //<--- Returns an empty string, when no Country is found.
    }
}

And <f:selectItem>'s itemValue to accept a null value as follows.

<p:selectOneMenu id="cmbCountry"
                 value="#{stateManagedBean.selectedItem}"
                 required="true">

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

    <f:selectItems var="country"
                   converter="#{countryConverter}"
                   value="#{stateManagedBean.selectedItems}"
                   itemLabel="#{country.countryName}"
                   itemValue="${country}"/>
</p:selectOneMenu>

<p:message for="cmbCountry"/>

This generates the following HTML.

<select id="form:cmbCountry_input" name="form:cmbCountry_input">
    <option value="" selected="selected">Select</option>
    <option value="56">Country1</option>
    <option value="55">Country2</option>
</select>

Earlier, the generated HTML looked like,

<select id="form:cmbCountry_input" name="form:cmbCountry_input">
    <option selected="selected">Select</option>
    <option value="56">Country1</option>
    <option value="55">Country2</option>
</select>

Notice the first <option> with no value attribute.

This works as expected bypassing the converter when the first option is selected (even though require is set to false). When itemValue is changed to other than null, then it behaves unpredictably (I don't understand this).

No other items in the list can be selected, if it is set to a non-null value and the item received in the converter is always an empty string (even though another option is selected).

Additionally, when this empty string is parsed to Long in the converter, the ConverterException which is caused after the NumberFormatException is thrown doesn't report the error in the UIViewRoot (at least this should happen). The full exception stacktrace can be seen on the server console instead.

If someone could expose some light on this, I would accept the answer, if it is given.

Tiny
  • 27,221
  • 105
  • 339
  • 599
-1

This is fully working to me :

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

    <f:selectItem itemLabel="Select"/>

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

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

The Converter

@Controller
@Scope("request")
public final class CountryConverter implements Converter {

    @Autowired
    private final transient Service service = null;

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

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return value == null ? null : value instanceof Country ? ((Country) value).getCountryId().toString() : null;
        //**** Returns an empty string, when no Country is found ---> wrong should return null, don't care about the rendering.
    }
}
Tiny
  • 27,221
  • 105
  • 339
  • 599
Nassim MOUALEK
  • 4,702
  • 4
  • 25
  • 44
-3
public void limparSelecao(AjaxBehaviorEvent evt) {
    Object submittedValue = ((UIInput)evt.getSource()).getSubmittedValue();

    if (submittedValue != null) {
        getPojo().setTipoCaixa(null);
    }
}
<p:selectOneMenu id="tipo"
                 value="#{cadastroCaixaMonitoramento.pojo.tipoCaixa}" 
                 immediate="true"
                 required="true"
                 valueChangeListener="#{cadastroCaixaMonitoramento.selecionarTipoCaixa}">

    <f:selectItem itemLabel="Selecione" itemValue="SELECIONE" noSelectionOption="false"/>

    <f:selectItems value="#{cadastroCaixaMonitoramento.tiposCaixa}" 
                   var="tipo" itemValue="#{tipo}"
                   itemLabel="#{tipo.descricao}" />

    <p:ajax process="tipo"
            update="iten_monitorado"
            event="change" listener="#{cadastroCaixaMonitoramento.limparSelecao}" />
</p:selectOneMenu>
Tiny
  • 27,221
  • 105
  • 339
  • 599
Gleidosn
  • 193
  • 1
  • 6