Module Layout, Deployment, & InstallationThe RHQ user interface is broken down into three primary modules: installer-war, portal-war, and webdav-war. All of these can be found under "modules/enterprise/gui", as the following directory snapshot illustrates. This document, however, will focus mostly on portal-war.
The left-hand side of the image above shows the RHQ source tree module hierarchy. The right-hand side shows the file structure of the product in its deployed form. Using the colored box highlights, you can see where each module from the source tree can be found in the product installation. Explanation of ".rej"Since JBossAS uses a http://www.redhat.com/docs/manuals/jboss/jboss-eap-4.2/doc/Server_Configuration_Guide/The_JBoss_JMX_Microkernel-JBoss_Deployer_Architecture.html suffix deployer, any file that ends in ".ear" or ".war" will be interpreted as an Enterprise Application or Web Archive, respectively, and the server will attempt to deploy it. So, in order to prevent the server from deploying the rhq.ear before other dependencies have been laid down, RHQ uses a ".rej" suffix by default. Below is an illustration showing what the directory looks like before (left-hand side) and after (right-hand side) the installer is run.
The diagram shows us that the rhq-installer.war does not have the ".rej" extension out-of-box. After the installer is run, it will get the ".rej" suffix added to it. This is to prevent users from accidentally rerunning the installer. In fact, postinstaller.war has logic redirect the user to the main start page for the "rhq-portal.war" if the user ever tries to access some URL that the installer uses. Conversely, the rhq-postinstaller.war and rhq.ear are both suffixed with ".rej" out-of-the-box, and as a result of running through the installation that suffix is removed...which will deploy them. JSF Container ConfigurationThe faces-config.xml file found in WEB-INF directory is the standard place to put all JSF-related configuration elements including:
However, instead of having one huge XML file (which would have been several thousands of lines long) RHQ breaks this information into separate files.
<!-- componentType := configuration, error, inventory, layout, navigation,
pagination, sorting, table, time, upload, metric
resourceBeanType := alert, content, autodiscovery, cluster, configuration
global, inventory, admin, measurement, operation, test
autoGroupBeanType := inventory, measurement
commonBeanType := availability, event, measurement, navigation
definitionBeanType := group-definition
groupBeanType := alert, configuration, inventory, operation, measurement
resourceNavType := alert, cluster, content, events, auto-discovery,
configuration, inventory, admin, operation, measurement,
summary, test
definitionNavType := group-definition
groupNavType := alert, configuration, events, inventory, operations,
measurement
autoGroupNavType := events, measurement
subsystemNavType := tab-container
converterType := common, content, inventory
-->
<context-param>
<param-name>javax.faces.CONFIG_FILES</param-name>
<param-value>
/WEB-INF/jsf-components/{componentType}-components.xml,
/WEB-INF/jsf-managed-beans/{resourceBeanType}-beans.xml,
/WEB-INF/jsf-managed-beans-auto-group/{autoGroupBeanType}-beans.xml,
/WEB-INF/jsf-managed-beans-common/{commonBeanType}-beans.xml,
/WEB-INF/jsf-managed-beans-definition/{definitionBeanType}-beans.xml,
/WEB-INF/jsf-managed-beans-resource-group/{groupBeanType}-beans.xml,
/WEB-INF/jsf-managed-beans-subsystem/{subsystem}-beans.xml,
/WEB-INF/jsf-navigation/{resourceNavType}-navigation.xml,
/WEB-INF/jsf-navigation-auto-group/{autoGroupNavType}-navigation.xml,
/WEB-INF/jsf-navigation-definition/{definitionNavType}-navigation.xml,
/WEB-INF/jsf-navigation-resource-group/{groupNavType}-navigation.xml,
/WEB-INF/jsf-navigation-subsystem/{subsystemNavType}-navigation.xml,
/WEB-INF/jsf-converters/{converterType}-converters.xml
</param-value>
</context-param>
As you can see, there are several subdirectories for the various configuration files:
Since there are only a handful of components and converters, all of those elements were placed into a single folder by type. However, because there are literally dozens (if not a few hundred) managed beans and navigation rules, these elements have been broken down into further subdirectories. These subdirectory suffix names are described below:
Using this knowledge, you can very easily wire the Java code together with the definitions of the managed beans and navigation rules that describe it. Let's say I'm looking at the inventory > overview sub-tab for a resource:
Then, without even looking at the URL, I know...
If I were instead looking at the operation > schedules sub-tab for a resource group:
Then, again, without even looking at the URL, I know...
JSF & RichFaces ExtensionsURL Parameter PassingThe 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:
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) 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>
FaceletRedirectionViewHandlerThis 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. FacesMessagePropogationPhaseListenerThere are 6 phases in the http://java.sun.com/j2ee/1.4/docs/tutorial/doc/JSFIntro10.html JSF lifecycle: 1. Restore View 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:
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) 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); } } } SummaryThus, 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 SortingMany 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:
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. PageControlThis object embodies the components needing for sorting and paging:
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. PageListAlthough 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:
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); } PersistenceUtilityIn 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:
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 / PagedListDataModelThese 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:
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:
PageControlViewAstute 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: {{ 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 DataTableTo 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:
SortableColumnHeader / SortableColumnHeaderListenerWhereas 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:
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}} 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>
SummarySo, let's recap what it takes to add paging and sorting to your tabular views:
|






