includeViewParams=true doesn't work in combination with a composite component which iterates over a collection

StackOverflow https://stackoverflow.com/questions/9085263

  •  21-04-2021
  •  | 
  •  

Question

NOTE: This question has been heavily edited as a contrived example has been found to work. However, the problem remains essentially the same as only the contrived example appears to work.

Consider the following JSF page:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets">
    <f:view contentType="text/html">
        <h:head>
            <title>Page</title>
        </h:head>
        <h:body>
            <ui:composition template="/WEB-INF/templates/myLayout.xhtml">
                <ui:define name="metadata">
                    <f:metadata>
                        <f:viewParam name="foobar" value="#{pageBean.foo}"/>
                    </f:metadata>
                </ui:define>
                <ui:define name="content">
                    <h:form>
                        <h:commandLink value="Click"
                                    action="#{util.currentPageAction()}"/>
                    </h:form>
                </ui:define>
            </ui:composition>
        </h:body>
    </f:view>
</html>

And these beans:

@Named
@RequestScoped
public class PageBean implements Serializable
{
    public String getFoo()
    {
        return foo;
    }

    public void setFoo(String foo)
    {
        this.foo = foo;
    }

    private String foo;
}
@Named
@ApplicationScoped
public class Util implements Serializable
{
    public String currentPageAction()
    {
        return FacesContext.getCurrentInstance().getViewRoot().getViewId() +
                   "?faces-redirect=true&includeViewParams=true";
    }
}

This contrived example actually does as expected - in particular, it passes through the original values of the view parameters to the URL upon redirect. So, for example, if the original URL is http://localhost:8080/faces/pages/test.xhtml?foo=bar, when I click on the <h:commandLink/> the full URL - including the view parameters - will survive the redirect.

The problem is, when I go from this contrived example to something more real-world (in my case, a search page), the view parameters do not survive the redirect.

Here's the real-world page for which it doesn't work. Sorry, it's a bit lengthy for a typical stackoverflow question.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:p="http://primefaces.org/ui"
      xmlns:search="http://java.sun.com/jsf/composite/components/search"
      xmlns:oc="http://java.sun.com/jsf/composite/components/mysite"
      xmlns:fn="http://www.mysite.com.au/jsf">
    <h:head>
        <title></title>
    </h:head>
    <h:body>
        <ui:composition template="/WEB-INF/templates/myLayout.xhtml">
            <ui:define name="metadata">
                <f:metadata>
                    <f:viewParam name="address" value="#{searchBean.address}"/>
                    <f:viewParam name="lng" value="#{searchBean.longitude}" required="#{!facesContext.postback}" requiredMessage="Longitude is required" validatorMessage="Longitude is invalid">
                        <f:validateDoubleRange minimum="-180" maximum="180"/>
                    </f:viewParam>
                    <f:viewParam name="lat" value="#{searchBean.latitude}" required="#{!facesContext.postback}" requiredMessage="Latitude is required" validatorMessage="Latitude is invalid">
                        <f:validateDoubleRange minimum="-90" maximum="90"/>
                    </f:viewParam>
                    <f:viewParam name="gender" value="#{searchBean.gender}"/>
                    <f:viewParam name="language" value="#{searchBean.spokenLanguageId}"/>
                    <f:viewParam name="service" value="#{searchBean.serviceId}"/>
                    <f:viewParam name="rangeKm" value="#{searchBean.rangeKm}" validatorMessage="Range (km) is invalid">
                        <f:validateLongRange minimum="1"/>
                    </f:viewParam>
                    <f:viewParam name="weeksOffset" value="#{searchBean.weeksOffset}"/>
                    <f:event type="preRenderView" listener="#{searchBean.preRender(e)}"/>
                </f:metadata>
            </ui:define>
            <ui:define name="windowTitle">#{searchBean.address}</ui:define>
            <ui:define name="scripts">
                <script type="text/javascript">
                    var mysite = mysite || {};
                    mysite.longitude      = #{searchBean.longitude};
                    mysite.latitude       = #{searchBean.latitude};
                    mysite.defaultAddress = "#{applicationBean.defaultAddress}";
                    mysite.region         = "#{applicationBean.region}";
                    mysite.baseUrl        = "#{applicationBean.baseUrl}";
                </script>
                <h:outputScript library="javascript" name="map2.js"/>
                <h:outputScript name="geocode.js" library="javascript" target="head"/>
                <h:outputScript name="search.js" library="javascript" target="head"/>
            </ui:define>

            <ui:define name="rightSidebar">
                <div class="ocSearch_sidebarSection">
                    <oc:map styleClass="search"/>
                    <div>
                        <a style="font-size:11px;text-decoration:underline;" href="#" onclick="mysite.resetMap();return false;">Reset</a>
                    </div>
                </div>

                <oc:context-menu-container isFirstChild="false">
                    <oc:context-menu title="Search Options">
                        <form id="searchForm" method="get" onsubmit="return mysite.geocode(this);">
                            <div style="clear:both;float:left;">
                                <label for="ocSearchRadius">Service Providers within</label>
                                <div id="ocSearchRadius">
                                    <input type="text" id="ocRangeKm" name="rangeKm" value="#{searchBean.rangeKm}" style="width:20px;float:left;"/>
                                    <div style="width:40px;margin-top:5px;float:left;overflow:hidden;text-align:center;vertical-align:bottom;">km of</div>
                                    <input type="text" id="ocAddress" name="address" value="#{searchBean.address}" style="width:104px;float:left;"/>
                                </div>
                            </div>
                            <div style="float:left;">
                                <div style="width:60px;margin-right:10px;float:left;">
                                    <label for="ocGender" style="display:block;">Gender</label>
                                    <select id="ocGender" name="gender" style="width:50px;">
                                        <h:panelGroup rendered="#{searchBean.gender eq 'any'}">
                                            <option value="any" selected="selected">Any</option>
                                            <option value="female">Female</option>
                                            <option value="male">Male</option>
                                        </h:panelGroup>
                                        <h:panelGroup rendered="#{searchBean.gender eq 'female'}">
                                            <option value="any">Any</option>
                                            <option value="female" selected="selected">Female</option>
                                            <option value="male">Male</option>
                                        </h:panelGroup>
                                        <h:panelGroup rendered="#{searchBean.gender eq 'male'}">
                                            <option value="any">Any</option>
                                            <option value="female">Female</option>
                                            <option value="male" selected="selected">Male</option>
                                        </h:panelGroup>
                                    </select>
                                </div>
                                <div style="float:left;">
                                    <label for="ocLanguage">Language</label>
                                    <select id="ocLanguage" name="language" style="width:176px;">
                                        <ui:repeat value="#{commonRequestBean.spokenLanguageItems}" var="item">
                                            <h:panelGroup rendered="#{searchBean.spokenLanguageId eq item.value}">
                                                <option value="#{item.value}" selected="yes">#{item.label}</option>
                                            </h:panelGroup>
                                            <h:panelGroup rendered="#{searchBean.spokenLanguageId ne item.value}">
                                                <option value="#{item.value}">#{item.label}</option>
                                            </h:panelGroup>
                                        </ui:repeat>
                                    </select>
                                </div>
                            </div>
                            <div style="float:left;">
                                <label for="ocService">Reason for visit</label>
                                <select id="ocService" name="service" style="width:176px;" >
                                    <ui:repeat value="#{commonRequestBean.serviceItems}" var="item">
                                        <h:panelGroup rendered="#{searchBean.serviceId eq item.value}">
                                            <option value="#{item.value}" selected="yes">#{item.label}</option>
                                        </h:panelGroup>
                                        <h:panelGroup rendered="#{searchBean.serviceId ne item.value}">
                                            <option value="#{item.value}">#{item.label}</option>
                                        </h:panelGroup>
                                    </ui:repeat>
                                </select>
                            </div>

                            <div style="float:left;">
                                <label for="ocStartDate">From</label>
                                <select id="ocStartDate" name="weeksOffset" style="width:176px;">
                                    <ui:repeat value="#{fn:selectableDates(13)}" var="date" varStatus="dateStatus">
                                        <h:panelGroup rendered="#{date.value eq searchBean.weeksOffset}">
                                            <option value="#{date.value}" selected="yes">#{date.label}</option>
                                        </h:panelGroup>
                                        <h:panelGroup rendered="#{date.value ne searchBean.weeksOffset}">
                                            <option value="#{date.value}">#{date.label}</option>
                                        </h:panelGroup>
                                    </ui:repeat>
                                </select>
                            </div>

                            <div style="margin-top:8px;float:left;">
                                <p:button id="searchAgainButton" value="Search Again"/>
                                <script type="text/javascript">
                                    //<![CDATA[
                                    $(function(){
                                        // Replace the Search Again button with a clone whose type is submit.
                                        var oldButton = $('#searchAgainButton');
                                        var newButton = oldButton.clone(true);
                                        newButton.attr("type","submit");
                                        newButton.attr("id","searchAgainButtonClone");
                                        newButton.removeAttr("onclick");
                                        newButton.insertBefore(oldButton);
                                        oldButton.remove();
                                        newButton.attr("id","searchAgainButton");
                                    });
                                    //]]>
                                </script>
                            </div>

                            <input type="hidden" id="ocLng"     name="lng"     value="#{searchBean.longitude}"/>
                            <input type="hidden" id="ocLat"     name="lat"     value="#{searchBean.latitude}"/>
                        </form>
                        <script>
                            // Ensure the form fields are set to the current values. (Webkit bug workaround.)
                            $(function(){
                                $('#ocRangeKm').val('#{searchBean.rangeKm}');
                                $('#ocAddress').val('#{searchBean.address}');
                                $('#ocGender').val('#{searchBean.gender}');
                                $('#ocLanguage').val('#{searchBean.spokenLanguageId}');
                                $('#ocService').val('#{searchBean.serviceId}');
                                $('#ocStartDate').val('#{searchBean.weeksOffset}');
                            });
                        </script>
                    </oc:context-menu>
                </oc:context-menu-container>
            </ui:define>

            <ui:define name="content">
                <div id="ocSearchResultsHeader" class="fixed">
                    <div style="margin-left:10px;">
                        <h3 style="margin:8px 0 4px 0;">#{searchBean.searchResultsTitle}</h3>
                        <span class="help">Click a time to book #{searchBean.service.indefiniteArticle} #{searchBean.service.name}</span>
                    </div>
                </div>
                <div class="ocSearch_resultSet">
                    <h:form>
                        <h:commandLink value="Foobar" action="#{util.currentPageAction}"/>
                    </h:form>
                    <search:result resultSet="#{searchBean.results}" startDate="#{searchBean.startDate}"/>
                </div>
            </ui:define>
        </ui:composition>
    </h:body>
</html>

The underlying bean, searchBean, is request scoped just like the contrived example.

Update: If I remove the <search:result/> composite component instance, it now works. That's completely unexpected. Similarly, if I leave the component instance there and strip out the body of the component itself, it works. So there's something in the component that interferes with something else. Weird. I'll see if I can figure out what it is.

Update: This looks like a bug. If I replace the composite component with this:

<ui:repeat value="#{searchBean.results} var="item" varStatus="itemStatus">
    <h:outputText value="#{item.mapMarker}"/>
    <br/>
</ui:repeat>

it also fails, just as it does with the composite component.

Similarly, this fails:

<h:dataTable value="#{searchBean.results}" var="item">
    <h:column>
        <h:outputText value="#{item.mapMarker}"/>
        <br/>
    </h:column>
</h:dataTable>

In my particular case, there are three results, so I can iterate over them manually as such:

<ui:param name="results" value="#{searchBean.results}"/>
<br/>
<h:outputText value="#{results.get(0).mapMarker}"/>
<br/>
<h:outputText value="#{results.get(1).mapMarker}"/>
<br/>
<h:outputText value="#{results.get(2).mapMarker}"/>
<br/>

and it works.

So there's something about iterating over the results that is screwing with the <h:commandLink/>.

What on earth could be going wrong? Yet another Mojarra bug where a simple use case fails?

Final update: Narrowed down the problem with contrived examples and moved the question here.

Was it helpful?

Solution

This is a doosey of a bug and I can't figure out whether it's JDK7, Mojarra, the EJB container, GlassFish in general or just a bad mix of two or more of these.

Consider the following snippet from SearchBean:

public List<NormalizedSearchResult> getResults()
{
    return searchEjb.findByLocation(getRangeKm(),
                                    getLongitude(),
                                    getLatitude(),
                                    gender,
                                    getSpokenLanguageId(),
                                    getServiceId(),
                                    1,
                                    99,
                                    getStartDate().toDateMidnight().toDate(),
                                    7);
}

public List<NormalizedSearchResult> getResults2()
{
    float lng = longitude;
    float lat = latitude;
    return searchEjb.findByLocation(getRangeKm(),
                                    lng,
                                    lat,
                                    gender,
                                    getSpokenLanguageId(),
                                    getServiceId(),
                                    1,
                                    99,
                                    getStartDate().toDateMidnight().toDate(),
                                    7);
}

public List<NormalizedSearchResult> getResults3()
{
    float lng = getLongitude();
    float lat = getLatitude();
    return searchEjb.findByLocation(getRangeKm(),
                                    lng,
                                    lat,
                                    gender,
                                    getSpokenLanguageId(),
                                    getServiceId(),
                                    1,
                                    99,
                                    getStartDate().toDateMidnight().toDate(),
                                    7);
}

public List<NormalizedSearchResult> getResults4()
{
    float lng = longitude;
    float lat = latitude;
    return searchEjb.findByLocation(getRangeKm(),
                                    138.5999594f,
                                    -34.9286212f,
                                    gender,
                                    getSpokenLanguageId(),
                                    getServiceId(),
                                    1,
                                    99,
                                    getStartDate().toDateMidnight().toDate(),
                                    7);
}

public List<NormalizedSearchResult> getResults5()
{
    float lng = 138.5999594f;
    float lat = -34.9286212f;
    return searchEjb.findByLocation(getRangeKm(),
                                    lng,
                                    lat,
                                    gender,
                                    getSpokenLanguageId(),
                                    getServiceId(),
                                    1,
                                    99,
                                    getStartDate().toDateMidnight().toDate(),
                                    7);
}

public Float getLongitude()
{
    return longitude;
}

public void setLongitude(Float longitude)
{
    this.longitude = longitude;
}

public Float getLatitude()
{
    return latitude;
}

public void setLatitude()
{
    this.latitude = latitude;
}

private Float longitude;
private Float latitude;

Either of these snippets, when included in search.xhtml, cause includeViewParams=true to fail on the abovementioned <h:commandLink/>:

<ui:repeat value="#{searchBean.results}"/>
<ui:repeat value="#{searchBean.results2}"/>
<ui:repeat value="#{searchBean.results3}"/>
<ui:repeat value="#{searchBean.results4}"/>

However, this one doesn't have the same undesirable effect:

<ui:repeat value="#{searchBean.results5}"/>

Nor does this:

<ui:param name="results" value="#{pageBean.results}"/>
<h:outputText value="#{results.get(0).mapMarker}"/>
<br/>
<h:outputText value="#{results.get(1).mapMarker}"/>
<br/>
<h:outputText value="#{results.get(2).mapMarker}"/>
<br/>

And of course, includeViewParams=true works when none of these xhtml snippets are used.

I then changed the type of the longitude and latitude private variables from Float to float and no longer had these failures for any of the above cases.

WTF?! Seriously?

How could anyone possibly imagine that unboxing view parameters during the earlier stages of rendering the view would break includeViewParams=true?

Comments at JAVASERVERFACES-2260.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top