AjaxFormLoop Tailored (1)

An AjaxFormLoop tailored to handle new Persons only.
The changes are not persisted to the database until the Save button is pressed.

To demonstrate a server-side error, change any First Name to Acme.

It uses a "PersonHolder" so that it can easily keep track of rows added, and rows added then removed.
Id First Name Last Name Region Start Date Action
3 grgo Sprat East Coast 2/24/07
4 kkkkk Spill East Coast 10/24/14
2 Mary Contrary East Coast 2/29/08
1 test Dumptys West Coast 4/19/91
5 xzc gold East Coast 3/1/08
Add a row

Refresh
Features: Notes: References: AjaxFormLoop, AddRowLink, RemoveRowLink, ValueEncoder, Forms and Validation, Request, Ajax and Zones, Zone.

Home

The source for @EJB handling, etc. is shown in the @EJB example.

AjaxFormLoopTailored1.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/examples/ajaxformloop.css}"/>
</head>
<body>
    <h1>AjaxFormLoop Tailored (1)</h1>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    An AjaxFormLoop tailored to handle new Persons only.<br/>
    The changes are not persisted to the database until the Save button is pressed.<br/><br/>

    To demonstrate a server-side error, change any First Name to <em>${BAD_NAME}</em>.<br/><br/>

    It uses a "PersonHolder" so that it can easily keep track of rows added, and rows added then removed.<br/>
    
    <div class="eg" t:type="Zone" t:id="personsEditZone" id="personsEditZone">
        <form t:type="form" t:id="personsEdit" t:zone="^">
            <t:errors/>

            <t:hidden value="personsDisplayed" encoder="personsDisplayedEncoder"/>
            
            <table class="grid">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>First Name</th>
                        <th>Last Name</th>
                        <th>Region</th>
                        <th>Start Date</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    <tr t:type="AjaxFormLoop" t:source="personHolders" t:value="personHolder" t:encoder="personHolderEncoder">
                        <t:submitnotifier>
                            <t:if t:test="!newPerson">
                                <td>${personHolder.id}</td>
                                <td>${personHolder.person.firstName}</td>
                                <td>${personHolder.person.lastName}</td>
                                <td>${personRegion}</td>
                                <td><t:output value="personHolder.person.startDate" format="dateFormat"/></td>
                                <td></td>

                                <!-- We shadow the output-only id with a hidden field to enable redisplay of the list exactly as it was submitted. -->
                                <t:hidden value="personHolder.id"/>

                                <!-- If optimistic locking is not needed then comment out this next bit. In this example we don't use it. -->
                                <t:hidden value="personHolder.version"/>
                                <t:hidden value="personHolder.person.version"/>
                            </t:if>
                            <t:if t:test="newPerson">
                                <td style="${hideIfRemoved}">${personHolder.id}</td>
                                <td style="${hideIfRemoved}"><input t:type="TextField" t:id="firstName" t:value="personHolder.person.firstName" t:validate="required, maxlength=10" size="10"/></td>
                                <td style="${hideIfRemoved}"><input t:type="TextField" t:id="lastName" t:value="personHolder.person.lastName" t:validate="required, maxlength=10" size="10"/></td>
                                <td style="${hideIfRemoved}"><input t:type="Select" t:id="region" value="personHolder.person.region" t:validate="required"/></td>
                                <td style="${hideIfRemoved}"><input t:type="DateField" t:id="startDate" t:value="personHolder.person.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>
                                <td style="${hideIfRemoved}"><t:removerowlink>remove</t:removerowlink></td>

                                <t:hidden value="personHolder.removed"/>
                            </t:if>
                        </t:submitnotifier>
                        <p:addRow>
                            <td colspan="6" style="text-align: right">
                                <t:addrowlink>Add a row</t:addrowlink>
                            </td>
                        </p:addRow>
                    </tr>
                </tbody>
            </table><br/>
            <input type="submit" value="Save"/>
            <a t:type="eventlink" t:event="refresh" t:zone="^" href="#" style="margin-left: 5px;">Refresh</a>
         </form>
    </div>

    Features:
    <ul>
    <li>Same technique as AjaxFormLoop With Holders.</li>
    <li>Avoids <a href="https://issues.apache.org/jira/browse/TAP5-1896">TAP5-1896</a> which corrupts the list on redisplay because, it seems, 
        Form's Validation Tracker is unaware that rows have been removed.
        <ul>
        <li>We work around this by reconstructing the original list, plus additions, on submit, flagging removed rows, and hiding them on redisplay.</li>
        </ul>
    </li> 
    <li>No need for FLASH persistence because there are no redirects - instead, responses are AJAX-delivered Zone updates.</li>
    <li>If another process creates a person by the time you submit, we ignore it. The encoder ensures we target the submitted persons only.</li>
    <li>If another process updates a person by the time you submit, we ignore it.</li>
    <li>If another process deletes a person by the time you submit, we ignore it.</li>
    <li>On error, we redisplay the list with the same persons and values you submitted.</li>
    </ul>

    Notes:
    <ul>
    <li>There have been reports of "Add a row" failing following inactivity (see <a href="https://issues.apache.org/jira/browse/TAP5-733">TAP5-733</a>), 
    and in another circumstance that might involve cookies (discussed
    <a href="http://tapestry.1045711.n5.nabble.com/Is-AjaxFormLoop-example-working-for-you-td2422439.html">here</a>).</li>
    <li>During development: if you modify the template, the page sometimes fails with a type coercion problem. 
        The problem should clear if you use the browser's reload button once or twice.</li> 
    </ul>

    References: 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/AjaxFormLoop.html">AjaxFormLoop</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/AddRowLink">AddRowLink</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/RemoveRowLink.html">RemoveRowLink</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/ValueEncoder.html">ValueEncoder</a>, 
    <a href="http://tapestry.apache.org/forms-and-validation.html">Forms and Validation</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/services/Request.html">Request</a>, 
    <a href="http://tapestry.apache.org/ajax-and-zones.html">Ajax and Zones</a>, 
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/components/Zone.html">Zone</a>.<br/><br/>

    <a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
    
    The source for @EJB handling, etc. is shown in the @EJB example.<br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormLoopTailored1.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormLoopTailored1.properties"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormLoopTailored1.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/ajaxformloop.css"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/PersonFinderService.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>

AjaxFormLoopTailored1.properties


## 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

AjaxFormLoopTailored1.java


package jumpstart.web.pages.examples.ajax;

import java.text.DateFormat;
import java.text.Format;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import javax.ejb.EJB;

import jumpstart.business.commons.IdVersion;
import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.util.ExceptionUtil;
import jumpstart.web.encoders.examples.IdVersionsEncoder;

import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;

public class AjaxFormLoopTailored1 {
    static private final int MAX_RESULTS = 30;

    // Screen fields

    @Property
    private List<PersonHolder> personHolders;

    private PersonHolder personHolder;

    @Property
    private final PersonHolderEncoder personHolderEncoder = new PersonHolderEncoder();

    // A snapshot of the (id, version) of each person displayed. On submit, we use it to determine which persons you
    // removed.
    @Property
    private List<IdVersion> personsDisplayed;

    @Property
    private final IdVersionsEncoder personsDisplayedEncoder = new IdVersionsEncoder();

    @Property
    private final String BAD_NAME = "Acme";

    // Work fields

    private List<Person> personsInDB;

    private boolean inFormSubmission;

    private List<PersonHolder> personHoldersSubmitted;

    private List<PersonHolder> personHoldersReconstructed;

    private List<Person> personsToCreate;

    // Other pages

    @InjectPage
    private AjaxFormLoopTailored2 page2;

    // Generally useful bits and pieces

    @InjectComponent
    private Zone personsEditZone;

    @Component(id = "personsEdit")
    private Form form;

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @Inject
    private Locale currentLocale;

    @Inject
    private Messages messages;

    @Inject
    private Request request;

    // The code

    void onActivate() {
        inFormSubmission = false;
    }

    // Form bubbles up the PREPARE_FOR_RENDER event during form render.

    void onPrepareForRender() {
        inFormSubmission = false;

        // If the page had errors and the user chose to reload it, then we detect that and clear the errors.

        if (personHoldersReconstructed == null) {
            form.clearErrors();
        }

        // If fresh start, populate screen with all persons from the database

        if (form.isValid()) {
            // Get all persons - ask business service to find them (from the database)
            personsInDB = personFinderService.findPersons(MAX_RESULTS);

            // Populate the persons to be edited, and also a snapshot of who is in that list.
            personHolders = new ArrayList<PersonHolder>();

            for (Person personInDB : personsInDB) {
                personHolders.add(new PersonHolder(personInDB.getId(), personInDB.getVersion(), personInDB));
            }
        }

        // Else, we're rendering after a redirect, so populate the screen from the reconstructed values

        else {
            personHolders = new ArrayList<PersonHolder>(personHoldersReconstructed);
        }

        personsDisplayed = new ArrayList<IdVersion>();

        for (PersonHolder personHolder : personHolders) {
            if (personHolder.getId() != null) {
                personsDisplayed.add(new IdVersion(personHolder.getId(), personHolder.getVersion()));
            }
        }
    }

    PersonHolder onAddRow() {
        // Return a skeleton person holder which AjaxFormLoop will overwrite.
        // We need a unique id so that if you remove it later we identify which one you removed and flag it removed.
        // We use a negative id so it doesn't clash with the id of an existing person.
        return new PersonHolder(0 - System.nanoTime(), null, new Person());
    }

    void onRemoveRow(PersonHolder personHolder) {
        // Nothing to do.
    }

    // Form bubbles up the PREPARE_FOR_SUBMIT event during form submission.

    void onPrepareForSubmit() {
        inFormSubmission = true;
        personHoldersSubmitted = new ArrayList<PersonHolder>();
        personsDisplayed = new ArrayList<IdVersion>();

        // Get all persons - ask business service to find them (from the database)
        personsInDB = personFinderService.findPersons(MAX_RESULTS);
    }

    void onValidateFromPersonsEdit() {

        // Reconstruct the displayed list of persons, creating dummies in place of the removed ones.

        personHoldersReconstructed = new ArrayList<PersonHolder>();

        for (IdVersion personDisplayed : personsDisplayed) {
            boolean missing = true;

            for (PersonHolder personHolderSubmitted : personHoldersSubmitted) {
                if (personHolderSubmitted.getId().equals(personDisplayed.getId())) {
                    personHoldersReconstructed.add(personHolderSubmitted);
                    missing = false;
                    break;
                }
            }

            if (missing) {
                // Create a dummy person that won't fail field-level validation if we have to redisplay the list.
                Person dummy = new Person("dummy", "dummy", Regions.EAST_COAST, new Date());
                // Add it the list, with "removed" flagged
                PersonHolder ph = new PersonHolder(personDisplayed.getId(), personDisplayed.getVersion(), dummy, true);
                personHoldersReconstructed.add(ph);
            }
        }

        // Add in the newly-added persons

        for (PersonHolder personHolderSubmitted : personHoldersSubmitted) {
            if (personHolderSubmitted.getId() < 0) {
                boolean missing = true;

                for (IdVersion personDisplayed : personsDisplayed) {
                    if (personDisplayed.getId().equals(personHolderSubmitted.getId())) {
                        missing = false;
                        break;
                    }
                }

                if (missing) {
                    personHoldersReconstructed.add(personHolderSubmitted);
                }
            }
        }

        // If any server-side validation errors detected so far, return.

        if (form.getHasErrors()) {
            return;
        }

        personsToCreate = new ArrayList<Person>();

        // Figure out which persons to create.

        for (PersonHolder holder : personHoldersReconstructed) {
            if (holder.getId() < 0) {
                if (!holder.isRemoved()) {
                    personsToCreate.add(holder.getPerson());
                }
            }
        }

        // Simulate a server-side validation error: return error if anyone's first name is BAD_NAME.

        for (PersonHolder holder : personHoldersReconstructed) {
            if (holder.getPerson().getFirstName() != null && holder.getPerson().getFirstName().equals(BAD_NAME)) {
                form.recordError("First name cannot be " + BAD_NAME + ".");
                return;
            }
        }

        try {
            System.out.println(">>> personsToCreate = " + personsToCreate);
            // In a real application we would persist them to the database instead of printing them.
            // personManagerService.createPersons(personsToCreate);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            form.recordError(ExceptionUtil.getRootCauseMessage(e));
            return;
        }

    }

    Object onSuccess() {
        page2.set(personsToCreate);
        return page2;
    }

    Object onFailure() {

        if (request.isXHR()) {
            return personsEditZone.getBody();
        }
        else {
            // Not an AJAX request, so don't bother. Just refresh the screen and it will display "JavaScript required".
            return onRefresh();
        }
    }

    Object onRefresh() {
        form.clearErrors();

        return request.isXHR() ? personsEditZone.getBody() : null;
    }

    // The AjaxFormLoop component will automatically call this for every row as it is rendered.

    public PersonHolder getPersonHolder() {
        return personHolder;
    }

    // The AjaxFormLoop component will automatically call this for every row on submit.

    public void setPersonHolder(PersonHolder personHolder) {
        this.personHolder = personHolder;

        if (inFormSubmission) {
            personHoldersSubmitted.add(personHolder);
        }
    }

    // We use PersonHolder to hold the person and any extra info we need. Its id field allows us to distinguish which persons 
    // you have added and/or removed, and which persons we started with even if they have been deleted from the database by others.

    public class PersonHolder {
        private Long id;
        private Integer version;
        private Person person;
        private boolean removed;

        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("person=" + person + DIVIDER);
            buf.append("removed=" + removed);
            buf.append("]");
            return buf.toString();
        }

        PersonHolder(Long id, Integer version, Person person) {
            this(id, version, person, false);
        }

        PersonHolder(Long id, Integer version, Person person, boolean removed) {
            if (person == null) {
                throw new IllegalArgumentException(id + ", " + person);
            }

            this.id = id;
            this.version = version;
            this.person = person;
            this.removed = removed;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public Integer getVersion() {
            return version;
        }

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

        public Person getPerson() {
            return person;
        }

        public boolean isRemoved() {
            return removed;
        }

        public void setRemoved(boolean removed) {
            this.removed = removed;
        }
    }

    // This encoder is used by our AjaxFormLoop:
    // - during render, to convert each person holder to an id (AjaxFormLoop then stores the ids in the form, hidden).
    // - during form submission, to convert each id back to a person holder which it puts in our personHolder field.
    // AjaxFormLoop will overwrite several fields of the person returned.

    private class PersonHolderEncoder implements ValueEncoder<PersonHolder> {

        @Override
        public String toClient(PersonHolder personHolder) {
            Long id = personHolder.getId();
            return id == null ? null : id.toString();
        }

        @Override
        public PersonHolder toValue(String idAsString) {
            PersonHolder holder = null;
            Long id = new Long(idAsString);

            if (id < 0) {
                holder = new PersonHolder(id, null, new Person());
            }
            else {
                holder = findPersonHolder(id);

                // If person has since been deleted from the DB. Create a skeleton person holder.
                if (holder == null) {
                    holder = new PersonHolder(id, null, new Person());
                }
            }

            // AjaxFormLoop will overwrite several fields of the person holder returned.
            return holder;
        }

        private PersonHolder findPersonHolder(Long id) {

            // If in submit, we could find the person in the database but it's cheaper to search the list we got in
            // onPrepareForSubmit().

            if (inFormSubmission) {
                for (Person person : personsInDB) {
                    if (person.getId().equals(id)) {
                        return new PersonHolder(id, person.getVersion(), person);
                    }
                }
            }

            // Else, find the person in the database.

            else {
                Person person = personFinderService.findPerson(id);
                if (person != null) {
                    return new PersonHolder(id, person.getVersion(), person);
                }
            }

            return null;
        }

    }

    public boolean isNewPerson() {
        return personHolder.getId() < 0;
    }

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

    public Format getDateFormat() {
        return DateFormat.getDateInstance(DateFormat.SHORT, currentLocale);
    }

    public String getHideIfRemoved() {
        return personHolder.isRemoved() ? "display: none;" : "";
    }

}

ajaxformloop.css


body, td        { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
                    line-height: 17px; }
h1              { font-size: 26px; line-height: 20px; } /* For IE 7 */

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

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

.grid           { border-collapse: collapse; border-spacing: 0; border: 1px solid #ddd; font-size: inherit; }
.grid th        { padding: 3px 5px; text-align: left; width: 130px; border: 1px solid #ddd; 
                    font-weight: normal; background-color: #eee; 
                    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbfbfb', endColorstr='#e4e4e4'); /* for IE */
                    background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#e4e4e4)); /* for webkit browsers */
                    background: -moz-linear-gradient(top, #fbfbfb, #e4e4e4); /* for firefox 3.6+ */ }
.grid td        { padding: 3px 5px; text-align:left; }

.nodata         { margin-top: 5px; margin-left:20px; }

PersonFinderService.java


package jumpstart.business.domain.person;

import java.util.Arrays;
import java.util.List;

import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonFinderServiceRemote;
import jumpstart.util.query.SortCriterion;

@Stateless
@Local(IPersonFinderServiceLocal.class)
@Remote(IPersonFinderServiceRemote.class)
public class PersonFinderService implements IPersonFinderServiceLocal, IPersonFinderServiceRemote {

    @PersistenceContext(unitName = "jumpstart")
    private EntityManager em;

    public Person findPerson(Long id) {
        return em.find(Person.class, id);
    }

    public long countPersons() {
        return (Long) em.createQuery("select count(p) from Person p").getSingleResult();
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersons(int maxResults) {
        return em.createQuery("select p from Person p order by lower(p.firstName), lower(p.lastName)")
                .setMaxResults(maxResults).getResultList();
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersons(String partialName, int maxResults) {
        String searchName = partialName == null ? "" : partialName.toLowerCase();

        StringBuilder buf = new StringBuilder();
        buf.append("select p from Person p");
        buf.append(" where lower(firstName) like :firstName");
        buf.append(" or lower(lastName) like :lastName");
        buf.append(" order by lower(p.firstName), lower(p.lastName)");

        Query q = em.createQuery(buf.toString());
        q.setParameter("firstName", "%" + searchName + "%");
        q.setParameter("lastName", "%" + searchName + "%");

        List<Person> l = q.setMaxResults(maxResults).getResultList();
        return l;
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersonsByFirstName(String firstName) {
        String searchName = firstName == null ? "" : firstName.trim().toLowerCase();

        StringBuilder buf = new StringBuilder();
        buf.append("select p from Person p");
        buf.append(" where lower(p.firstName) = :searchName");
        buf.append(" order by lower(p.firstName), lower(p.lastName)");

        Query q = em.createQuery(buf.toString());
        q.setParameter("searchName", searchName);

        List<Person> l = q.getResultList();
        return l;
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersonsByLastName(String lastName) {
        String searchName = lastName == null ? "" : lastName.trim().toLowerCase();

        StringBuilder buf = new StringBuilder();
        buf.append("select p from Person p");
        buf.append(" where lower(p.lastName) = :searchName");
        buf.append(" order by lower(p.lastName), lower(p.firstName)");

        Query q = em.createQuery(buf.toString());
        q.setParameter("searchName", searchName);

        List<Person> l = q.getResultList();
        return l;
    }

    public long countPersons(String partialName) {
        return (Long) findPersons(true, partialName, 0, 0);
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersons(String partialName, int startIndex, int maxResults) {
        return (List<Person>) findPersons(false, partialName, startIndex, maxResults);
    }

    @SuppressWarnings("unchecked")
    private Object findPersons(boolean counting, String partialName, int startIndex, int maxResults) {
        String searchName = partialName == null ? "" : partialName.toLowerCase();

        StringBuilder buf = new StringBuilder();

        if (counting) {
            buf.append("select count(p) from Person p");
        }
        else {
            buf.append("select p from Person p");
        }
        buf.append(" where lower(firstName) like :firstName");
        buf.append(" or lower(lastName) like :lastName");

        if (!counting) {
            buf.append(" order by lower(p.firstName), lower(p.lastName)");
        }

        Query q = em.createQuery(buf.toString());
        q.setParameter("firstName", "%" + searchName + "%");
        q.setParameter("lastName", "%" + searchName + "%");

        if (counting) {
            Long qty = (Long) q.getSingleResult();
            return qty;
        }
        else {
            List<Person> l = q.setFirstResult(startIndex).setMaxResults(maxResults).getResultList();
            return l;
        }
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersons(int startIndex, int maxResults, List<SortCriterion> sortCriteria) {
        final List<String> PROPERTIES_TO_LOWER_FOR_SORT = Arrays.asList("firstName", "lastName");

        // Here we use JPQL. An alternative is to use javax.persistence.criteria.CriteriaQuery. For an example see
        // Tapestry's JpaGridDataSource.

        StringBuilder buf = new StringBuilder();
        buf.append("select p from Person p");
        buf.append(" order by ");

        boolean firstOrderByItem = true;
        boolean orderByIncludesId = false;

        for (SortCriterion sortCriterion : sortCriteria) {
            String propertyName = sortCriterion.getPropertyName();

            // Append an "order by" item, eg. "startDate", or ", lower(firstName) desc".

            if (!firstOrderByItem) {
                buf.append(", ");
            }
            if (PROPERTIES_TO_LOWER_FOR_SORT.contains(propertyName)) {
                buf.append("lower(").append(propertyName).append(")");
            }
            else {
                buf.append(propertyName);
            }
            buf.append(sortCriterion.getSortDirection().toStringForJpql());

            // We need to know later whether the "order by" includes id.

            if (propertyName.equals("id")) {
                orderByIncludesId = true;
            }
            firstOrderByItem = false;
        }

        // Ensure sequence is predictable by ensuring a unique property, id, is in the "order by".

        if (!orderByIncludesId) {
            if (!firstOrderByItem) {
                buf.append(", ");
            }
            buf.append("id");
        }

        Query q = em.createQuery(buf.toString());

        List<Person> l = q.setFirstResult(startIndex).setMaxResults(maxResults).getResultList();
        return l;
    }

    public long countPersons(String firstNameStartsWith, String lastNameStartsWith, Regions region) {
        return (Long) findPersons(true, firstNameStartsWith, lastNameStartsWith, region, 0, 0, null);
    }

    @SuppressWarnings("unchecked")
    public List<Person> findPersons(String firstNameStartsWith, String lastNameStartsWith, Regions region,
            int startIndex, int maxResults, List<SortCriterion> sortCriteria) {
        return (List<Person>) findPersons(false, firstNameStartsWith, lastNameStartsWith, region, startIndex,
                maxResults, sortCriteria);
    }

    @SuppressWarnings("unchecked")
    private Object findPersons(boolean counting, String firstNameStartsWith, String lastNameStartsWith, Regions region,
            int startIndex, int maxResults, List<SortCriterion> sortCriteria) {
        final List<String> PROPERTIES_TO_LOWER_FOR_SORT = Arrays.asList("firstName", "lastName");

        String searchFirstName = firstNameStartsWith == null ? "" : firstNameStartsWith.toLowerCase();
        String searchLastName = lastNameStartsWith == null ? "" : lastNameStartsWith.toLowerCase();

        StringBuilder buf = new StringBuilder();

        if (counting) {
            buf.append("select count(p) from Person p");
        }
        else {
            buf.append("select p from Person p");
        }

        buf.append(" where lower(firstName) like :firstName");
        buf.append(" and lower(lastName) like :lastName");
        if (region != null) {
            buf.append(" and region = :region");
        }

        if (!counting) {
            buf.append(" order by ");

            boolean firstOrderByItem = true;
            boolean orderByIncludesId = false;

            for (SortCriterion sortCriterion : sortCriteria) {
                String propertyName = sortCriterion.getPropertyName();

                // Append an "order by" item, eg. "startDate", or ", lower(firstName) desc".

                if (!firstOrderByItem) {
                    buf.append(", ");
                }
                if (PROPERTIES_TO_LOWER_FOR_SORT.contains(propertyName)) {
                    buf.append("lower(").append(propertyName).append(")");
                }
                else {
                    buf.append(propertyName);
                }
                buf.append(sortCriterion.getSortDirection().toStringForJpql());

                // We need to know later whether the "order by" includes id.

                if (propertyName.equals("id")) {
                    orderByIncludesId = true;
                }
                firstOrderByItem = false;
            }

            // Ensure sequence is predictable by ensuring a unique property, id, is in the "order by".

            if (!orderByIncludesId) {
                if (!firstOrderByItem) {
                    buf.append(", ");
                }
                buf.append("id");
            }
        }

        Query q = em.createQuery(buf.toString());
        q.setParameter("firstName", searchFirstName + "%");
        q.setParameter("lastName", searchLastName + "%");
        if (region != null) {
            q.setParameter("region", region);
        }

        if (counting) {
            Long qty = (Long) q.getSingleResult();
            return qty;
        }
        else {
            List<Person> l = q.setFirstResult(startIndex).setMaxResults(maxResults).getResultList();
            return l;
        }
    }

}

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;
}