includeViewParams = true将空模型值转换为查询字符串中的空字符串

问题描述:

给出一个<p:selectOneMenu>如下.

<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"/>

相应的托管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";
    }
}

<f:viewParam>指示的参数是可选的.例如,使用URL如下访问页面.

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

由于id是可选参数,因此在未提供以下内容的情况下,会将URL附加空参数(当语言从<p:selectOneMenu>更改时).

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=

这不应该发生.如果未提供空参数,则不应附加空参数,并且URL看起来应该像第一个一样.

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.

有没有一种方法可以防止在不传递空参数时将其附加到URL?

Is there a way to prevent an empty parameter from being appended to the URL, when it is not passed?

这仅与<f:viewParam>-javax.faces.Long指定的转换器相关联.

This is only associated with the converter as specified with <f:viewParam> - javax.faces.Long.

如果删除了此转换器,则在不提供任何参数的情况下,不会将参数附加到URL.

If this converter is removed then, parameters are not appended to the URL, in case no parameters are supplied.

尽管完全没有必要像此处演示的那样指定转换器,但我具有如下所示的转换器,可以将通过URL传递的id作为查询字符串参数转换为JPA实体.

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() : "";
    }
}

现在需要使用<f:viewParam>明确指定此转换器,如下所示.

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

关联的受管Bean需要进行如下更改.

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

这可能是 UIViewParameter#getStringValueFromModel() ,其来源供参考复制粘贴如下:

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    }

在为includeViewParams=true构建查询字符串期间,每个UIViewParameter(位于<f:viewParam>之后的UI组件)都会调用此方法.我们在源代码中看到,无论currentValue是否为null,它都将调用转换器.换句话说,即使模型值是null,它仍然会使用它来调用转换器.

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.

根据

getAsString

...

getAsString

...

返回:如果值为null,则为零长度的字符串,否则为转换结果

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

因此,转换器实际上应该永远不会在getAsString()上返回null.然后,他们返回一个空字符串.对于查询字符串中的视图参数,这是非常不希望的.空字符串值和查询字符串完全不存在之间的区别确实非常重要.

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.

我已经以 issue 3288 的形式向Mojarra的人报告了它.然后,他们应按以下步骤解决此问题:

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        }

同时,我已经提交了 OmniFaces解决方案.此修复程序扩展了 <o:viewParam> .根据今天的 1.8快照可用.

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>


更新:他们决定不对其进行修复.无论如何,都有OmniFaces.


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