Using JSF with Spring and an introduction to Page scope

- (c) 2008, Software Engineering Solutions, Inc.

- Damodar Chetty

- Nov 19, 2008

In this article, my objective is to introduce three distinct concepts.

1.      Implement a very common user interface idiom - the display of data using a user manipulable table.

2.      Demonstrate how Spring can be used to replace JSF's rudimentary Managed Bean Creation Facility.

3.      Demonstrate the use of a new scoping mechanism - which is a flash-like scope, i.e. one that lasts between requests, but does not happen automatically (as in say, Grails).

1.1. User Interface Idiom - Data Tables

A very common user interface idiom is the display of data using a user manipulable table. A degenerate table simply presents the data in the form of rows and columns, while a more ambitious table would encourage a user to interact with it.

This interaction takes one of the following forms:

Sorting (usually by clicking a particular column header), as seen on Netflix:

Paging is used to display data to the user in bite-sized chunks. It requires the presence of a secondary set of navigation controls, usually with options for First, Prev, page numbers, Next, and Last, e.g., as seen with the ubiquitous:

Filtering is a lesser known variant of paging, where the number of records displayed are not based on a fixed page size, but instead are determined by some characteristic of the row's data. In addition, the navigation controls are no longer a sequential numerical progression, but instead are the criteria used to filter a particular data set. For instance, either by the first character of some key field, or a range of values in a particular column.

The JSF data table's raison d'etre is to ease the implementation of this idiom. In this article, I'll explain how to use the data table, and to support sorting.

In a subsequent article, I intend to address filtering. I omit the use of paging, since it is a degenerate case of filtering (where the filtering occurs by the page to which a row belongs, rather than some other criterion).

1.2. Spring Integration

While the Managed Bean Creation Facility is a fairly functional piece of code, it is far more likely that you will run into limitations using its bean creation, than you would with the use of Spring. E.g., the use of static factory methods to set bean references - as we do later in our example. In addition, there's only one less DI framework you need to care about, freeing up some memory cycles (always a pleasant side effect.)

As a result, Spring 2.0's mechanisms for integrating with JSF give you an industrial strength replacement for very little cost.

1.3. Flash Scope

The traditional web application scopes (application, session, and request) often turn out not to be granular enough for our needs. This has resulted in a number of attempts to define an intermediate scope, such as Flash Scope, Page Flow Scope, Conversation Scope, etc. which are all variants on the same basic theme.

Unfortunately these scopes are not yet part of the servlet spec, and you tend to be locked into proprietary implementations depending on your frameworks of choice.

The basic theory is simple enough.

An object placed in request scope is lost as soon as the request processing ends. I.e., on a redirect, you cannot expect to find that attribute anymore.

An object placed in session scope lives for a very long time, and needs active management by the developer in order to minimize memory usage on the server. Since memory footprint directly impacts scalability and performance, most of us are warned to be very careful about what we place in here.

An object placed in flash scope lives only across 2 consecutive requests - so it survives a redirect, after which it is automatically removed. This works exceedingly well for error or validation messages where the message is generated by a POST, and then has to survive a Redirect (as part of the Post-Redirect-Get pattern).

The Tomahawk t:saveState gives you a programmer driven (some would say crude) way of mimicking the flash scope. However, it is a valuable addition to the JSF arsenal and is worthy of discussion.

Before we begin:

If you are not familiar with how to setup a project with MyFaces and Tomahawk, then this is the best time to read my earlier post. I'll repeat some of the key steps, but if you're new to this, you might as well take the more complete route.

We'll also take a couple of short detours - to use Spring to replace JSF's managed bean creation facility; and to use the incredibly useful Tomahawk t:saveState action tag.

 

Our example project will display a list of contacts that it retrieves from somewhere (say, a database), and will let you click on column headers to sort it in ascending order. Clicking on a contact's last name will select that contact, which for now, simply prints the contact's details to your console.

 

2. Project Setup

I'm going to rush through the creation of a JSF project. To create a blank new Java project:

1.      Start up the project creation wizard,

2.      give your project a memorable name and location,

3.      Accept defaults for the remaining screens:

4.      This results in a pretty bland project.

5.      To give it some character, add in dependencies to MyFaces 1.2.4, Taglibs 1.1.2, Tomahawk 1.1.7, Spring 2.0.8, and Facelets 1.1.14.
I've previously downloaded these into a folder I created at d:\testing\lib.

6.      Finally, add a new web facet to our module. This will create a new [web] folder as your application's content root, and will copy a dummy web.xml into it. Ensure that your dependency Jars are copied to your WEB-INF/lib.

7.      On the Java EE Build Settings tab, set an exploded WAR directory, and turn off JSP validation.

8.      You may choose to set your compiler to never deploy web apps after compilation, and your debugger to always reload classes after compilation.

9.      Voila - you're done. Here's your freshly baked project:

10.  Now, set up your Tomcat instance, and ask it to deploy your application's exploded WAR folder.
 

3. Setting up JSF

This section describes the changes to the files in WEB-INF.

Again, more details can be found in my earlier post on setting up MyFaces, Tomahawk, and Facelets.

3.1. web.xml

First, we update web.xml by

(a)   Configure Spring,

(b)   Configure Tomahawk,

(c)   Configure MyFaces, and

(d)   Configure Facelets

The listeners required by Spring enable the use of the scopes (request, session, and application) available to web applications.

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

     http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

       version="2.5">

  <listener>

  <listener-class>

     org.apache.myfaces.webapp.StartupServletContextListener

   </listener-class>

  </listener>

  <listener>

  <listener-class>

     org.springframework.web.context.ContextLoaderListener

   </listener-class>

  </listener>

  <listener>

  <listener-class>

     org.springframework.web.context.request.RequestContextListener

   </listener-class>

    </listener>

    <filter>

<filter-name>extensionsFilter</filter-name>

        <filter-class>org.apache.myfaces.webapp.filter.ExtensionsFilter</filter-class>

        <init-param>

<param-name>uploadMaxFileSize</param-name>

<param-value>100m</param-value>

        </init-param>

        <init-param>

            <param-name>uploadThresholdSize</param-name>

<param-value>100k</param-value>

        </init-param>

    </filter>

    <filter-mapping>

<filter-name>extensionsFilter</filter-name>

        <servlet-name>Faces Servlet</servlet-name>

    </filter-mapping>

    <filter-mapping>

<filter-name>extensionsFilter</filter-name>

<url-pattern>/faces/myFacesExtensionResource/*</url-pattern>

    </filter-mapping>

    <servlet>

        <servlet-name>Faces Servlet</servlet-name>

        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>

<load-on-startup>1</load-on-startup>

    </servlet>

    <servlet-mapping>

        <servlet-name>Faces Servlet</servlet-name>

<url-pattern>*.jsf</url-pattern>

    </servlet-mapping>

    <servlet-mapping>

        <servlet-name>Faces Servlet</servlet-name>

<url-pattern>/faces/*</url-pattern>

    </servlet-mapping>

    <context-param>

<param-name>javax.faces.STATE_SAVING_METHOD</param-name>

<param-value>server</param-value>

    </context-param>

    <context-param>

<param-name>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</param-name>

<param-value>true</param-value>

    </context-param>

    <context-param>

<param-name>org.apache.myfaces.COMPRESS_STATE_IN_SESSION</param-name>

<param-value>true</param-value>

    </context-param>

    <context-param>

<param-name>org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION</param-name>

<param-value>20</param-value>

    </context-param>

    <context-param>

      <param-name>org.apache.myfaces.VALIDATE</param-name>

<param-value>true</param-value>

    </context-param>

    <context-param>

<param-name>javax.faces.DEFAULT_SUFFIX</param-name>

<param-value>.xhtml</param-value>

    </context-param>

    <context-param>

<param-name>facelets.REFRESH_PERIOD</param-name>

<param-value>1</param-value>

    </context-param>

    <context-param>

<param-name>facelets.DEVELOPMENT</param-name>

<param-value>true</param-value>

    </context-param>

    <context-param>

<param-name>facelets.SKIP_COMMENTS</param-name>

<param-value>true</param-value>

    </context-param>

    <context-param>

<param-name>facelets.LIBRARIES</param-name>

        <param-value>/WEB-INF/tomahawk.taglib.xml</param-value>

    </context-param>

</web-app>

 

3.2. faces-config.xml:

Next, lets register Facelets and Spring with MyFaces/JSF. This tells JSF to ask Spring to resolve references to EL variables.

<?xml version='1.0' encoding='UTF-8'?>

<!DOCTYPE faces-config PUBLIC

  "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"

  "http://java.sun.com/dtd/web-facesconfig_1_1.dtd">

<faces-config>

  <application>

   <view-handler>com.sun.facelets.FaceletViewHandler</view-handler>

    <variable-resolver>

     org.springframework.web.jsf.DelegatingVariableResolver

   </variable-resolver>

  </application>

</faces-config>

3.3. tomahawk.taglib.xml

This file allows Tomahawk to play nice with Facelets. I'm using a minimal tag lib here since all I intend to use is the t:saveState tag.

<?xml version="1.0"?>

<!DOCTYPE facelet-taglib PUBLIC

  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"

  "http://java.sun.com/dtd/facelet-taglib_1_0.dtd">

<facelet-taglib>

<namespace>http://myfaces.apache.org/tomahawk</namespace>

    <tag>

        <tag-name>saveState</tag-name>

        <component>

            <component-type>org.apache.myfaces.SaveState</component-type>

        </component>

    </tag>

</facelet-taglib>

3.4. applicationContext.xml

This is the Spring bean configuration file. We will focus on using Spring 2.0.8 to replace JSF's native managed bean creation facility. For now, let's leave this empty - we'll fill it in as we go along.

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

  <bean id="contactModel" class="com.swengsol.domain.ContactModel"
   factory-method="getInstance"/>

  <bean id="dataTableBean" class="com.swengsol.datatable.DataTableBean" scope="request">

  <constructor-arg ref="contactModel" />

  </bean>

</beans>

4. The problem domain

Your customer has asked you for a page that displays a list of sales contacts, and allow the ability to select one of these for further processing. Your customer wants you to allow this list to be sorted by clicking on a column header, and would like the ability to filter records by the first letter of the contact's last name.

For simplicity, the following approximations are made:

1.      The fields for each contact are limited, comprising a first name, a last name, a city, and a country.

2.      Rather than retrieving the list of contacts from a database, we use a method that builds a hard coded list. The dynamic nature of this data is simulated by having a fresh list returned each time the method is invoked.

4.1. Contact.java

Lets start with the Contact - which is a JavaBean that exposes some comparators - which will be used to define sorting rules for Contact objects.

package com.swengsol.domain;

 

import java.io.Serializable;

import java.util.Comparator;

 

public final class Contact implements Serializable {

    private String firstName;

    private String lastName;

    private String city;

    private String country;

    public Contact(String firstName, String lastName, String city, String country) {

     this.firstName = firstName;  this.lastName = lastName;

     this.city = city; this.country = country;

    }

    public void setFirstName(String newValue) { firstName = newValue; }

    public String getFirstName() { return firstName; }

    public void setLastName(String newValue) { lastName = newValue; }

    public String getLastName() { return lastName; }

    public void setCity(String newValue) { city = newValue; }

    public String getCity() { return city; }

    public void setCountry(String newValue) { country = newValue; }

    public String getCountry() { return country; }

 

    public static final class FirstNameComparator implements Comparator<Contact> {

        public int compare(Contact i1, Contact i2) {

            return i1.getFirstName().compareToIgnoreCase(i2.getFirstName());

        }

    }

    public static final class LastNameComparator implements Comparator<Contact> {

        public int compare(Contact i1, Contact i2) {

            return i1.getLastName().compareToIgnoreCase(i2.getLastName());

        }

    }

    public static final class CityComparator implements Comparator<Contact> {

        public int compare(Contact i1, Contact i2) {

            return i1.getCity().compareToIgnoreCase(i2.getCity());

        }

    }

    public static final class CountryComparator implements Comparator<Contact> {

        public int compare(Contact i1, Contact i2) {

            return i1.getCountry().compareToIgnoreCase(i2.getCountry());

        }

    }

    public String toString() {

        return firstName + ", " + lastName + " [" + city + " - " + country + "]";

    }

}

4.2. ContactModel.java

This class is a singleton that simulates the DAO layer that goes over the wire to some business service or persistence tier component to retrieve your list of contacts.

package com.swengsol.domain;

 

import java.util.List;

import java.util.ArrayList;

 

public final class ContactModel {

    private static ContactModel instance = new ContactModel();

    private ContactModel() {   }

    public static synchronized ContactModel getInstance() {

        return instance;

    }

    public List<Contact> getAllContacts() {

        System.out.println("Must go over the wire to build list of contacts");

        //TODO do something meaningful like getting this from a database.

        final List<Contact> contacts = new ArrayList<Contact>(10);

        contacts.add(new Contact("Damodar", "Chetty", "Woodbury", "USA"));

        contacts.add(new Contact("Manlio","Brosio", "Turin", "Italy"));

        contacts.add(new Contact("Sergio", "Balanzino", "Rome", "Italy"));

        contacts.add(new Contact("Javier","Solana", "Madrid", "Spain"));

        contacts.add(new Contact("Vlad","da Impaler", "Bucharest", "Romania"));

        contacts.add(new Contact("Matthias", "Corvinus", "Budapest", "Hungary"));

        contacts.add(new Contact("Trygve", "Lie", "Oslo", "Norway"));

        contacts.add(new Contact("Kurt", "Waldheim", "Innsbruck", "Austria"));

        contacts.add(new Contact("Kofi", "Annan", "Tamale", "Ghana"));

        contacts.add(new Contact("Ban", "Ki-moon", "Seoul", "South Korea"));

        contacts.add(new Contact("U", "Thant", "Yangon", "Burma"));

        contacts.add(new Contact("Jaap", "de Hoop Scheffer", "Amsterdam", "Netherlands"));

        return contacts;

    }

}

 

4.3. DataTableBean.java

This is the managed bean that provides the UI processing logic for the current view. Spring DI is used to inject in the ContactModel that this bean will use to retrieve its collection of Contacts. It contains a few action listeners (which are perfect for UI events such as sorting requests) and an action method.

package com.swengsol.datatable;

 

import com.swengsol.domain.Contact;

import com.swengsol.domain.ContactModel;

 

import javax.faces.event.ActionEvent;

import javax.faces.component.UIData;

import javax.faces.context.FacesContext;

import java.util.List;

import java.util.Collections;

 

public class DataTableBean {

    private List<Contact> allContacts;

    private ContactModel contactModel;

 

    public DataTableBean(ContactModel contactModel) { this.contactModel = contactModel; }

    public List<Contact> getAllContacts() {

        if (null == allContacts)

            initializeContacts();

        return allContacts;

    }

    private void initializeContacts() {

        allContacts = contactModel.getAllContacts();

    }

 

    public void setAllContacts(List<Contact> list) { allContacts = list; }

 

    public ContactModel getContactModel() { return contactModel; }

    public void setContactModel(ContactModel contactModel) { this.contactModel = contactModel; }

 

    public void sortByFirstName(ActionEvent event) {

        Collections.sort(allContacts, new Contact.FirstNameComparator());

    }

    public void sortByLastName(ActionEvent event) {

        Collections.sort(allContacts, new Contact.LastNameComparator());

    }

    public void sortByCity(ActionEvent event) {

        Collections.sort(allContacts, new Contact.CityComparator());

    }

    public void sortByCountry(ActionEvent event) {

        Collections.sort(allContacts, new Contact.CountryComparator());

    }

    public String selectContactAction() {

        UIData table = (UIData) FacesContext.getCurrentInstance().getViewRoot().findComponent("dataTableForm").findComponent("contactsTable");

        Contact contact = (Contact) table.getRowData();

 

        System.out.println("Selected contact: " + contact);

 

        return "";

    }

}

 

4.4. dataTableExample.xhtml

This is the front end page requested by the client browser. It exposes just two columns of the Contact - the last and first names. It also

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"

      xmlns:ui="http://java.sun.com/jsf/facelets"

    xmlns:c="http://java.sun.com/jstl/core"

    xmlns:f="http://java.sun.com/jsf/core"

      xmlns:h="http://java.sun.com/jsf/html"

      xmlns:t="http://myfaces.apache.org/tomahawk">

  <head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

  <title>DataModel: Tutorial</title>

  </head>

  <body>

      <h:form id="dataTableForm">

          <h:dataTable id="contactsTable"

value="#{dataTableBean.allContacts}" var="contact" >

              <h:column>

                <f:facet name="header">

                    <h:commandLink id="lastName"

actionListener="#{dataTableBean.sortByLastName}">

                      Last Name

                    </h:commandLink>

                </f:facet>

                <h:commandLink id="name"

action="#{dataTableBean.selectContactAction}"

value="#{contact.lastName}" />

              </h:column>

              <h:column>

                  <f:facet name="header">

                      <h:commandLink id="firstName"

actionListener="#{dataTableBean.sortByFirstName}">

                          First Name

                      </h:commandLink>

                  </f:facet>

                  <h:outputText value="#{contact.firstName}"/>

              </h:column>

          </h:dataTable>

      </h:form>

  </body>

</html>

 

That's it - now run up your server, and let's mosey on over to http://localhost/dataTableExample.jsf.

 

Click on the Last Name and First Name column headers to change the sort order. Clicking on a link will select that contact, which is then printed to your console.

4.5. Saving State

So are we done? Not quite. The observant will have noticed all those extra "Must go over the wire to build list of contacts" lines that seem like an unnecessary performance overhead. Why would sorting a collection require a fresh retrieval of a dataset that we obtained just a few seconds ago?

And, doesn't JSF cache stuff like this somewhere? What about that component tree that is supposed to be restored during the Restore View phase?

 

To answer this, lets' consider what happens in an approximated version of the JSF lifecycle. See my previous article for specifics on the JSF life cycle.

As you can see, JSF does cache the tree (i.e. all the components that are used in the dataTableExample.xhtml file.) Interestingly, this include the contactsTable datatable. However, what gets stored/restored is just the tree and its appropriate bindings. I.e., when JSF resurrects the datatable component, it tells it which method to call to retrieve its data model (via its value="#{dataTableBean.allContacts}" attribute and method binding). However, the actual list of contacts as returned by getAllContacts() was never part of the view state, and so was not  stored during Render Response, and by extension, cannot be restored.

Are we doomed?

Not if we can trick JSF into making this data set part of the view state. What we need is a dummy component that we can attach to this view, by including it in the xhtml document. This component would then be bound to our list of Contacts. Then when the view is saved, during Render Response, this list of Contacts is saved as well; and when the view is restored, during Restore View, that list of Contacts will be resurrected and placed back in our page's managed bean.

That sounds great - so how do we get started building such a component?

The good news is that we don't need to - the work's already been done for us. All we need to do is include the Tomahawk tag libraries in our xhtml document, and bring the t:saveState tag into play.

All we need to do, is add this single line to our view:

<body>

<t:saveState value="#{dataTableBean.allContacts}" />

<h:form id="dataTableForm">

    <h:dataTable id="contactsTable" value="#{dataTableBean.allContacts}" var="contact" >

        <h:column>

            <f:facet name="header">

 

And we're off to the races!

 

Now restart your server, and mozy back on to http://localhost/dataTableExample.jsf.

Watch your console carefully. Sorting your list multiple times no longer causes the repeated retrieval of your contact list.

 

Caveats:

Note that you cannot store arbitrary objects using t:saveState - they should either be Serializable OR they should implement javax.faces.component.StateHolder and have a default constructor. See http://myfaces.apache.org/tomahawk-project/tomahawk12/tagdoc/t_saveState.html for more details.

Also note that you can "chain" the saved object from view to view to implement conversational scope. In other words, you can forward an object to another view as a request attribute, and as long as that view has a t:saveState tag, and its view bean has the appropriate setter/getter for this attribute, it will be restored as before.

Another caveat is that the order of any <t:saveState> tags is significant. I.e., you must restore any independent objects, before the objects that depend on them. Else, you'll get ugly looking exceptions that don't clearly indicate what is wrong.

5. TANSTAAFL

As with anything else in life, There Aint No Such Thing As A Free Lunch.

What seems like a free lunch here is actually just sleight of hand. You've reduced the over the wire costs between your web servers and your messaging/persistence servers, but you have increased the memory footprint of your application. It is not farfetched to imagine tables with thousands of rows - which can really take a toll on your server.

You may also choose to persist the component tree on the browser client - and then you tradeoff your memory use against bandwidth.

Another alternative is to explicitly place this in your HttpSession or in some other caching mechanism. However, this is the trickiest to get right - esp. since you then have to worry about stale caches and must tend to the health of your session.

The correct answer as to when each of these options should be used depends on your particular application and your particular motivations.

 

 

6. Filtering A Data Table

Last time round we looked at sorting a data table. This article looks at how you might implement a filtering table.

Note that filtering is available using the RichFaces <rich:dataTable>, so you may want to check out this tag before you read this article. However, the use case this is intended to solve is very different, and there should be room in the world for alternative implementations, so here goes.

The filtering idiom is composed of a filter subcomponent, and a sortable data table component. The filter is composed of the first letters of the last name of our contacts. Clickable filter links are provided when there is data that matches that filter.

It begins with no rows displayed, but it could just easily have defaulted to either displaying all rows, or just the rows for the first filter criterion.

Clicking on Show All displays all the rows in our table.

Clicking on a particular alphabet displays only those contacts that start with that letter.

Also, sorting works just as it did before.

Note that sorting and filtering multiple times does not cause us to go back to the server - t:saveState is working its wonders again.

 

A possible extension for the filter subcomponent could be to filter by ranges - e.g., [A-E], [F-J], ...; or [0-99], [100-199], ...; or [Aaa - Ace], [Acf - Bar], etc.

The default filtering by RichFaces works by a startsWith search of any number of characters that you type in to a text field that lives above the column.

6.1. FilteringDataTableBean.java

This file is an evolution of our earlier DataTableBean to support filtering. In addition to the list of all contacts (allContacts), we are now able to store the contacts that match our currently enforced filtering criterion (filteredContacts); our current filter criterion (filteredCharacter); and the complete set of filters with pointers into our data set (filters).

package com.swengsol.datatable;

 

import com.swengsol.domain.Contact;

import com.swengsol.domain.ContactModel;

 

import javax.faces.event.ActionEvent;

import javax.faces.component.UIData;

import javax.faces.context.FacesContext;

import java.util.List;

import java.util.Collections;

import java.util.ArrayList;

import java.io.Serializable;

 

public class FilteringDataTableBean {

    private List<TableDataFilter> filters;

    private List<Contact> allContacts;

    private List<Contact> filteredContacts;

    private ContactModel contactModel;

    private char filterCharacter;

 

    public FilteringDataTableBean(ContactModel contactModel) {
     this.contactModel = contactModel;

   }

    public List<Contact> getAllContacts() {

        if (null == allContacts)

            initializeContacts();

        return allContacts;

    }

    private void initializeContacts() {

        allContacts = contactModel.getAllContacts();

        Collections.sort(allContacts, new Contact.LastNameComparator());

        initializeFilters();

    }

    public void setAllContacts(List<Contact> list) { allContacts = list; }

 

    private void initializeFilters() {

        filters = new ArrayList<TableDataFilter>();

        for (char c = 'A'; c <= 'Z'; c++) {

            final TableDataFilter filter = new TableDataFilter(c);

            filters.add(filter);

        }

        if (null == allContacts) {

            return;

        }

        char currentLetter = '0';

        int i = -1;

        TableDataFilter filter = null;

        for (Contact contact: allContacts) {

            i++;

            final String name = contact.getLastName();

            if (name != null && name.length() > 0) {

                final char firstLetter = Character.toUpperCase( name.charAt(0) );

                if (firstLetter >= 'A' && firstLetter <= 'Z') {

                    if (currentLetter != firstLetter) {

                        filter = filters.get(firstLetter - 'A');

                        currentLetter = firstLetter;

                        filter.setStartIndex(i);

                    }

                    if (null != filter) {

                        filter.setEndIndex(i);

                    }

                }

            }

        }

    }

    public void setfilters(List<TableDataFilter> list) {

        filters = list;

    }

    public List getfilters() {

        getAllContacts();

        return filters;

    }

    public void filterContactsAction(ActionEvent event) {

        final String character = FacesContext.getCurrentInstance().getExternalContext().\
          getRequestParameterMap().get("filterCharacter");

        setFilterCharacter(character.toCharArray()[0]);

    }

    public List<Contact> getFilteredContacts() { return filteredContacts; }

    public void setfilteredContacts(List<Contact> list) {

        filteredContacts = list;

    }

    public char getFilterCharacter() { return filterCharacter; }

    public void setFilterCharacter(char filterCharacter) {

        this.filterCharacter = filterCharacter;

        if (filterCharacter == '*') {

            filteredContacts = allContacts;

        } else if (filterCharacter >= 'A' && filterCharacter <= 'Z') {

            final TableDataFilter filter = filters.get(filterCharacter - 'A');

            if (null == filter || !filter.isAvailable() ||
              filter.getStartIndex() == -1 || filter.getEndIndex() == -1)

                throw new RuntimeException("Should never happen");

            filteredContacts = allContacts.subList(filter.getStartIndex(),
                                                filter.getEndIndex() + 1);

        }

    }

 

    public ContactModel getContactModel() { return contactModel; }

    public void setContactModel(ContactModel contactModel) {
     this.contactModel = contactModel;
   }

 

    public void sortByFirstName(ActionEvent event) {

        Collections.sort(filteredContacts, new Contact.FirstNameComparator());

    }

    public void sortByLastName(ActionEvent event) {

        Collections.sort(filteredContacts, new Contact.LastNameComparator());

    }

    public void sortByCity(ActionEvent event) {

        Collections.sort(filteredContacts, new Contact.CityComparator());

    }

    public void sortByCountry(ActionEvent event) {

        Collections.sort(filteredContacts, new Contact.CountryComparator());

    }

    public String selectContactAction() {

        final UIData table = (UIData) FacesContext.getCurrentInstance().getViewRoot(). \
          findComponent("dataTableForm").findComponent("contactsTable");

        final Contact contact = (Contact) table.getRowData();

 

        System.out.println("Selected contact: " + contact);

 

        return "";

    }

    public static final class TableDataFilter implements Serializable {

        private char filterCharacter;

        private int startIndex = -1;

        private int endIndex = -1;

        public TableDataFilter() { }

        public TableDataFilter(char filter) { filterCharacter = filter; }

        public void setStartIndex(int start) { startIndex = start; }

        public void setEndIndex(int end) { endIndex = end; }

        public int getStartIndex() { return startIndex; }

        public int getEndIndex() { return endIndex; }

        public void setFilterCharacter(char filter) { filterCharacter = filter; }

        public char getFilterCharacter() { return filterCharacter; }

        public boolean isAvailable() { return (startIndex != -1); }

    }

}

 

6.2. FilteringDataTableExample.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"

      xmlns:ui="http://java.sun.com/jsf/facelets"

      xmlns:c="http://java.sun.com/jstl/core"

      xmlns:f="http://java.sun.com/jsf/core"

      xmlns:h="http://java.sun.com/jsf/html"

      xmlns:t="http://myfaces.apache.org/tomahawk">

<head>

    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

    <title>DataModel: Tutorial</title>

</head>

<body>

<t:saveState value="#{filteringDataTableBean.allContacts}" />

<t:saveState value="#{filteringDataTableBean.filters}" />

<t:saveState value="#{filteringDataTableBean.filterCharacter}" />

<h:form id="dataTableForm">

  <ul style="list-style-type:none; padding:0">

  <c:forEach items="#{filteringDataTableBean.filters}" var="filter">

    <li style="display: inline;">

         <c:if test="#{filter.available}">

          <h:commandLink actionListener="#{filteringDataTableBean.filterContactsAction}"

                           value="#{filter.filterCharacter}">

          <f:param name="filterCharacter" value="#{filter.filterCharacter}" />

         </h:commandLink>

      </c:if>

         <c:if test="#{!filter.available}">#{filter.filterCharacter}</c:if>

     </li>

  </c:forEach>

    <li style="display: inline;">

    <h:commandLink actionListener="#{filteringDataTableBean.filterContactsAction}"

                           value="Show All">

         <f:param name="filterCharacter" value="*" />

       </h:commandLink>

    </li>

  </ul>

 

  <h:dataTable id="contactsTable"

value="#{filteringDataTableBean.filteredContacts}" var="contact" >

  <h:column>

    <f:facet name="header">

         <h:commandLink id="lastName"

actionListener="#{filteringDataTableBean.sortByLastName}">

          Last Name

         </h:commandLink>

       </f:facet>

       <h:commandLink id="name"

action="#{filteringDataTableBean.selectContactAction}"

                    value="#{contact.lastName}" />

   </h:column>

    <h:column>

    <f:facet name="header">

         <h:commandLink id="firstName"
              actionListener="#{filteringDataTableBean.sortByFirstName}">

          First Name

      </h:commandLink>

       </f:facet>

     <h:outputText value="#{contact.firstName}"/>

   </h:column>

  </h:dataTable>

</h:form>

</body>

</html>

 

6.3. ApplicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

  <bean id="contactModel" class="com.swengsol.domain.ContactModel"
       factory-method="getInstance"/>

  <bean id="dataTableBean" class="com.swengsol.datatable.DataTableBean"
       scope="request">

  <constructor-arg ref="contactModel" />

  </bean>

  <bean id="filteringDataTableBean"
   class="com.swengsol.datatable.FilteringDataTableBean" scope="request">

  <constructor-arg ref="contactModel" />

  </bean>

</beans>