The ICEfaces Tutorial
Table of Contents


Lazy Loading DataTable with JPA

Editable DataTable with Pagination

This tutorial will display an editable dataTable using ICEfaces and JPA. All records displayed in the GUI will be lazily loaded. Most importantly, when a record is updated, the ICEfaces rendering api will push updates out to all users currenlty viewing the updated record.

The Java Persistence API (JPA), is a Java programming language framework that allows developers to manage relational data inside a Java EE 5 application server or outside an EJB container in a Java Standard Edition (Java SE) 5 application. Many features of third-party persistence frameworks were incorporated into the Java Persistence API, and projects like Hibernate and Open-Source Version TopLink Essentials are now implementations of the Java Persistence API. For this tutorial we use the Hibernate JPA implementation and include notes on any divergence between this implementation and TopLink Essentials. Hibernate allows us to package our database in the tutorial web application using HSQLDB and an import.sql file to populate records.

Lazy loading defers initialization of an object to the point when it is required, to make the program operate more efficiently. In the case of a dataTable, although we might bind a list of hundreds if not thousands of records to a component (for instance, an employee list) we only want to retrieve records from the database that the user is currently viewing. Our sample application will use an ICEfaces ice:dataTable component in conjunction with an ice:dataPaginator to display a subset of lazily loaded data in the GUI.

Finally, our tutorial dataTable is editable and if we want our users to be informed of record updates, we will have to bring our GUI to life. When a single user updates a record, ICEfaces will push this update out to all other users viewing that record. This is made possible with the ICEfaces rendering api.

This tutorial will discuss the following topics related to Lazy Loading a DataTable with JPA:

 

JPA Classes

 

1: persistence.xml File

JPA uses a persistence.xml file to define connection settings to the database. This tutorial uses JPA stand-alone (outside an EJB container) with Tomcat. This means an application managed (rather than injected) Entity Manager is used and we will not have support for JTA. Without support for JTA, we must specify a resource-local entity manager in the persistence.xml:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                                 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">

    <persistence-unit name="tutorialPU" transaction-type="RESOURCE_LOCAL">

        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <properties>
            <property name="hibernate.archive.autodetection"
                      value="class, hbm"/>

            <property name="hibernate.show_sql"
                      value="true"/>
            <property name="hibernate.format_sql"
                      value="true"/>

            <property name="hibernate.connection.driver_class"
                      value="org.hsqldb.jdbcDriver"/>
            <property name="hibernate.connection.url"
                      value="jdbc:hsqldb:mem:tutorialPU"/>
            <property name="hibernate.connection.username"
                      value="sa"/>
            <property name="hibernate.dialect"
                      value="org.hibernate.dialect.HSQLDialect"/>

            <property name="hibernate.hbm2ddl.auto"
                      value="create-drop"/>
        </properties>

    </persistence-unit>

</persistence>
      

Inclusion of the hibernate.archive.autodetection property means Hibernate will automatically find our entity classes, the classes do not need to be explicitly listed in this file. The hibernate.hbm2ddl.auto setting results in our in-memory database schema being dropped when the SessionFactory is closed explicitly.

To make our in-memory database work, we include an import.sql file in the runtime classpath of Hibernate (this has been available since Hibernate 3.1). After schema export Hibernate will execute the SQL statements contained in the import.sql file. The import.sql file allows us to insert the same 100 records into the in-memory HSQLDB database each time we run the web application.

For comparison, here is a more minimal persistence.xml file without Hibernate specific settings, using TopLink Essentials and a Derby database:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                                 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">

    <persistence-unit name="tutorialPU" transaction-type="RESOURCE_LOCAL">

        <provider>oracle.toplink.essentials.PersistenceProvider</provider>

        <class>com.icesoft.icefaces.samples.datatable.jpa.Customer</class>

        <properties>
            <property name="toplink.jdbc.user"
                      value="app"/>
            <property name="toplink.jdbc.password"
                      value="app"/>
            <property name="toplink.jdbc.url"
                      value="jdbc:derby://localhost:1527/sample"/>
            <property name="toplink.jdbc.driver"
                      value="org.apache.derby.jdbc.ClientDriver"/>
        </properties>

    </persistence-unit>

</persistence>
      
 

2: Entity

Here is our Customer Entity with JPA annotations detailing the database table and column names associated with the entity properties.

@Entity
@Table(name = "CUSTOMER", uniqueConstraints = {})
public class Customer implements java.io.Serializable {


    private Integer customernumber;
    private String contactlastname;
    private String contactfirstname;

    // Constructors

    /** default constructor */
    public Customer() {
    }

    /** minimal constructor */
    public Customer(Integer customernumber) {
        this.customernumber = customernumber;
    }

    /** full constructor */
    public Customer(Integer customernumber,
            String contactlastname, String contactfirstname) {
        this.customernumber = customernumber;
        this.contactlastname = contactlastname;
        this.contactfirstname = contactfirstname;
    }

    // Property accessors
    @Id
    @Column(name = "CUSTOMERNUMBER", unique = true, nullable = false, insertable = true, updatable = true)
    public Integer getCustomernumber() {
        return this.customernumber;
    }

    public void setCustomernumber(Integer customernumber) {
        this.customernumber = customernumber;
    }

    @Column(name = "CONTACTLASTNAME", unique = false, nullable = true, insertable = true, updatable = true, length = 50)
    public String getContactlastname() {
        return this.contactlastname;
    }

    public void setContactlastname(String contactlastname) {
        this.contactlastname = contactlastname;
    }

    @Column(name = "CONTACTFIRSTNAME", unique = false, nullable = true, insertable = true, updatable = true, length = 50)
    public String getContactfirstname() {
        return this.contactfirstname;
    }

    public void setContactfirstname(String contactfirstname) {
        this.contactfirstname = contactfirstname;
    }
      
 

3: Entity Manager

The EntityManagerHelper class is used to manage our entities. This is an example of an application managed Entity Manager used outside of an EJB3 container.

public class EntityManagerHelper {

    private static final EntityManagerFactory emf;
    private static final ThreadLocal threadLocal;

    static {
        emf = Persistence.createEntityManagerFactory("tutorialPU");
        threadLocal = new ThreadLocal();
    }

    public static EntityManager getEntityManager() {
        EntityManager manager = threadLocal.get();
        if (manager == null || !manager.isOpen()) {
            manager = emf.createEntityManager();
            threadLocal.set(manager);
        }
        return manager;
    }

    public static void closeEntityManager() {
        EntityManager em = threadLocal.get();
        threadLocal.set(null);
        if (em != null) em.close();
    }

    public static void beginTransaction() {
        getEntityManager().getTransaction().begin();
    }

    public static void commit() {
        getEntityManager().getTransaction().commit();
    }
      

The CustomerDAO class makes use of the EntityManager to interact with the database.

public class CustomerDAO implements ICustomerDAO{

    private EntityManager getEntityManager() {
        return EntityManagerHelper.getEntityManager();
    }

    public Customer update(Customer detachedInstance) {
        try {
            Customer result = getEntityManager().merge(detachedInstance);
            return result;
        } catch (RuntimeException re) {
            throw re;
        }
    }

    @SuppressWarnings("unchecked")
    public List findPageCustomers(String sortColumnName, boolean sortAscending, int startRow, int maxResults) {
        try {
            String queryString = "select c from Customer c order by c." + sortColumnName + " " + (sortAscending?"asc":"desc");
            // In the next statement a Hint is required with TopLink to force an update from the database.
            // This prevents stale data being assigned from the l2 cache when we push updates out from the server.
            // With Hibernate, we refresh the l1 cache by calling EntityManagerHelper.getEntityManager().clear() in the SessionBean.
            return getEntityManager().createQuery(queryString).setFirstResult(startRow).setMaxResults(maxResults).getResultList();
        } catch (RuntimeException re) {
            throw re;
        }
    }

    @SuppressWarnings("unchecked")
    public Long findTotalNumberCustomers() {
        try {
            String queryString = "select count(c) from Customer c";
            return (Long)getEntityManager().createQuery(queryString).getSingleResult();
        } catch (RuntimeException re) {
            throw re;
        }
    }

}
      

This concludes the summary of our JPA classes, the bridge between our database and the web application.

 

Web Page

This is our page, tutorial.jspx. There is nothing out of the ordinary in this page which consists of html, standard jsf and ICEfaces tags. Please review the component showcase or some of the other tutorials for a more thorough understanding of how the components work.

<?xml version="1.0" encoding="ISO-8859-1" ?>
<jsp:root version="1.2"
          xmlns:jsp="http://java.sun.com/JSP/Page"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:h="http://java.sun.com/jsf/html"
          xmlns:ice="http://www.icesoft.com/icefaces/component">
    <jsp:directive.page contentType="text/html;charset=ISO-8859-1" pageEncoding="ISO-8859-1" />
    <f:view>
        <ice:outputDeclaration doctypeRoot="html"
                               doctypePublic="-//W3C//DTD XHTML 1.0 Transitional//EN"
                               doctypeSystem="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" />
    <html>
        <head>
            <title>ICEfaces, Ajax for Java EE</title>
            <link rel="stylesheet" type="text/css" href="./xmlhttp/css/xp/xp.css"/>
            <link rel="stylesheet" type="text/css" href="./tutorial.css"/>
        </head>
        <body>

            <ice:outputText value="Thank you for using ICEfaces." />
            <ice:outputConnectionStatus />

            <ice:form partialSubmit="true" >

                <ice:dataPaginator for="customerdatatable"
                                   paginator="true"
                                   fastStep="3" >
                    <f:facet name="first">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-first.gif"/>
                    </f:facet>
                    <f:facet name="last">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-last.gif"/>
                    </f:facet>
                    <f:facet name="previous">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-previous.gif"/>
                    </f:facet>
                    <f:facet name="next">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-next.gif"/>
                    </f:facet>
                    <f:facet name="fastforward">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-ff.gif"/>
                    </f:facet>
                    <f:facet name="fastrewind">
                        <ice:graphicImage style="border:none;"
                            url="./xmlhttp/css/xp/css-images/arrow-fr.gif"/>
                    </f:facet>
                </ice:dataPaginator>

                <ice:dataTable cellspacing="1"
                               value="#{sessionBean.data}"
                               id="customerdatatable"
                               rows="#{sessionBean.pageSize}"
                               sortAscending="#{sessionBean.sortAscending}"
                               sortColumn="#{sessionBean.sortColumnName}"
                               width="750px"
                               rowClasses="evenRow,oddRow"
                               var="customerbean">
                    <ice:column>

                        <ice:rowSelector value="#{customerbean.expanded}" selectedClass="tableRowSelected" mouseOverClass="tableRowMouseOver" toggleOnClick="false"/>

                        <!-- HEADER -->
                        <f:facet name="header">
                            <ice:panelGrid columns="2" width="100%" cellspacing="1" columnClasses="headerColumn" >
                                <ice:commandSortHeader arrow="true" columnName="#{sessionBean.CONTACTFIRSTNAME}">
                                    <ice:outputText value="#{sessionBean.FIRSTCOLUMNNAME}"/>
                                </ice:commandSortHeader>
                                <ice:commandSortHeader arrow="true" columnName="#{sessionBean.CONTACTLASTNAME}">
                                    <ice:outputText value="#{sessionBean.SECONDCOLUMNNAME}"/>
                                </ice:commandSortHeader>
                            </ice:panelGrid>
                        </f:facet>

                        <!-- TABLE CONTENT ROW -->
                        <ice:panelGrid columns="2" width="100%" cellspacing="1" columnClasses="dataColumn">
                            <ice:commandLink actionListener="#{customerbean.toggleSelected}">
                                <ice:outputText value="#{customerbean.customer.contactfirstname}"/>
                            </ice:commandLink>
                            <ice:commandLink actionListener="#{customerbean.toggleSelected}">
                                <ice:outputText value="#{customerbean.customer.contactlastname}"/>
                            </ice:commandLink>
                        </ice:panelGrid>

                        <!-- EDITABLE ROW  -->
                        <ice:panelGrid columns="4" rendered="#{customerbean.expanded}" width="100%" columnClasses="inputColumn,inputColumn,buttonColumn,buttonColumn">
                            <ice:inputText value="#{customerbean.tempcontactfirstname}" />
                            <ice:inputText value="#{customerbean.tempcontactlastname}" />
                            <ice:commandButton value="Commit" actionListener="#{customerbean.commit}" />
                            <ice:commandButton value="Cancel" actionListener="#{customerbean.cancel}" />
                        </ice:panelGrid>

                    </ice:column>

                </ice:dataTable>
                <ice:messages />
            </ice:form>

        </body>
    </html>
    </f:view>
</jsp:root>

      

The page contains a single column dataTable with a panelGrid of table content and another panelGrid containing the editable row. The editable row is rendered when the table content row has been clicked. The source of this click is the ice:commandLink that nests the content in the row. This decision was made because the editable row contains buttons and input components that should not fire the ice:rowSelector toggleOnClick. As a result, we set the ice:rowSelector toggleOnClick attribute to "false" and use the ice:commandLinks to toggle the rendering of the editable rows. This means the ice:rowSelector component is relegated to applying the appropriate styles to our rows, based on it's bound value.

We will now see what happens with the component value bindings and actionListeners in the web application beans.

 

Web Application Classes

 

1: Web Page Bindings

The Customer entity is represented in the GUI by the CustomerBean, a wrapper class with view specific properties and methods.

CustomerBean holds the expanded property, which is bound to the ice:rowSelector component and determines when the editable row ice:panelGrid is rendered. The tempcontactfirstname and tempcontactlastname properties are bound to the ice:inputText components in the editable row.

CustomerBean also has the methods bound to the actionListeners in the ice:dataTable. The toggleSelected method is called when the ice:commandLinks are used to expand/contract an editable row. The commit and cancel methods are bound to the ice:commandButton components to commit or cancel changes to a particular record.

public class CustomerBean {

    private Customer customer;
    private SessionBean sessionBean;
    private boolean expanded = false;

    private String tempcontactfirstname;
    private String tempcontactlastname;

    public CustomerBean (Customer customer, SessionBean sessionBean){
        this.customer = customer;
        this.sessionBean = sessionBean;
        tempcontactfirstname = customer.getContactfirstname();
        tempcontactlastname = customer.getContactlastname();
    }

    public boolean isExpanded() {
        return expanded;
    }

    public void setExpanded(boolean expanded) {
        this.expanded = expanded;
    }

    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    public String getTempcontactfirstname() {
        return tempcontactfirstname;
    }

    public void setTempcontactfirstname(String tempcontactfirstname) {
        this.tempcontactfirstname = tempcontactfirstname;
    }

    public String getTempcontactlastname() {
        return tempcontactlastname;
    }

    public void setTempcontactlastname(String tempcontactlastname) {
        this.tempcontactlastname = tempcontactlastname;
    }

    /**
     * Bound to commandLink actionListener in the ui that renders/unrenders
     * the Customer details for editing.
     */
    public void toggleSelected(ActionEvent e){
        tempcontactfirstname = customer.getContactfirstname();
        tempcontactlastname = customer.getContactlastname();
        expanded = !expanded;
    }

    /**
     * Bound to commandButton actionListener in the ui that commits Customer
     * changes to the database.
     */
    public void commit(ActionEvent e){
        customer.setContactfirstname(tempcontactfirstname);
        customer.setContactlastname(tempcontactlastname);
        sessionBean.commit(customer);
        expanded = !expanded;
    }

    /**
     * Bound to commandButton actionListener in the ui that cancels potential
     * Customer changes to the database and unrenders the editable Customer
     * details.
     */
    public void cancel(ActionEvent e){
        tempcontactfirstname = customer.getContactfirstname();
        tempcontactlastname = customer.getContactlastname();
        expanded = !expanded;
    }

}
      

The rest of the bindings in tutorial.jspx are to the SessionBean class. SessionBean is our web application controller class that makes calls to the JPA layer to maintain a list of records displayed by a particular user during his/her session.

For the ice:commandSortHeader to work, we add the sortAscending and sortColumn attributes to the ice:dataTable component. The ice:dataTable's visible rows are set with a binding to the rows attribute. These bindings are encapsulated in a class called DataSource. SessionBean extends this class.

/**
 * The DataSource class is a utility class used by the data table paginator
 * and commandSortHeader.
 */
public abstract class DataSource {

    // Sortable Headers
    protected String sortColumnName;
    protected boolean sortAscending;

    // DataModel bound to dataTable
    protected PagedListDataModel onePageDataModel;
    // bound to rows attribute in dataTable
    protected int pageSize = 5;

    protected DataSource(String defaultSortColumn) {
        sortColumnName = defaultSortColumn;
        sortAscending = isDefaultAscending(defaultSortColumn);
    }

    /**
     * Is the default sortColumnName direction for the given column "sortAscending" ?
     */
    protected abstract boolean isDefaultAscending(String sortColumn);

    /**
     * Gets the sortColumnName column.
     *
     * @return column to sortColumnName
     */
    public String getSortColumnName() {
        return sortColumnName;
    }

    /**
     * Sets the sortColumnName column.
     *
     * @param sortColumnName column to sortColumnName
     */
    public void setSortColumnName(String sortColumnName) {
        if(!sortColumnName.equals(this.sortColumnName)){
            onePageDataModel.setDirtyData();
            this.sortColumnName = sortColumnName;
        }
    }

    /**
     * Is the sortColumnName sortAscending?
     *
     * @return true if the sortAscending sortColumnName otherwise false
     */
    public boolean isSortAscending() {
        return sortAscending;
    }

    /**
     * Sets sortColumnName type.
     *
     * @param sortAscending true for sortAscending sortColumnName, false for descending sortColumnName.
     */
    public void setSortAscending(boolean sortAscending) {
        if(sortAscending != (this.sortAscending)){
            onePageDataModel.setDirtyData();
            this.sortAscending = sortAscending;
        }
    }

    public PagedListDataModel getOnePageDataModel() {
        return onePageDataModel;
    }

    public int getPageSize() {
        return pageSize;
    }
}
      

There is one more important binding in tutorial.jspx and that is the ice:dataTable value binding. This leads us to the subject of lazy loading and the use of the PagedListDataModel property in the DataSource class.

 

2: Lazy Loading

In order to lazily load our dataTable content, we need to create the notion of a DataPage that contains only the records currently displayed in the GUI. This also requires us extending the standard JSF DataModel to work with the DataPage. The next two classes come from WorkingWithLargeTables in the Myfaces Wiki.

First, the "page" of data that gets passed back to the GUI.

/**
 * A simple class that represents a "page" of data out of a longer set, ie
 * a list of objects together with info to indicate the starting row and
 * the full size of the dataset. Business methods can return instances of this type
 * when returning subsets of available data.
 */
public class DataPage {

  private int datasetSize;
  private int startRow;
  private List data;

  /**
   * Create an object representing a sublist of a dataset.
   *
   * @param datasetSize is the total number of matching rows
   * available.
   *
   * @param startRow is the index within the complete dataset
   * of the first element in the data list.
   *
   * @param data is a list of consecutive objects from the
   * dataset.
   */
  public DataPage(int datasetSize, int startRow, List data) {
    this.datasetSize = datasetSize;
    this.startRow = startRow;
    this.data = data;
  }

  /**
   * Return the number of items in the full dataset.
   */
  public int getDatasetSize() {
    return datasetSize;
  }

  /**
   * Return the offset within the full dataset of the first
   * element in the list held by this object.
   */
  public int getStartRow() {
    return startRow;
  }

  /**
   * Return the list of objects held by this object, which
   * is a continuous subset of the full dataset.
   */
  public List getData() {
    return data;
  }
}
      

The PagedListDataModel, our custom DataModel for lazy loading paginated data. In our case the ice:dataPaginator is used to navigate paginated data bound to the ice:dataTable component.

/**
 * A special type of JSF DataModel to allow a datatable and datapaginator
 * to page through a large set of data without having to hold the entire
 * set of data in memory at once.
 *
 * Any time a managed bean wants to avoid holding an entire dataset,
 * the managed bean should declare an inner class which extends this
 * class and implements the fetchData method. This method is called
 * as needed when the table requires data that isn't available in the
 * current data page held by this object.
 *
 * This does require the managed bean (and in general the business
 * method that the managed bean uses) to provide the data wrapped in
 * a DataPage object that provides info on the full size of the dataset.
 */
public abstract class PagedListDataModel extends DataModel {

    int pageSize;
    int rowIndex;
    DataPage page;
    // Triggers a fetch from the database
    protected boolean dirtyData = false;

    /*
     * Create a datamodel that pages through the data showing the specified
     * number of rows on each page.
     */
    public PagedListDataModel(int pageSize) {
        super();
        this.pageSize = pageSize;
        this.rowIndex = -1;
        this.page = null;
    }

    /**
     * Not used in this class; data is fetched via a callback to the
     * fetchData method rather than by explicitly assigning a list.
     */
    @Override
    public void setWrappedData(Object o) {
        throw new UnsupportedOperationException("setWrappedData");
    }

    @Override
    public int getRowIndex() {
        return rowIndex;
    }

    /**
     * Specify what the "current row" within the dataset is. Note that
     * the UIData component will repeatedly call this method followed
     * by getRowData to obtain the objects to render in the table.
     */
    @Override
    public void setRowIndex(int index) {
        rowIndex = index;
    }

    /**
     * Return the total number of rows of data available (not just the
     * number of rows in the current page!).
     */
    @Override
    public int getRowCount() {
        return getPage().getDatasetSize();
    }

    /**
     * Return a DataPage object; if one is not currently available then
     * fetch one. Note that this doesn't ensure that the datapage
     * returned includes the current rowIndex row; see getRowData.
     */
    private DataPage getPage() {
        if (page != null)
            return page;

        int rowIndex = getRowIndex();
        int startRow = rowIndex;
        if (rowIndex == -1) {
            // even when no row is selected, we still need a page
            // object so that we know the amount of data available.
           startRow = 0;
        }

        // invoke method on enclosing class
        page = fetchPage(startRow, pageSize);
        return page;
    }

    /**
     * Return the object corresponding to the current rowIndex.
     * If the DataPage object currently cached doesn't include that
     * index or the data is marked as dirty, then fetchPage is called
     * to retrieve the appropriate page.
     */
    @Override
    public Object getRowData(){
        if (rowIndex < 0) {
            throw new IllegalArgumentException(
                "Invalid rowIndex for PagedListDataModel; not within page");
        }

        // ensure page exists; if rowIndex is beyond dataset size, then
        // we should still get back a DataPage object with the dataset size
        // in it...
        if (page == null) {
            page = fetchPage(rowIndex, pageSize);
        }

        // Check if rowIndex is equal to startRow,
        // useful for dynamic sorting on pages
        if (rowIndex == page.getStartRow() && dirtyData){
            page = fetchPage(rowIndex, pageSize);
        }

        int datasetSize = page.getDatasetSize();
        int startRow = page.getStartRow();
        int nRows = page.getData().size();
        int endRow = startRow + nRows;

        if (rowIndex >= datasetSize) {
            throw new IllegalArgumentException("Invalid rowIndex");
        }

        if (rowIndex < startRow) {
            page = fetchPage(rowIndex, pageSize);
            startRow = page.getStartRow();
        } else if (rowIndex >= endRow) {
            page = fetchPage(rowIndex, pageSize);
            startRow = page.getStartRow();
        }
        return page.getData().get(rowIndex - startRow);
    }

    @Override
    public Object getWrappedData() {
        return page.getData();
    }

    /**
     * Return true if the rowIndex value is currently set to a
     * value that matches some element in the dataset. Note that
     * it may match a row that is not in the currently cached
     * DataPage; if so then when getRowData is called the
     * required DataPage will be fetched by calling fetchData.
     */
    @Override
    public boolean isRowAvailable() {
        DataPage page = getPage();
        if (page == null)
            return false;

        int rowIndex = getRowIndex();
        if (rowIndex < 0) {
            return false;
        } else if (rowIndex >= page.getDatasetSize()) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Method which must be implemented in cooperation with the
     * managed bean class to fetch data on demand.
     */
    public abstract DataPage fetchPage(int startRow, int pageSize);

    public boolean isDirtyData() {
        return dirtyData;
    }

    public void setDirtyData(boolean dirtyData) {
        this.dirtyData = dirtyData;
    }

    public void setDirtyData() {
        dirtyData = true;
    }
}
      

The dirtyData property has been added so we can trigger a fetch from outside the paginating mechanisms. For instance, we will need to mark data as dirty and force a fetch in all GUI's viewing an updated record.

Here is the relevant code in the SessionBean, including getData() which is bound to the GUI:

    // Current items in ui
    private List uiCustomerBeans = new ArrayList(pageSize);

    /**
     * Bound to DataTable value in the ui.
     */
    public DataModel getData() {
        state = PersistentFacesState.getInstance();
        if(onePageDataModel == null){
            onePageDataModel = new LocalDataModel(pageSize);
        }
        return onePageDataModel;
    }

    /**
     * This is where the Customer data is retrieved from the database and
     * returned as a list of CustomerBean objects for display in the UI.
     */
    private DataPage getDataPage(int startRow, int pageSize) {
        // Retrieve the total number of customers from the database.  This
        // number is required by the DataPage object so the paginator will know
        // the relative location of the page data.
        int totalNumberCustomers = CUSTOMERDAO.findTotalNumberCustomers().intValue();

        // Calculate indices to be displayed in the ui.
        int endIndex = startRow + pageSize;
        if (endIndex > totalNumberCustomers) {
            endIndex = totalNumberCustomers;
        }

        // Query database for sorted results.
        List pageCustomers = CUSTOMERDAO.findPageCustomers(sortColumnName, sortAscending, startRow, endIndex-startRow);

        // Remove this Renderable from the existing render groups.
        leaveRenderGroups();

        // Populate the list of uiCustomerBeans for binding to the dataTable.
        uiCustomerBeans.clear();
        for(Customer pageCustomer : pageCustomers){
            uiCustomerBeans.add(new CustomerBean(pageCustomer, this));
        }
        // Add this Renderable to the new render groups.
        joinRenderGroups();

        // Reset the dirtyData flag.
        onePageDataModel.setDirtyData(false);

        // This is required when using Hibernate JPA.  If the EntityManager is not
        // cleared or closed objects are cached and stale objects will show up
        // in the table.
        // This way, the detached objects are reread from the database.
        // This call is not required with TopLink JPA, which uses a Query Hint
        // to clear the l2 cache in CustomerDAO.
        EntityManagerHelper.getEntityManager().clear();

        return new DataPage(totalNumberCustomers,startRow,uiCustomerBeans);
    }

    private class LocalDataModel extends PagedListDataModel {
        public LocalDataModel(int pageSize) {
            super(pageSize);
        }

        public DataPage fetchPage(int startRow, int pageSize) {
            // call enclosing managed bean method to fetch the data
            return getDataPage(startRow, pageSize);
        }
    }
      

PersistentFacesState, leaveRenderGroups() and joinRenderGroups() are all related to using the ICEfaces rendering api. A render call will trigger a server push to all users viewing an updated record. The use of this api is our next topic.

 

3: Using the Rendering API

The key elements to the Rendering API are:

Renderable: A bean that implements the Renderable interface and associates the bean with a specific PersistentFacesState. Typically, there will be a single Renderable per client.

RenderManager: An application-scoped bean that manages all rendering requests through the RenderHub and a set of named GroupAsyncRenderers.

GroupAsyncRenderer: Supports rendering of a group of Renderables. GroupAsyncRenderers can support on-demand, interval, and delayed rendering of a group. This tutorial uses OnDemandRenderers.

Here is how it works. The RenderManager is an application scope managed bean that the SessionBean has a reference to from faces-config.xml. When a call is made to joinRenderGroups(), the RenderManager is used to retrieve or create an OnDemandRenderer for the specified Customer object (a call to RenderManager.getOnDemandRenderer(String customernumber) will create the OnDemandRenderer if the name is not found). After the OnDemandRenderer is created or retrieved, the SessionBean Renderable is added to the Customer render group. The SessionBean implements Renderable, so its member PersistentFacesState will ensure it receives a push from the server when a render call is made to this render group.

After the commit button is pressed in the GUI and a successful update is performed, a render call is made to the updated Customer's render group. At this point getState() will be called on all the Renderables in that group, so we mark the data as dirty in the getState() method to ensure these views will fetch fresh data from the database, before rendering. Please consult the ICEfaces documentation for more information on the rendering api.

The relevant code from SessionBean:

    // required for rendering api
    private RenderManager renderManager;
    private PersistentFacesState state = PersistentFacesState.getInstance();

    /**
     * This method is called when a render call is made from the server.  Render
     * calls are only made to views containing an updated record. The data is
     * marked as dirty to trigger a fetch of the updated record from the
     * database before rendering takes place.
     */
    public PersistentFacesState getState() {
        onePageDataModel.setDirtyData();
        return state;
    }

    /**
     * This method is called from faces-config.xml with each new session.
     */
    public void setRenderManager(RenderManager renderManager) {
        this.renderManager = renderManager;
    }

    public void renderingException(RenderingException arg0) {
        if (arg0 instanceof TransientRenderingException ){

        }
        else if(arg0 instanceof FatalRenderingException){
            // Remove from existing Customer render groups.
            leaveRenderGroups();
        }
    }

    /**
     * Remove this Renderable from existing uiCustomerBeans render groups.
     * OnDemandRenderers are named/created using the underlying Customer Number.
     */
    private void leaveRenderGroups(){
        if(onePageDataModel.page != null){
            for(CustomerBean uiCustomerBean : uiCustomerBeans){
                renderManager.getOnDemandRenderer(uiCustomerBean.getCustomer().getCustomernumber().toString()).remove(this);
            }
        }
    }

    /**
     * Add this Renderable to the new uiCustomerBeans render groups.
     * OnDemandRenderers are named/created using the underlying Customer Number.
     */
    private void joinRenderGroups(){
        for(CustomerBean uiCustomerBean : uiCustomerBeans){
            renderManager.getOnDemandRenderer(uiCustomerBean.getCustomer().getCustomernumber().toString()).add(this);
        }
    }

    /**
     * Commit updates Customer data to the database and requests a render of views
     * containing the Customer data so they will be refreshed with the update.
     */
    public void commit(Customer customer){
        EntityManagerHelper.beginTransaction();
        CUSTOMERDAO.update(customer);
        EntityManagerHelper.commit();
        renderManager.getOnDemandRenderer(customer.getCustomernumber().toString()).requestRender();
    }
      

The faces-config shows our managed beans and how they are chained together:

<managed-bean>
    <description>
        Used to initiate server side renders
    </description>
    <managed-bean-name>renderManager</managed-bean-name>
    <managed-bean-class>
        com.icesoft.faces.async.render.RenderManager
    </managed-bean-class>
    <managed-bean-scope>application</managed-bean-scope>
</managed-bean>

<managed-bean>
    <managed-bean-name>sessionBean</managed-bean-name>
    <managed-bean-class>
        com.icesoft.icefaces.samples.datatable.ui.SessionBean
    </managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
        <property-name>renderManager</property-name>
        <value>#{renderManager}</value>
</managed-bean>
      
 

4: Disposable Bean Interface

Finally, we implement the disposable bean interface on our SessionBean. This is an ICEfaces interface that allows us to be notified when the bean instance is no longer in use. This means we can perform the following cleanup in the SessionBean when the session ends:

    /**
     * Cleanup - before this object is disposed, leave RenderGroups and remove
     * this SessionBean from the list of current sessions.
     */
    public void dispose() {
        // Remove from existing Customer render groups.
        leaveRenderGroups();
    }
      

This concludes the tutorial. You may view the source code by following the links below.

 

Example

Example Source Notes
dataTable-JPA dataTable-JPA source code Connects to an in-memory HSQLDB database through Hibernate JPA.

The ICEfaces Tutorial
Table of Contents

Copyright 2009 ICEsoft Technologies Inc. All rights reserved.