includeViewParams=true converts null model value to empty string in query string

jsf, jsf-2.2, viewparams

Solution

This is likely an oversight in Mojarra's default implementation of `UIViewParameter#getStringValueFromModel()` whose source is for reference copypasted below:

384    public String getStringValueFromModel(FacesContext context)
385        throws ConverterException {
386        ValueExpression ve = getValueExpression("value");
387        if (ve == null) {
388            return null;
389        }
390
391        Object currentValue = ve.getValue(context.getELContext());
392
393        // If there is a converter attribute, use it to to ask application
394        // instance for a converter with this identifer.
395        Converter c = getConverter();
396
397        if (c == null) {
398            // if value is null and no converter attribute is specified, then
399            // return null (null has meaning for a view parameters; it means remove it).
400            if (currentValue == null) {
401                return null;
402            }
403            // Do not look for "by-type" converters for Strings
404            if (currentValue instanceof String) {
405                return (String) currentValue;
406            }
407
408            // if converter attribute set, try to acquire a converter
409            // using its class type.
410
411            Class converterType = currentValue.getClass();
412            c = context.getApplication().createConverter(converterType);
413
414            // if there is no default converter available for this identifier,
415            // assume the model type to be String.
416            if (c == null) {
417                return currentValue.toString();
418            }
419        }
420
421        return c.getAsString(context, this, currentValue);
422    }

This method is called for every `UIViewParameter` (the UI component behind `<f:viewParam>`) during building the query string for `includeViewParams=true`. We see in the source that it calls the converter regardless of whether `currentValue` is `null` or not. In other words, even if the model value is `null`, it still calls the converter with it.

As per the javadoc of `Converter#getAsString()` converters are by specification required to return a zero-length String if value is `null`:

getAsString

...

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

So, converters are actually supposed to never return `null` on `getAsString()`. They return an empty string then. In case of view parameters in query string, this is highly undesirable. The difference between an empty string value and a complete absence in query string is really significant.

I've reported it to Mojarra guys as issue 3288. They should then fix this problem as follows:

391        Object currentValue = ve.getValue(context.getELContext());
392
393        if (currentValue == null) {
394            return null;
395        }

In the meanwhile, I've committed a solution to OmniFaces. The `<o:viewParam>` has been extended with this fix. It's available as per today's 1.8 snapshot.

<f:metadata>
    <o:viewParam name="id" value="#{testManagedBean.id}" converter="javax.faces.Long"/>
</f:metadata>

Update: they decided to not fix it. In any case, there's OmniFaces.

Problem

Given a `<p:selectOneMenu>` as follows. ``` <f:metadata> <f:viewParam name="id" value="#{testManagedBean.id}" converter="javax.faces.Long"/> </f:metadata> <p:selectOneMenu value="#{localeBean.language}" onchange="changeLanguage();"> <f:selectItem itemValue="en" itemLabel="English" /> <f:selectItem itemValue="hi" itemLabel="Hindi" /> </p:selectOneMenu> <p:remoteCommand action="#{testManagedBean.submitAction}" name="changeLanguage" process="@this" update="@none"/> ``` The corresponding managed bean : ``` @ManagedBean @RequestScoped public final class TestManagedBean { private Long id; //Getter and setter. public TestManagedBean() {} public String submitAction() { return FacesContext.getCurrentInstance().getViewRoot().getViewId() + "?faces-redirect=true&includeViewParams=true"; } } ``` The parameter as indicated by `<f:viewParam>` is optional. A page, for example is accessed using a URL as follows. `https://localhost:8181/Project-war/private_resources/Test.jsf` Since `id` is an optional parameter, an empty parameter is attached to the URL (when a language is changed from `<p:selectOneMenu>`), in case it is not supplied as follows. `https://localhost:8181/Project-war/private_resources/Test.jsf?id=` This should not happen. An empty parameter should not be appended, if it is not supplied and the URL should look like the first one. Is there a way to prevent an empty parameter from being appended to the URL, when it is not passed? This is only associated with the converter as specified with `<f:viewParam>` - `javax.faces.Long`. If this converter is removed then, parameters are not appended to the URL, in case no parameters are supplied. Although specifying a converter as demonstrated here is completely unnecessary, I have converters as shown below to convert an `id` passed though the URL as a query-string parameter to a JPA entity. ``` @ManagedBean @RequestScoped public final class ZoneConverter implements Converter { @EJB private final SharableBeanLocal sharableService = 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, "Message Summary", "Message")); } ZoneTable entity = sharableService.findZoneById(parsedValue); if (entity == null) { throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_WARN, "Message Summary", "Message")); } return entity; } catch (NumberFormatException e) { throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Message Summary", "Message"), e); } } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { return value instanceof ZoneTable ? ((ZoneTable) value).getZoneId().toString() : ""; } } ``` This converter is now required to be specified explicitly with `<f:viewParam>` as follows. ``` <f:viewParam name="id" value="#{testManagedBean.id}" converter="#{zoneConverter}" rendered="#{not empty param.id}"/> ``` And the associated managed bean needs to be changed as follows. ``` @ManagedBean @RequestScoped public final class TestManagedBean { private ZoneTable id; //Getter and setter. public TestManagedBean() {} public String submitAction() { return FacesContext.getCurrentInstance().getViewRoot().getViewId() + "?faces-redirect=true&includeViewParams=true"; } } ```

Original source