Graceful AJAX Components CRUD

This example builds on AJAX Components CRUD, modifying it to handle when JavaScript support has been turned off in the browser.
Highlight zone updates? This shows you which zones are updated by the Ajax response.

Create...

Delete

Delete Mary2 Contrary?

Cancel
All the comments made in AJAX Components CRUD apply to this example too.

References: Zone, Request, AjaxResponseRenderer, ComponentResources, @Inject, @InjectComponent.

Home

The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.
The source for CustomForm and CustomError is shown in the No Validation Bubbles example.

Persons.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/together/gracefulajaxcomponentscrud.css}"/>
</head>
<body>
    <h1>Graceful AJAX Components CRUD</h1>
    
    This example builds on <em>AJAX Components CRUD</em>, modifying it to handle when JavaScript support has been turned off in 
    the browser.<br/>

    <div class="eg">
        <form t:type="form" t:id="preferencesForm">
            <noscript class="js-recommended">
                ${message:javascript_recommended}
            </noscript>     
            Highlight zone updates? 
             <input t:type="checkbox" t:id="highlightZoneUpdates" value="highlightZoneUpdates" onclick="this.form.submit()"/>
            This shows you which zones are updated by the Ajax response.
        </form><br/>
    
        <a t:type="eventLink" t:event="toCreate" t:zone="editorZone" href="#">Create...</a><br/>
        
        <table id="listAndEditor">
            <tbody>
                <tr>
    
                    <!-- This is the left side of the table: a list of Persons -->
    
                    <td id="listCell">
                        <t:zone t:id="listZone" id="listZone" t:update="prop:zoneUpdateFunction">
                            <t:together.gracefulajaxcomponentscrud.PersonList t:id="list" partialName="partialName" selectedPersonId="listPersonId"/>
                        </t:zone>
                    </td>
                    
                    <!-- This is the right side of the table: where a Person will be created, reviewed, or updated. -->
            
                    <td id="editorCell">
                        <t:zone t:id="editorZone" id="editorZone" t:update="prop:zoneUpdateFunction">
                            <t:together.gracefulajaxcomponentscrud.PersonEditor t:id="editor" mode="editorMode" personId="editorPersonId"/>
                        </t:zone>
                    </td>
                    
                </tr>
            </tbody>
        </table>
    </div>
    
    All the comments made in <em>AJAX Components CRUD</em> apply to this example too.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/Zone.html">Zone</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/Request.html">Request</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/ajax/AjaxResponseRenderer.html">AjaxResponseRenderer</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/ComponentResources.html">ComponentResources</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">@Inject</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/annotations/InjectComponent.html">@InjectComponent</a>.<br/><br/> 
    
    <a t:type="pageLink" t:page="Index" href="#">Home</a><br/><br/>
    
    The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.<br/>
    The source for CustomForm and CustomError is shown in the No Validation Bubbles example.<br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/gracefulajaxcomponentscrud/Persons.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/gracefulajaxcomponentscrud/Persons.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/together/gracefulajaxcomponentscrud/PersonList.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/together/gracefulajaxcomponentscrud/PersonList.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/together/gracefulajaxcomponentscrud/PersonEditor.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/together/gracefulajaxcomponentscrud/PersonEditor.properties"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/components/together/gracefulajaxcomponentscrud/PersonEditor.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/together/gracefulajaxcomponentscrud.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/Confirm.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/Confirm.js"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/model/together/PersonFilteredDataSource.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/Person.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/Regions.java"/>
</body>
</html>

Persons.java


package jumpstart.web.pages.together.gracefulajaxcomponentscrud;

import jumpstart.web.components.together.gracefulajaxcomponentscrud.PersonEditor.Mode;

import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.annotations.ActivationRequestParameter;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

public class Persons {

    // The activation context

    @Property
    private Mode editorMode;

    @Property
    private Long editorPersonId;

    @ActivationRequestParameter
    private Mode arpEditorMode;

    @ActivationRequestParameter
    private Long arpEditorPersonId;
    
    // Screen fields

    @Property
    @Persist
    private boolean highlightZoneUpdates;

    @Property
    // If we use @ActivationRequestParameter instead of @Persist, then our handler for filter form success would have
    // to render more than just the listZone, it would have to render all other links and forms: it would need a zone
    // around the "Create..." link so it could render it; and it would render the editorZone, which would be destructive
    // if the user has been typing into Create or Update. Alternatively, it could use a custom JavaScript callback to
    // update the partialName in all other links and forms - see AjaxResponseRenderer#addCallback(JavaScriptCallback).
    @Persist
    private String partialName;

    @Property
    private Long listPersonId;

    // Generally useful bits and pieces

    @InjectComponent
    private Zone listZone;

    @InjectComponent
    private Zone editorZone;

    @Inject
    private Request request;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    // The code

    // onPassivate() is called by Tapestry to get the activation context to put in the URL.

    Object[] onPassivate() {

        if (request.isXHR()) {
            arpEditorMode = editorMode;
            arpEditorPersonId = editorPersonId;
            
            return null;
        }
        else {
            
            if (editorMode == null) {
                return null;
            }
            else if (editorMode == Mode.CREATE) {
                return new Object[] { editorMode };
            }
            else if (editorMode == Mode.REVIEW || editorMode == Mode.UPDATE
                    || editorMode == Mode.CONFIRM_DELETE) {
                return new Object[] { editorMode, editorPersonId };
            }
            else {
                throw new IllegalStateException(editorMode.toString());
            }
        }

    }

    // onActivate() is called by Tapestry to pass in the activation context from the URL.

    void onActivate(EventContext ec) {

        if (request.isXHR()) {
            editorMode = arpEditorMode;
            editorPersonId = arpEditorPersonId;
        }
        else {
            if (ec.getCount() == 0) {
                editorMode = null;
                editorPersonId = null;
            }
            else if (ec.getCount() == 1) {
                editorMode = ec.get(Mode.class, 0);
                editorPersonId = null;
            }
            else {
                editorMode = ec.get(Mode.class, 0);
                editorPersonId = ec.get(Long.class, 1);
            }
        }

        listPersonId = editorPersonId;
    }

    // setupRender() is called by Tapestry right before it starts rendering the page.

    void setupRender() {
        listPersonId = editorPersonId;
    }

    // /////////////////////////////////////////////////////////////////////
    // FILTER
    // /////////////////////////////////////////////////////////////////////

    // Handle event "filter" from component "list"

    void onFilterFromList() {
        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toCreate" from this page

    void onToCreate() {
        editorMode = Mode.CREATE;
        editorPersonId = null;
        listPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // Handle event "cancelCreate" from component "editor"

    void onCancelCreateFromEditor() {
        editorMode = null;
        editorPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "successfulCreate" from component "editor"

    void onSuccessfulCreateFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
        listPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // Handle event "failedCreate" from component "editor"

    void onFailedCreateFromEditor() {
        editorMode = Mode.CREATE;
        editorPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    // Handle event "selected" from component "list"

    void onSelectedFromList(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
        listPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toUpdate" from component "editor"

    void onToUpdateFromEditor(Long personId) {
        editorMode = Mode.UPDATE;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "cancelUpdate" from component "editor"

    void onCancelUpdateFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "successfulUpdate" from component "editor"

    void onSuccessfulUpdateFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
        listPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // Handle event "failedUpdate" from component "editor"

    void onFailedUpdateFromEditor(Long personId) {
        editorMode = Mode.UPDATE;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // DELETE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "successfulDelete" from component "editor"

    void onSuccessfulDeleteFromEditor(Long personId) {
        editorMode = null;
        editorPersonId = null;
        listPersonId = null;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(listZone).addRender(editorZone);
        }
    }

    // Handle event "failedDelete" from component "editor"

    void onFailedDeleteFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // CONFIRM DELETE - used only when JavaScript is disabled.
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toConfirmDelete" from component "editor"

    void onToConfirmDeleteFromEditor(Long personId) {
        editorMode = Mode.CONFIRM_DELETE;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "cancelConfirmDelete" from component "editor"

    Object onCancelConfirmDeleteFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
        listPersonId = personId;
        return null;
    }

    // Handle event "successfulConfirmDelete" from component "editor"

    void onSuccessfulConfirmDeleteFromEditor(Long personId) {
        editorMode = null;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // Handle event "failedConfirmDelete" from component "editor"

    void onFailedConfirmDeleteFromEditor(Long personId) {
        editorMode = Mode.CONFIRM_DELETE;
        editorPersonId = personId;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(editorZone);
        }
    }

    // /////////////////////////////////////////////////////////////////////
    // GETTERS
    // /////////////////////////////////////////////////////////////////////

    public String getZoneUpdateFunction() {
        return highlightZoneUpdates ? "highlight" : "show";
    }

}

PersonList.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
<!--  At runtime the page will supply the stylesheet. The link here is to enable preview. -->
<link rel="stylesheet" type="text/css" href="../../../css/examples/ajaxgracefulcomponentscrud.css"/>

<t:content>

    <!-- We can't use the form's id in the css because the Zone messes with it, so we add a div with its own id. -->
    <div id="personFilter">
        <form t:type="form" t:id="filterForm" t:zone="^">
            <div>
                Person
            </div>
            <div>
                <input t:id="partialName" t:type="TextField" size="15" t:validate="maxLength=15"/>
                <input type="submit" value="Filter" title="Filter"/>
            </div>
        </form>
    </div>
    
    <div id="personList">
        <table t:type="grid" t:id="list" t:source="persons" t:row="person"
            t:exclude="id,version,firstName,lastName,region,startDate" t:add="name"
            t:rowsPerPage="4" t:pagerPosition="bottom"
            t:class="personGrid" t:empty="block:emptyPersons" t:inplace="ajax">[Grid here]
            <p:nameCell>
                <a t:type="eventLink" t:event="selected" t:context="person.id" class="prop:linkCSSClass" t:zone="^" href="#">
                    ${person.firstName} ${person.lastName}
                </a>
            </p:nameCell>
        </table>
    </div>
        
    <t:block t:id="emptyPersons">
        <div id="noPersons">
            (No persons found)
        </div>
    </t:block>

</t:content>
</html>

PersonList.java


package jumpstart.web.components.together.gracefulajaxcomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.web.model.together.PersonFilteredDataSource;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonList#SELECTED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may trigger. It is not checked at runtime.
@Events({ PersonList.SELECTED })
public class PersonList {
    public static final String SELECTED = "selected";

    // Parameters

    @Parameter(required = true)
    @Property
    private String partialName;

    @Parameter(required = true)
    @Property
    private Long selectedPersonId;

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @Inject
    private ComponentResources componentResources;

    @Inject
    private Request request;

    // The code

    boolean onSuccessFromFilterForm() {
        // Trigger new event "filter" which will bubble up.
        componentResources.triggerEvent("filter", null, null);
        // We don't want "success" to bubble up, so we return true to say we've handled it.
        return true;
    }

    // Handle event "selected"

    boolean onSelected(Long personId) {
        // Return false, which means we haven't handled the event so bubble it up.
        // This method is here solely as documentation, because without this method the event would bubble up anyway.
        return false;
    }

    // Getters

    public GridDataSource getPersons() {
        return new PersonFilteredDataSource(personFinderService, partialName);
    }

    public boolean isAjax() {
        return request.isXHR();
    }

    public String getLinkCSSClass() {
        if (person != null && person.getId().equals(selectedPersonId)) {
            return "active";
        }
        else {
            return "";
        }
    }
}

PersonEditor.tml


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">
<!--  At runtime the page will supply the stylesheet. The link here is to enable preview. -->
<link rel="stylesheet" type="text/css" href="../../../css/examples/ajaxgracefulcomponentscrud.css"/>

<t:content>

    <t:if test="modeCreate">
        <h1>Create</h1>
        
        <form t:type="CustomForm" t:id="createForm" t:zone="^">
            <t:errors/>
    
            <table>
                <tr>
                    <th><t:label for="firstName"/>:</th>
                    <td><input t:type="TextField" t:id="firstName" value="person.firstName" t:validate="required, maxlength=10" size="10"/></td>
                    <td>(required)</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="firstName"/></td>
                </tr>
                <tr>
                    <th><t:label for="lastName"/>:</th>
                    <td><input t:type="TextField" t:id="lastName" t:clientid="clastname" value="person.lastName" t:validate="required, maxlength=10" size="10"/></td>
                    <td>(required)</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="lastName"/></td>
                </tr>
                <tr>
                    <th><t:label for="region"/>:</th>
                    <td><input t:type="Select" t:id="region" value="person.region" t:validate="required" t:blankOption="ALWAYS"/></td>
                    <td>(required)</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="region"/></td>
                </tr>
                <tr>
                    <th><t:label for="startDate"/>:</th>
                    <td><input t:type="DateField" t:id="startDate" t:clientid="cstartdate" value="person.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>
                    <td>(required, ${datePattern})</td>
                </tr>
                <tr class="err">
                    <th></th>
                    <td colspan="2"><t:CustomError for="startDate"/></td>
                </tr>
            </table>

            <div class="buttons">
                <a t:type="eventLink" t:event="cancelCreate" t:zone="^" href="#">Cancel</a>
                <input type="submit" value="Save"/>
            </div>
        </form>

    </t:if>

    <t:if test="modeReview">
        <h1>Review</h1>
        
        <t:if test="person">
            <div t:type="if" t:test="deleteMessage" class="error">
                ${deleteMessage}
            </div>

            <table>
                <tr>
                    <th>Id:</th>
                    <td>${person.id}</td>
                </tr>
                <tr>
                    <th>Version:</th>
                    <td>${person.version}</td>
                </tr>
                <tr>
                    <th>Name:</th>
                    <td>${person.firstName} ${person.lastName}</td>
                </tr>
                <tr>
                    <th>Region:</th>
                    <td>${personRegion}</td>
                </tr>
                <tr>
                    <th>Start Date:</th>
                    <td><t:output value="person.startDate" format="prop:dateFormat"/></td>
                </tr>
            </table>

            <div class="buttons">
                <a t:type="eventLink" t:event="toUpdate" t:context="person.id" t:zone="^" href="#">Update...</a>
                <a t:type="eventLink" t:event="delete" t:context="[person.id,person.version]" t:zone="^" href="#">
                    <!-- The Confirm mixin can't cancel an EventLink that specifies a Zone, so we put the Confirm inside the EventLink. -->
                    <!-- See http://tapestry-users.832.n2.nabble.com/Confirm-mixin-won-t-cancel-when-in-zone-td5048950.html#a5048950 -->
                    <span t:type="any" t:mixins="Confirm" t:message="Delete ${person.firstName} ${person.lastName}?">
                        Delete...
                    </span>
                </a>
            </div>

        </t:if>
        <t:if negate="true" test="person">
            Person ${personId} does not exist.<br/><br/>
        </t:if>
        
    </t:if>

    <t:if test="modeUpdate">
        <h1>Update</h1>
        
        <form t:type="form" t:id="updateForm" t:context="personId" t:zone="^">
            <t:errors/>
        
            <t:if test="person">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <t:hidden value="person.version"/>
        
                <table>
                    <tr>
                        <th><t:label for="updFirstName"/>:</th>
                        <td><input t:type="TextField" t:id="updFirstName" value="person.firstName" t:validate="required, maxlength=10" size="10"/></td>
                        <td>(required)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updFirstName"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updLastName"/>:</th>
                        <td><input t:type="TextField" t:id="updLastName" value="person.lastName" t:validate="required, maxlength=10" size="10"/></td>
                        <td>(required)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updLastName"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updRegion"/>:</th>
                        <td><input t:type="Select" t:id="updRegion" value="person.region" t:validate="required"/></td>
                        <td>(required)</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updRegion"/></td>
                    </tr>
                    <tr>
                        <th><t:label for="updStartDate"/>:</th>
                        <td><input t:type="DateField" t:id="updStartDate" value="person.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>
                        <td>(required, ${datePattern})</td>
                    </tr>
                    <tr class="err">
                        <th></th>
                        <td colspan="2"><t:CustomError for="updStartDate"/></td>
                    </tr>
                </table>

                <div class="buttons">
                    <a t:type="eventLink" t:event="cancelUpdate" t:context="personId" t:zone="^" href="#">Cancel</a>
                    <input t:type="submit" value="Save"/>
                </div>
            </t:if>
    
            <t:if negate="true" test="person">
                Person ${personId} does not exist.<br/><br/>
            </t:if>
                        
        </form>
        
    </t:if>

    <t:if test="modeConfirmDelete">
        <h1>Delete</h1>
        
        <form t:type="form" t:id="confirmDeleteForm" t:zone="^">
            <t:errors/>
        
            <t:if test="person">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <t:hidden value="person.version"/>
        

                <div class="buttons">
                    Delete ${person.firstName} ${person.lastName}?<br/><br/>
                    
                    <a t:type="eventLink" t:event="cancelConfirmDelete" t:context="person.id" t:zone="^" href="#">Cancel</a>
                    <input type="submit" value="Delete"/>
                </div>
            </t:if>
    
            <t:if negate="true" test="person">
                Person ${personId} does not exist.<br/><br/>
            </t:if>
                        
        </form>
        
    </t:if>

    <!-- This is needed to clear the zone. -->
    <t:if test="modeNull">
        <!-- The space character is needed only to make the zone update highlight visible. -->
        &nbsp;
    </t:if>
        
</t:content>
</html>

PersonEditor.properties


region-blankLabel=Choose...

## These enum conversions could be moved to the central message properties file called app.properties
Regions.EAST_COAST=East Coast
Regions.WEST_COAST=West Coast

updFirstName-label=First Name
updLastName-label=Last Name
updRegion-label=Region
updStartDate-label=Start Date

PersonEditor.java


package jumpstart.web.components.together.gracefulajaxcomponentscrud;

import java.text.Format;
import java.text.SimpleDateFormat;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;
import jumpstart.web.components.CustomForm;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#CANCEL_CREATE},
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#SUCCESSFUL_CREATE}(Long personId),
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#FAILED_CREATE},
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#TO_UPDATE}(Long personId),
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#CANCEL_UPDATE}(Long personId),
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#SUCCESSFUL_UPDATE}(Long personId),
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#FAILED_UPDATE}(Long personId),
 * personId), {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#SUCCESSFUL_DELETE}(Long
 * personId), {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#FAILED_DELETE}(Long
 * personId), {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#TO_CONFIRM_DELETE}(Long
 * personId), {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#CANCEL_CONFIRM_DELETE}
 * (Long personId),
 * {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#SUCCESSFUL_CONFIRM_DELETE}(Long
 * personId), {@link jumpstart.web.components.examples.ajax.gracefulcomponentscrud.PersonEditor#FAILED_CONFIRM_DELETE}
 * (Long personId)
 */
// @Events is applied to a component solely to document what events it may trigger. It is not checked at runtime.
@Events({ PersonEditor.CANCEL_CREATE, PersonEditor.SUCCESSFUL_CREATE, PersonEditor.FAILED_CREATE,
        PersonEditor.TO_UPDATE, PersonEditor.CANCEL_UPDATE, PersonEditor.SUCCESSFUL_UPDATE, PersonEditor.FAILED_UPDATE,
        PersonEditor.SUCCESSFUL_DELETE, PersonEditor.FAILED_DELETE, PersonEditor.TO_CONFIRM_DELETE,
        PersonEditor.CANCEL_CONFIRM_DELETE, PersonEditor.SUCCESSFUL_CONFIRM_DELETE, PersonEditor.FAILED_CONFIRM_DELETE })
public class PersonEditor {
    public static final String CANCEL_CREATE = "cancelCreate";
    public static final String SUCCESSFUL_CREATE = "successfulCreate";
    public static final String FAILED_CREATE = "failedCreate";
    public static final String TO_UPDATE = "toUpdate";
    public static final String CANCEL_UPDATE = "cancelUpdate";
    public static final String SUCCESSFUL_UPDATE = "successfulUpdate";
    public static final String FAILED_UPDATE = "failedUpdate";
    public static final String SUCCESSFUL_DELETE = "successfulDelete";
    public static final String FAILED_DELETE = "failedDelete";
    public static final String TO_CONFIRM_DELETE = "toConfirmDelete";
    public static final String CANCEL_CONFIRM_DELETE = "cancelConfirmDelete";
    public static final String SUCCESSFUL_CONFIRM_DELETE = "successfulConfirmDelete";
    public static final String FAILED_CONFIRM_DELETE = "failedConfirmDelete";

    private final String demoModeStr = System.getProperty("jumpstart.demo-mode");

    public enum Mode {
        CREATE, REVIEW, UPDATE, CONFIRM_DELETE;
    }

    // Parameters

    @Parameter(required = true)
    @Property
    private Mode mode;

    @Parameter(required = true)
    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String deleteMessage;

    // Work fields

    // This carries version through the redirect that follows a server-side validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @Component
    private CustomForm createForm;

    @Component
    private CustomForm updateForm;

    @Component
    private Form confirmDeleteForm;

    @Inject
    private ComponentResources componentResources;

    @Inject
    private Request request;

    @Inject
    private Messages messages;

    // The code

    // setupRender() is called by Tapestry right before it starts rendering the component.

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (personId == null) {
                person = null;
                // Handle null person in the template.
            }
            else {
                person = personFinderService.findPerson(personId);
                // Handle null person in the template.
            }
        }

    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelCreate"

    boolean onCancelCreate() {
        // Return false, which means we haven't handled the event so bubble it up.
        // This method is here solely as documentation, because without this method the event would bubble up anyway.
        return false;
    }

    // Component "createForm" bubbles up the PREPARE event when it is rendered or submitted

    void onPrepareFromCreateForm() throws Exception {
        // Instantiate a Person for the form data to overlay.
        person = new Person();
    }

    // Component "createForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromCreateForm() {

        if (createForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        if (demoModeStr != null && demoModeStr.equals("true")) {
            createForm.recordError("Sorry, but Create is not allowed in Demo mode.");
            return;
        }

        try {
            person = personManagerService.createPerson(person);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "createForm" bubbles up SUCCESS or FAILURE when it is submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromCreateForm() {
        // We want to tell our containing page explicitly what person we've created, so we trigger new event
        // "successfulCreate" with a parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(SUCCESSFUL_CREATE, new Object[] { person.getId() }, null);
        // We don't want "success" to bubble up, so we return true to say we've handled it.
        return true;
    }

    boolean onFailureFromCreateForm() {
        // Rather than letting "failure" bubble up which doesn't say what you were trying to do, we trigger new event
        // "failedCreate". It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(FAILED_CREATE, null, null);
        // We don't want "failure" to bubble up, so we return true to say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "toUpdate"

    boolean onToUpdate(Long personId) {
        // Return false, which means we haven't handled the event so bubble it up.
        // This method is here solely as documentation, because without this method the event would bubble up anyway.
        return false;
    }

    // Handle event "cancelUpdate"

    boolean onCancelUpdate(Long personId) {
        // Return false, which means we haven't handled the event so bubble it up.
        // This method is here solely as documentation, because without this method the event would bubble up anyway.
        return false;
    }

    // Component "updateForm" bubbles up the PREPARE_FOR_RENDER event during form render

    void onPrepareForRenderFromUpdateForm(Long personId) {
        this.personId = personId;

        if (request.isXHR()) {

            // If the form is valid then we're not redisplaying due to error, so get the person.

            if (updateForm.isValid()) {
                person = personFinderService.findPerson(this.personId);
                // Handle null person in the template.
            }

        }
        else {
            person = personFinderService.findPerson(personId);
            // Handle null person in the template.

            // If the form has errors then we're redisplaying after a redirect.
            // Form will restore your input values but it's up to us to restore Hidden values.

            if (updateForm.getHasErrors()) {
                if (person != null) {
                    person.setVersion(versionFlash);
                }
            }

        }

    }

    // Component "updateForm" bubbles up the PREPARE_FOR_SUBMIT event during form submission

    void onPrepareForSubmitFromUpdateForm(Long personId) {
        this.personId = personId;

        // Get objects for the form fields to overlay.
        person = personFinderService.findPerson(this.personId);

        if (person == null) {
            person = new Person();
            updateForm.recordError("Person has been deleted by another process.");
        }
    }

    // Component "updateForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromUpdateForm() {

        if (updateForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            personManagerService.changePerson(person);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }
    }

    // Component "updateForm" bubbles up SUCCESS or FAILURE when it is submitted, depending on whether VALIDATE
    // records an error

    boolean onSuccessFromUpdateForm() {
        // We want to tell our containing page explicitly what person we've updated, so we trigger new event
        // "successfulUpdate" with a parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(SUCCESSFUL_UPDATE, new Object[] { personId }, null);
        // We don't want "success" to bubble up, so we return true to say we've handled it.
        return true;
    }

    boolean onFailureFromUpdateForm() {
        if (!request.isXHR()) {
            versionFlash = person.getVersion();
        }

        // Rather than letting "failure" bubble up which doesn't say what you were trying to do, we trigger new event
        // "failedUpdate". It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(FAILED_UPDATE, new Object[] { personId }, null);
        // We don't want "failure" to bubble up, so we return true to say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // DELETE
    // /////////////////////////////////////////////////////////////////////

    // Handle event "delete"

    boolean onDelete(Long personId, Integer personVersion) {
        this.personId = personId;

        // If request is AJAX then the user has pressed Delete..., was presented with a Confirm dialog, and OK'd it.

        if (request.isXHR()) {
            boolean successfulDelete = false;

            if (demoModeStr != null && demoModeStr.equals("true")) {
                deleteMessage = "Sorry, but Delete is not allowed in Demo mode.";
            }
            else {

                try {
                    personManagerService.deletePerson(personId, personVersion);
                    successfulDelete = true;
                }
                catch (Exception e) {
                    // Display the cause. In a real system we would try harder to get a user-friendly message.
                    deleteMessage = ExceptionUtil.getRootCauseMessage(e);
                }

            }

            if (successfulDelete) {
                // Trigger new event "successfulDelete" (which in this example will bubble up to the page).
                componentResources.triggerEvent(SUCCESSFUL_DELETE, new Object[] { personId }, null);
            }
            else {
                // Trigger new event "failedDelete" (which in this example will bubble up to the page).
                componentResources.triggerEvent(FAILED_DELETE, new Object[] { personId }, null);
            }
        }

        // Else, (JavaScript disabled) user has pressed Delete..., but not yet confirmed so go to confirmation mode.

        else {
            // Trigger new event "toConfirmDelete" (which in this example will bubble up to the page).
            componentResources.triggerEvent(TO_CONFIRM_DELETE, new Object[] { personId }, null);
        }

        // We don't want "delete" to bubble up, so we return true to say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // CONFIRM DELETE - used only when JavaScript is disabled.
    // /////////////////////////////////////////////////////////////////////

    // Handle event "cancelConfirmDelete"

    boolean onCancelConfirmDelete(Long personId) {
        // Return false, which means we haven't handled the event so bubble it up.
        // This method is here solely as documentation, because without this method the event would bubble up anyway.
        return false;
    }

    // Component "confirmDeleteForm" bubbles up the PREPARE_FOR_RENDER event during form render

    void onPrepareForRenderFromConfirmDeleteForm() {
        person = personFinderService.findPerson(personId);
        // Handle null person in the template.

        // If the form has errors then we're redisplaying after a redirect.
        // Form will restore your input values but it's up to us to restore Hidden values.

        if (confirmDeleteForm.getHasErrors()) {
            if (person != null) {
                person.setVersion(versionFlash);
            }
        }
    }

    // Component "confirmDeleteForm" bubbles up the PREPARE_FOR_SUBMIT event during form submission

    void onPrepareForSubmitFromConfirmDeleteForm() {
        // Get objects for the form fields to overlay.
        person = personFinderService.findPerson(personId);

        if (person == null) {
            person = new Person();
            confirmDeleteForm.recordError("Person has already been deleted by another process.");
        }
    }

    // Component "confirmDeleteForm" bubbles up the VALIDATE event when it is submitted

    void onValidateFromConfirmDeleteForm() {

        if (confirmDeleteForm.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        if (demoModeStr != null && demoModeStr.equals("true")) {
            confirmDeleteForm.recordError("Sorry, but Delete is not allowed in Demo mode.");
        }
        else {

            try {
                personManagerService.deletePerson(personId, person.getVersion());
            }
            catch (Exception e) {
                // Display the cause. In a real system we would try harder to get a user-friendly message.
                confirmDeleteForm.recordError(ExceptionUtil.getRootCauseMessage(e));
            }

        }

    }

    // Component "confirmDeleteForm" bubbles up SUCCESS or FAILURE when it is submitted, depending on whether
    // VALIDATE records an error

    boolean onSuccessFromConfirmDeleteForm() {
        // We want to tell our containing page explicitly what person we've deleted, so we trigger new event
        // "successfulDelete" with a parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(SUCCESSFUL_CONFIRM_DELETE, new Object[] { person.getId() }, null);
        // We don't want "success" to bubble up, so we return true to say we've handled it.
        return true;
    }

    boolean onFailureFromConfirmDeleteForm() {
        versionFlash = person.getVersion();

        // Rather than letting "failure" bubble up which doesn't say what you were trying to do, we trigger new event
        // "failedDelete". It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(FAILED_CONFIRM_DELETE, new Object[] { person.getId() }, null);
        // We don't want "failure" to bubble up, so we return true to say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // OTHER
    // /////////////////////////////////////////////////////////////////////

    // Getters

    public boolean isModeCreate() {
        return mode == Mode.CREATE;
    }

    public boolean isModeReview() {
        return mode == Mode.REVIEW;
    }

    public boolean isModeUpdate() {
        return mode == Mode.UPDATE;
    }

    public boolean isModeConfirmDelete() {
        return mode == Mode.CONFIRM_DELETE;
    }

    public boolean isModeNull() {
        return mode == null;
    }

    public String getPersonRegion() {
        // Follow the same naming convention that the Select component uses
        return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());
    }

    public String getDatePattern() {
        return "dd/MM/yyyy";
    }

    public Format getDateFormat() {
        return new SimpleDateFormat(getDatePattern());
    }
}

gracefulajaxcomponentscrud.css


body, td        { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
                    line-height: 17px; }

.eg             { margin: 20px 0; padding: 20px; 
                    border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }

a               { text-decoration: none; color: #3D69B6; }
a:hover         { text-decoration: underline; }

.js-recommended     { color: red; display: block; margin-bottom: 14px; }

table       { border-collapse: collapse; border-spacing: 0; }
:root table { border-collapse: separate; } /* Firefox 3 */
th, td      { padding: 0; }
form        { margin-bottom: 0; } /* IE 7 */

#listAndEditor          { width: 800px; border: none; margin: 10px 0; }

#listCell               { width: 25%; border: 1px solid white; background-color: #eee; vertical-align: top; }

#personFilter           { width: 100%; padding: 5px 0 10px 0; text-align: center; vertical-align: middle; 
                            background-color: #3d69b6; color: white; font-weight: bold; border: 1px solid white; }

/* Add some padding around the list so that we can see the yellow flash when "Highlight zone updates" is on. */ 
#listZone               { padding: 4px; background-color: inherit; }
#listZone               { background-color: #eee; zoom: 1; } /* IE 7. zoom is to fix hasLayout bug. */

#personList             { height: 238px; position: relative; }
.personGrid             { width: 100%; font-family: Arial, Helvetica, sans-serif; }
.personGrid th          { display: none; }
.personGrid td          { border: thin solid white; background-color: #eee; }
.personGrid a           { width: 100%; line-height: 50px; display: block; text-align: center;
                            text-decoration: none; color: black; }
.personGrid a:visited   { color: inherit; }
.personGrid a:hover     { background: #ccc; color: #fff; }
.personGrid a.active    { background: #999; color: #fff; }
.personGrid span.current    { background-color: #3d69b6; }

#personList .t-data-grid-pager  /* Need line-height to work around IE7 hasLayout and missing margins bug */
                        { line-height: 24px; position: absolute; left: 2px; bottom: 0px; margin: 0;
                            font-family: Arial, Helvetica, sans-serif; }

#noPersons              { text-align: center; padding-top: 10px; }

#editorCell             { width: 75%; height: 100%; vertical-align: top; 
                             border: 1px solid white; background-color: #eee; padding: 20px; }
#editorZone             { background-color: inherit; /* For IE7: */ background-color: #eee; }
#editorCell table       { margin: auto; } 
#editorCell h1          { font-size: large; text-align: center; } 
#editorCell th          { padding: 2px 5px; text-align: right; }
#editorCell td          { padding: 2px 5px; text-align: left; }
#editorCell tr.err th   { padding: 0; }
#editorCell tr.err td   { padding: 0; }
#editorCell tr.err td.error-msg-c   { padding: 0 0 4px 7px; font-size: 11px; color: red; } 
#editorCell .buttons    { text-align: center; padding-top: 15px; } 

.error                  { color: red; text-align: center; padding-bottom: 13px; }

Confirm.java


// Based on http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained

package jumpstart.web.mixins;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

/**
 * A simple mixin for attaching a javascript confirmation box to the onclick event of any component that implements
 * ClientElement.
 * 
 * @author <a href="mailto:chris@thegodcode.net">Chris Lewis</a> Apr 18, 2008
 */
// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in. 
@Import(library = "Confirm.js")
public class Confirm {

    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
    private String message;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @InjectContainer
    private ClientElement clientElement;

    @AfterRender
    public void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of a Confirm, which it will do when the DOM has been
        // fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("message", message);
        javaScriptSupport.addInitializerCall("confirm", spec);
    }

}

Confirm.js


// Based on http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained

// A class that attaches a confirmation box (with logic)  to
// the 'onclick' event of any HTML element.
// @author Chris Lewis Apr 18, 2008 <chris@thegodcode.net>

Confirm = Class.create({
        
    initialize: function(elementId, message) {
        this.message = message;
        Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
    },
    
    doConfirm: function(e) {
        
        // Pop up a javascript Confirm Box (see http://www.w3schools.com/js/js_popup.asp)
        
        if (!confirm(this.message)) {
                e.stop();
        }
    }
        
})

// Extend the Tapestry.Initializer with a static method that instantiates a Confirm.

Tapestry.Initializer.confirm = function(spec) {
    new Confirm(spec.elementId, spec.message);
}

PersonFilteredDataSource.java


package jumpstart.web.model.together;

import java.util.List;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceRemote;

import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.SortConstraint;

public class PersonFilteredDataSource implements GridDataSource {
    private IPersonFinderServiceRemote personFinderService;
    private String partialName;

    private int startIndex;
    private List<Person> preparedResults;

    public PersonFilteredDataSource(IPersonFinderServiceRemote personFinderService, String partialName) {
        this.personFinderService = personFinderService;
        this.partialName = partialName;
    }

    @Override
    public int getAvailableRows() {
        return (int) personFinderService.countPersons(partialName);
    }

    @Override
    public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
        preparedResults = personFinderService.findPersons(partialName, startIndex, endIndex - startIndex + 1);
        this.startIndex = startIndex;
    }

    @Override
    public Object getRowValue(final int index) {
        return preparedResults.get(index - startIndex);
    }

    @Override
    public Class<Person> getRowType() {
        return Person.class;
    }

}

Person.java


package jumpstart.business.domain.person;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;


/**
 * The Person entity.
 */
@Entity
@SuppressWarnings("serial")
public class Person implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private Long id;

    @Version
    @Column(nullable = false)
    private Integer version;

    @Column(length = 10, nullable = false)
    private String firstName;

    @Column(length = 10, nullable = false)
    private String lastName;
    
    @Enumerated(EnumType.STRING)
    private Regions region;

    @Temporal(TemporalType.DATE)
    private Date startDate;

    public String toString() {
        final String DIVIDER = ", ";
        
        StringBuilder buf = new StringBuilder();
        buf.append(this.getClass().getSimpleName() + ": ");
        buf.append("[");
        buf.append("id=" + id + DIVIDER);
        buf.append("version=" + version + DIVIDER);
        buf.append("firstName=" + firstName + DIVIDER);
        buf.append("lastName=" + lastName + DIVIDER);
        buf.append("region=" + region + DIVIDER);
        buf.append("startDate=" + startDate);
        buf.append("]");
        return buf.toString();
    }

    // Default constructor is required by EJB3.
    public Person() {
    }

    public Person(String firstName, String lastName, Regions region, Date startDate) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
        this.region = region;
        this.startDate = startDate;
    }

    // The need for an equals() method is discussed at http://www.hibernate.org/109.html
    
    @Override
    public boolean equals(Object obj) {
        return (obj == this) || (obj instanceof Person) && id != null && id.equals(((Person) obj).getId());
    }

    // The need for a hashCode() method is discussed at http://www.hibernate.org/109.html

    @Override
    public int hashCode() {
        return id == null ? super.hashCode() : id.hashCode();
    }

    @PrePersist
    @PreUpdate
    public void validate() throws ValidationException {

        // Validate syntax...

        if ((firstName == null) || (firstName.trim().length() == 0)) {
            throw new ValidationException("First name is required.");
        }

        if ((lastName == null) || (lastName.trim().length() == 0)) {
            throw new ValidationException("Last name is required.");
        }

        if (region == null) {
            throw new ValidationException("Region is required.");
        }

        if (startDate == null) {
            throw new ValidationException("Start date is required.");
        }

    }

    public Long getId() {
        return id;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Regions getRegion() {
        return region;
    }

    public void setRegion(Regions region) {
        this.region = region;
    }

    public Date getStartDate() {
        return startDate;
    }

    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

}

Regions.java


package jumpstart.business.domain.person;

public enum Regions {
    EAST_COAST, WEST_COAST;
}