URL Parameter Passing
The JSF 1.x framework only supports POST. Support for GET was never in its design. As a result, look what happens to the following workflow:
-
User clicks a link to navigate to some resource group-specific page
-
This means that URL will have the 'groupId' parameter in it
-
User edits some form data
-
User clicks OK (assume success) and is redirected to the success page
As a result of the redirection, the URL for the success page will no longer contain the 'groupId'. This has two major issues:
1. It prevents the user from using browser bookmarks (because the target page requires the 'groupId' parameter to exist for bookmark-ability)
2. It quite possibly breaks other navigation on this page
Issue (1) could be bypassed if you built the concept of bookmarks into the web application itself, but this is non-trivial to do in practice and wouldn't be much more powerful than browser bookmarks (if at all).
Issue (2) is far more severe. For instance, the listAlertDefinition.xhtml page (the facelet that displays the list of alert definitions for some resource), needs to pass the id of the resource through to the workflow that creates a new alert definition. The fragment looks like this:
<h:commandButton action="#{ListAlertDefinitionsUIBean.createNewAlertDefinition}"
value="NEW DEFINITION" ... >
<f:param name="id" value="#{param.id}"/>
<f:param name="mode" value="new"/>
</h:commandButton>
Notice how "#{param.id}" is used to pass the data to this other workflow. This is why it's critical to be able to put the identifiers into the URL after a form post. If the contextual data in the URL were lost, the facelet would either render elements with null data, or fail to render altogether.
Support for passing URL parameters across redirect boundaries was added to RHQ by enhancing the standard JSF lifecycle. Two elements were added to the faces-config.xml file:
<faces-config>
<application>
<view-handler>
org.rhq.enterprise.gui.common.framework.FaceletRedirectionViewHandler
</view-handler>
</application>
...
<lifecycle>
<phase-listener>
org.rhq.enterprise.gui.common.framework.FacesMessagePropogationPhaseListener
</phase-listener>
</lifecycle>
</faces-config>
FaceletRedirectionViewHandler
This class extends the standard FaceletViewHandler by injecting additional logic. In particular, it overrides the mechanism which generates the target URL as follows:
/*
* evaluate any EL variables that are contained in the view id.
* e.g. "/rhq/resource/inventory/view.xhtml?id=#{param.id}"
*/
public String getActionURL(FacesContext facesContext, String viewId) {
ValueExpression valueExpression =
FacesExpressionUtility.createValueExpression(viewId, String.class);
String actionURL =
FacesExpressionUtility.getValue(valueExpression, String.class);
return actionURL;
}
As the JavaDoc implies, this assumes that the standard JSF viewId passed to the handler has this augmented syntax. This allows you to use http://java.sun.com/javaee/5/docs/tutorial/doc/bnahq.html UEL (unified expression language) "#{token}" mechanism in any of your URLs in any of your *-navigation.xml files. For example, the /WEB-INF/jsf-navigation/configuration-navigation.xml file has the following fragment:
<navigation-rule>
<from-view-id>/rhq/resource/configuration/view.xhtml</from-view-id>
<navigation-case>
<from-action>#{ExistingResourceConfigurationViewUIBean.edit}</from-action>
<to-view-id>/rhq/resource/configuration/edit.xhtml?id=#{param.id}</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
When I first navigate to the configuration > current sub-tab for some resources, the URL will look like /rhq/resource/configuration/view.xhtml?id=<myResourceId>
Since the URL contains the id parameter, if I were to click on the edit button on this page (which is backed by ExistingResourceConfigurationViewUIBean.edit), the view handler above will evaluate "#{param.id}" and consequently redirect the user to the following page /rhq/resource/configuration.edit.xhtml?id=<myResourceId>. Notice how myResourceId was propagated through.
However, the view handler performs a full redirect (governed by the <redirect/> tag in the <navigation-case> element) in order to change the URL. This creates issues of its own because additional data might have been available in the context, which is now lost across the redirect boundary. In particular, the standard JSf mechanism for error handling - the FacesMessages object - is stored in that context. Thus, there would be no way to display a success (or error) message on the page that you redirect to. The next section, FacesMessagePropogationPhaseListener, presents the solution RHQ uses to bypass this.
FacesMessagePropogationPhaseListener
There are 6 phases in the http://java.sun.com/j2ee/1.4/docs/tutorial/doc/JSFIntro10.html JSF lifecycle:
1. Restore View
2. Apply Request Values
3. Process Validations
4. Update Model Values
5. Invoke Application
6. Render Response
The imaginary line between the "Invoke Application" and the "Render Response" phases essentially forms our redirect boundary. While the lifecycle is in one of the first 5 phases, it still processing on the current page. The "Invoke Application" phase is the execution of the form action itself, which must return a String identifying the viewId you which to navigate to. An example will help explain this boundary.
On the listAlertHistory.xhtml page, there is a button which deletes ALL of the alerts on that resource:
<h:commandButton action="#{ListAlertHistoryUIBean.purgeAllAlerts}"
value="PURGE ALL" ... />
That backing bean looks as follows:
public String purgeAllAlerts() {
Subject subject = EnterpriseFacesContextUtility.getSubject();
Resource resource = EnterpriseFacesContextUtility.getResource();
try {
AlertManagerLocal alertManager = LookupUtil.getAlertManager();
int numDeleted = alertManager.deleteAlerts(subject, resource.getId());
FacesContextUtility.addMessage(FacesMessage.SEVERITY_INFO,
"Deleted " + numDeleted + " alerts ...");
} catch (Exception e) {
FacesContextUtility.addMessage(FacesMessage.SEVERITY_ERROR,
"Failed to delete alerts ... ", e);
}
return "success";
}
As with every other managed bean method in JSF that supplies the backing action to commandButtons and commandLinks, it returns an "outcome" string that is used to lookup the navigation rule that should fire. Since this method only returns a single value - "success" - it has one and only one page it will ever navigate to.
Using our knowledge about the layout of the various *-navigation.xml files, and since we know this button is found on the resource-level listAlertDefinition.xhtml page, it will map to the following navigation rule in /WEB-INF/jsf-navigation/alert-navigation.xhtml:
<!-- Alert History List -->
<navigation-rule>
<from-view-id>/rhq/resource/alert/listAlertHistory.xhtml</from-view-id>
<navigation-case>
<!-- catch all navigation-case: if any actions return 'success', go here -->
<from-outcome>success</from-outcome>
<to-view-id>/rhq/resource/alert/listAlertHistory.xhtml?id=#{param.id}</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
In other words, when the "PURGE ALL" button is clicked, it will:
-
purge the alerts
-
succeed or fail
-
write an appropriate error message into the FacesContext
-
the navigation rule that matches the "success" outcome will be invoked
-
our FaceletRedirectionViewHandler will dereference "#{param.id}"
-
the page flow will cross the redirection boundary
So, in order to propagate the data stored in the FacesContext, it needs to be:
1. pulled out just before the new page is rendered (just after the action method is invoked)
2. stored in some temporary location that exists across the redirect boundary
3. reinserted into the FacesContext after the new view is displayed (just before the new view is rendered / restored)
The class FacesMessagePropogationPhaseListener, which leverages the user's web session as the temporary storage location, will do just the trick:
public class FacesMessagePropogationPhaseListener implements PhaseListener {
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
public void afterPhase(PhaseEvent event) {
if (event.getPhaseId() == PhaseId.INVOKE_APPLICATION) {
// want to store the messages in the context after the app is done processing
putGlobalFacesMessagesInSession();
}
}
public void beforePhase(PhaseEvent event) {
if (event.getPhaseId() == PhaseId.RESTORE_VIEW) {
/*
* want to add the saved messages back to the context immediately after the
* view is restored. remove them from the session once we've added them
* back, otherwise the messages will just keep building up across requests
*/
List<FacesMessage> savedMessages = removeGlobalFacesMessagesFromSession();
putGlobalFacesMessagesInFacesContext(savedMessages);
}
}
}
Summary
Thus, by leveraging the FacesMessagePropogationPhaseListener to close the gaps created by supporting URL parameter passing with FaceletRedirectionViewHandler, RHQ effectively bypasses the "everything is a POST" restriction of JSF and supports actions that can redirect to bookmarkable URLs while maintaining data across the redirect boundary.
Paging and Sorting
Many UI pages within RHQ need to display portions of very large sets of data. One way of doing this would be to add arguments at the business logic layer as follows:
public List<Resource> findResources(Subject subject,
String nameFilter,
int startingRecord,
int numberOfRecords,
String sortBy);
The problem with this model is that if you ever wanted to change your application's pagination or sorting logic, you would have to update every single one of your methods that support paging across the entire business tier. Let's say that you wanted to expose an easier method for people to call who didn't want to paginate and just wanted all of the results back. You would either have to convert the 'int' types to 'Integer', or create a new overloaded method that only did the sorting:
public List<Resource> findResources(Subject subject,
String nameFilter,
String sortBy);
Or, let's say you had an enhancement request to support ascending or descending order on your field. Your new method would look like:
public List<Resource> findResources(Subject subject,
String nameFilter,
int startingRecord,
int numberOfRecords,
String sortBy,
boolean isAscending);
In all of these cases, you're either:
-
Adding more methods, which bloats your interface)
-
Changing the invariants on existing methods by making certain parameters null-able, or
-
Modifying the signature of the method outright, which will require changing all callers to that method
Again, if you had to do this for every method across your entire business tier, it could be quite tedious. So RHQ took a slightly different route to support paging and sorting. Instead of using static parameters to control what data is retrieved, an abstraction was developed in the form of two objects called PageControl and PageList. As a result, all methods in RHQ that need to return a subset of data will look as follows:
public PageList<Resource> findResources(Subject subject,
...
PageControl pageControl);
The next sections describe these objects in detail as well as how to effectively leverage them.
PageControl
This object embodies the components needing for sorting and paging:
-
Wraps page number & page size
-
Provides reasonable defaults (the first page, showing 100 records)
-
Has accommodations for returning all results (i.e., ignore pagination)
-
Wraps sorting information
-
Has accommodations for default sorting rules
-
Can perform an ordered sort of up to 3 fields
-
Explicit specification of ASC and DESC property for each field
Note: in early versions of RHQ, you could only sort on a single field ascending or descending. However, because all the paging logic was completely contained in the PageControl object, it was very easy to enhance that class to support ordering on 3 fields independently. As a result, all business tier methods that were using it automatically inherited the new functionality.
PageList
Although returning a simple java.util.List would work, it would make rendering the user interface portion for those results more difficult. Any reasonably sophisticated UI will want to indicate to the user:
-
which page of data they are current viewing
-
providing paging controls for moving forward / backward through the result set
-
also give the user the ability to change the page size
-
display the total number of results found (even though only one page of data is being displayed)
-
enable sorting on individual columns in tabular displays
-
allow the user to flip the ASC / DESC order for any field
As a result, RHQ created the PageList object as an extension of Java's basic ArrayList, which compositionally includes the PageControl in it. Accordingly, the generic structure for the implementation of a paginated business method should be written as follows:
public PageList<Resource> findResources(Subject subject,
...
PageControl pageControl) {
List<Resource> results = // get results from Hibernate
long count = // get total number of Objects of the given type
return new PageList<Resources>(results, count, pageControl);
}
PersistenceUtility
In standard EJB3, the interaction with the EntityManager would roughly be as follows:
@NamedQueries( {
@NamedQuery( name = "FindAllResources",
query = "SELECT res FROM Resource res ORDER BY ..."),
@NamedQuery( name = "FindAllResources_count",
query = "SELECT COUNT(*) FROM Resource res")
} )
Query query = em.createNamedQuery("FindAllResources");
query.setFirstResult(pageNumber * pageSize);
query.setMaxResults(pageSize);
List<Resource> results = query.getResultList();
Query countQuery = em.createNamedQuery("FindAllResources_count");
long count = ((Number) countQuery.getSingleResult()).longValue();
Although this does handle the pagination, this completely ignores sorting. Any sorting logic would have to be hard-coded in your NamedQuery. Furthermore, it requires you to essentially duplicate all of your queries: once to return a single page of the result set, and once to return the cardinality of result set.
The PersistenceUtility class was written to simplify the programming model for developers writing business interfaces for RHQ. With it, the above code reduces to:
Query query = PersistenceUtility.createQueryWithOrderBy(entityManager,
"FindAllResources",
pageControl);
List<Resource> results = query.getResultList();
Query countQuery = PersistenceUtility.createCountQuery(entityManager,
"FindAllResources");
long count = ((Number) countQuery.getSingleResult()).longValue();
Notice how the PageControl instance is passed to the "createQueryWithOrderBy" method. It does several things for us:
-
talks to the EntityManager and get the underlying JPQL for that NamedQuery
-
uses regular expressions to transform that query into one dynamically populated with the ordering information contained within the PageControl object (you no longer have to statically embed this information in your NamedQuery)
-
still allows pagination rules by calling setFirstResult(int) and setMaxResults(int) on the query object, before returning it to the user
Notice that the user didn't have to write two different named queries. A single query, "FindAllResources", handled everything.
The "createCountQuery" will, in a similar fashion, talk to the EntityManager to get the underlying JPQL and use regular expressions to transform the existing select list into a query that performs "SELECT COUNT(*)..." auto-magically.
PagedDataTableUIBean / PagedListDataModel
These classes are integration points that function as the glue between the business tier (*ManagerBean SLSBs) and the web tier (*UIBean JSF Managed Beans). The facilities offered by them are:
-
The ability to link a specific instance of a PageControl object to a rich:dataTable defined in some facelet
-
Automatic persistence of the PageControl data on a user-by-user basis (pagination & sorting remembered if the user comes back to that view)
-
Implicit error handling of invalid PageControl data (such as trying to access a non-existent page), which consequently would reset (and re-persist) default pagination and sorting values for that user
-
Errors are captured and the underlying query that loads the table is automatically re-run without programmer intervention
-
Contains debug logging, which can be turned on to trace the performance of loading data into various tables
You never need to touch these on a day-to-day basis, just keep in mind all the features they are providing simply by writing your JSF managed beans as extensions to these classes. Let's look at an example, which loads the events for a resource:
public class EventHistoryUIBean extends PagedDataTableUIBean {
@Override
public DataModel getDataModel() {
if (dataModel == null) {
dataModel = new ListEventsHistoryDataModel(PageControlView.EventsHistoryList,
"EventHistoryUIBean");
}
return dataModel;
}
private class ListEventsHistoryDataModel extends PagedListDataModel<EventComposite> {
public ListEventsHistoryDataModel(PageControlView view, String beanName) {
super(view, beanName);
}
@Override
public PageList<EventComposite> fetchPage(PageControl pc) {
// get various context data such as search parameters and selected time range
PageList<EventComposite> results = eventMgr.findEvents(getSubject(),
getResource().getId(),
...,
pc);
return results;
}
}
}
The rendered table footer will look similar to:
There are several parts to this:
-
Total - this is the 'count', which you used PersistenceUtility.createCountQuery to retrieve, and which you passed as the 2nd constructor argument when creating your PageList object
-
Items Per Page - this is bound to the pagesize property of the PageControl object that backs this data table; the user can freely change this and the table will re-render with more (or less) row results.
-
datascroller (right-most element) - this is bound to the pagenumber property of the PageControl object; it will only render if there are more elements than can fit on a single page; the user can freely change this and the table will re-render with the new page of data
PageControlView
Astute readers will notice that the PageControlView seems to play an integral part in the example above. Indeed, this object is the integration point that functions as the glue between this UIBean and the facelet which exposes that UIBean's functionality to the view. The PageControlView enum is how each table gets a semantic RHQ identity, which can then be used to remember the pagination and sorting information for this table on a user-by-user basis.
One thing to note about the PageControlView objects - the pagination and sorting are inextricably linked together. There are, however, valid cases where the data sets being dealt with are small, and you want to specifically disallow pagination but still enable / support sorting. This is easily done by creating your enumeration as follows:
{{
ResourceMeasurementScheduleList(ListResourceMeasurementScheduleUIBean.class, true)
}}
This then changes the style of the table footer to look like:
Notice that the "Items Per Page" as well as the datascroller are missing. Since we're dealing with an unlimited PageControlView, the framework will only render the "Total" as a read-only label, and nothing else.
RichFaces DataTable
To expose the pagination functionality to your view (i.e., some facelet), we use a standard DataTable component from the RichFaces library, but we decorate it and annotate it in various ways. Let's take a look at /rhq/resource/configuration/history.xhtml to see how this works:
01 <ui:param name="configurationUpdateDataModel"
02 value="#{ListConfigurationUpdateUIBean.dataModel}"/>
03 <rich:dataTable id="configurationUpdateDataTable"
04 rows="#{PageControl.ConfigurationHistory.pageSize}"
05 value="#{configurationUpdateDataModel}"
06 var="item" >
07 <f:facet name="PageControlView">
08 <onc:paginationControl id="ConfigurationHistory"/>
09 </f:facet>
10 <rich:column>
11 <f:facet name="header">
12 <onc:sortableColumnHeader sort="cu.subjectName">
13 <h:outputText styleClass="headerText" value="User"/>
14 </onc:sortableColumnHeader>
15 </f:facet>
16 <h:outputText value="#{item.subjectName}"/>
17 </rich:column>
18 <f:facet name="footer">
19 <rich:columnGroup>
20 <rich:column colspan="...">
21 <!-- buttons -->
22 <ui:param name="paginationDataTableName"
23 value="configurationUpdateDataTable"/>
24 <ui:param name="paginationDataModel"
25 value="#{configurationUpdateDataModel}"/>
26 <ui:param name="paginationPageControl"
27 value="#{PageControl.ConfigurationHistory}"/>
28 <ui:include src="/rhq/resource/include/pagination.xhtml"/>
29 </rich:column>
30 </rich:columnGroup>
31 </f:facet>
32
33 </rich:dataTable>
The various parts of this fragment are explained:
'''Lines'''
|
'''Description'''
|
01-02
|
Loading the dataModel from our UIBean, which in this case is going to be an instance of PagedListDataModel, and saving it into a parameter called 'configurationUpdateDataModel' to be referenced elsewhere in the view
|
04
|
Binding the number of rows to render / display for this table to the pageSize property of the PageControl instance associated with this table
|
05
|
Referencing the variable created in lines 01-02, which binds the PagedListDataModel loaded from our UIBean to this table
|
07-09
|
Giving this table its semantic RHQ identity, which is used to look up the appropriate PageControlView enum, and that is subsequently leveraged to retrieve the appropriate PageControl instance from the backing store
|
12-14
|
RHQ custom JSF component - it finds which DataTable it's a child of, looks up the PageControlView enum for that table, which is subsequently leveraged to retrieve the appropriate PageControl instance from the backing store that contains the user-specific sorting information for this table (see the SortableColumnHeader section below for more details)
|
22-28
|
The pagination.xhtml is a parameterized facelet that requires 3 variables to exist in context - 'paginationDataTableName', 'paginationDataModel', and 'paginationPageControl' - so that it knows how to generate the pagination-aware table footer
|
SortableColumnHeader / SortableColumnHeaderListener
Whereas the pagination elements in the previous sections worked to control how the footer of some DataTable rendered itself, this component controls how each individual column header will render. An example will help illustrate the functionality this component provides:
Looking at the headers for the operation history table, I can tell that it will:
-
list operations that have failed before operations that have succeed, because the data set is first being sorting on the 'Status' field in ascending order
-
list the most recent failed operations first, because the data set is secondarily being sorted on the 'Date Completed' field in descending order
You'll also notice that each of the column headers is clickable. When you click a column header, it will fire off an ActionEvent containing the string data you added to the "sort" attribute for the tag. That ActionEvent will be captured by an instance of the SortableColumnHeaderListener, which was wired up during the construction of each SortableColumnHeader component. The listener will then do the following:
1. find the DataTable that contains the SortableColumnHeader component that triggered the{{ActionEvent}}
a. assume that the DataTable is backed by a PagedListDataModel and get the PageControl object associated with that data model
2. using the PageControl object gotten in (1), it will change the field that was just clicked to the primary sort field, which has the follow side effects:
a. if that column had been anywhere in the sort list - whether as the primary, secondary, or tertiary sort field - it will have its ordering reversed (if it had been ASC it will "flip" to DESC, and vice versa)
b. the previous primary sort field, if any, becomes the secondary sort field
c. the previous secondary sort field, if any, becomes the tertiary sort field
d. the previous tertiary sort field, if any, will be dropped
3. invoke the navigation rule for the view that has "sort" as the outcome
This means that the corresponding /WEB-INF/jsf-navigation/operation-navigation.xml file would need to have the following rule:
<!-- Resource Operation History -->
<navigation-rule>
<from-view-id>/rhq/resource/operation/resourceOperationHistory.xhtml</from-view-id>
<navigation-case>
<from-outcome>sort</from-outcome>
<to-view-id>
/rhq/resource/operation/resourceOperationHistory.xhtml?id=#\{param.id}
</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
Summary
So, let's recap what it takes to add paging and sorting to your tabular views:
-
Write your managed bean (*UIBean naming convention) as an extension of PagedDataTableUIBean
-
Override the getDataModel() method, and have it return an instance of PagedListDataModel<T> - the generic, T, should be specific to the data you want to display in your rich:dataTable
-
Add another element to the PageControlView enumeration, which wires the *UIBean you just created with a persistent, table-specific & user-specific instance of a PageControl object
-
Write your facelet using a rich:dataTable tag
-
Add an <f:facet> tag whose sole child is "onc:paginationControl"
-
The "id" attribute of "onc:paginationControl" should match the name of the PageControlView enumeration you just added
-
Decorate any, all, or none of the columns with SortableColumnHeader component wrappers
-
The "sort" attribute should be the precise JPQL fragment that you want to be dynamically appended to the query PersistenceUtility generates automatically for you
-
Add the navigation rule for sorting as shown in the previous section; make sure that you are passing any necessary context data as tokenized URL parameters (using the "#{}" syntax supported by the FaceletRedirectionViewHandler)