jsfjsf-2.2viewparams

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


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";
    }
}

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.