AjaxFormLoop (1)

An AjaxFormLoop used that gives full CRUD functionality (Create, Review, Update, and Delete).
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.
Id First Name Last Name Region Start Date Action
1 [Show] remove
3 [Show] remove
4 [Show] remove
2 [Show] remove
5 [Show] remove
Add a row

Refresh
Features: Flaws: 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.

AjaxFormLoop1.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 (1)</h1>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    An AjaxFormLoop used that gives full CRUD functionality (Create, Review, Update, and Delete).<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/>

    <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="persons" t:value="person" t:encoder="personEncoder">
                        <t:submitnotifier>
                            <td>${person.id}</td>
                            <td><input t:type="TextField" t:id="firstName" t:value="person.firstName" t:validate="required, maxlength=10" size="10"/></td>
                            <td><input t:type="TextField" t:id="lastName" t:value="person.lastName" t:validate="required, maxlength=10" size="10"/></td>
                            <td><input t:type="Select" t:id="region" value="person.region" t:validate="required"/></td>
                            <td><input t:type="DateField" t:id="startDate" t:value="person.startDate" t:format="prop:dateFormat" t:validate="required" size="10"/></td>

                            <!-- If optimistic locking is not needed then comment out this next line. -->
                            <t:hidden value="person.version"/>

                            <td><t:removerowlink>remove</t:removerowlink></td>
                        </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 t:type="submit" value="Save" t:disabled="disableSubmit"/>
            <a t:type="eventlink" t:event="refresh" t:zone="^" href="#" style="margin-left: 5px;">Refresh</a>
        </form>
    </div>

    Features:
    <ul>
    <li>Very similar technique to Editable Loop For Update, but this time all interaction with the server is via AJAX.</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, your update will be rejected by the business layer (optimistic locking exception).</li>
    <li>If another process deletes a person by the time you submit, we treat it as an error.</li>
    <li>On error, we redisplay the list with the same persons and values you submitted except if you have removed a row - see below.</li>
    </ul>

    Flaws:
    <ul>
    <li>It protects you from <a href="https://issues.apache.org/jira/browse/TAP5-1896">TAP5-1896</a> but it is a nuisance for the user:
        <ul>
        <li>The issue is: if you remove a row, submit, and the server-side fails due to error, the list will redisplay corrupted.</li>
        <li>To demonstrate, remove an existing row (not the last two), change any First Name to <em>${BAD_NAME}</em>, and press Save.</li>
        <li>In this example we protect you by disabling the submit button. The next example has a better solution.</li>
        </ul>
    </li>
    <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/AjaxFormLoop1.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormLoop1.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/iface/PersonDTO.java"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/commons/IdVersion.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/encoders/examples/IdVersionsEncoder.java"/>
    <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>

AjaxFormLoop1.java


package jumpstart.web.pages.examples.ajax;

import java.text.DateFormat;
import java.text.Format;
import java.util.ArrayList;
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.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.PersonDTO;
import jumpstart.util.ExceptionUtil;
import jumpstart.web.encoders.examples.IdVersionsEncoder;

import org.apache.tapestry5.PersistenceConstants;
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.Persist;
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.annotations.Inject;
import org.apache.tapestry5.services.Request;

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

    // Screen fields

    @Property
    private List<PersonDTO> persons;

    private PersonDTO person;

    @Property
    private final PersonDTOEncoder personEncoder = new PersonDTOEncoder();

    // A snapshot of the (id, version) of each person originally displayed before you added or removed any. 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";

    @Property
    @Persist(PersistenceConstants.FLASH)
    private boolean disableSubmit;

    // Work fields

    private List<Person> personsInDB;

    private boolean inFormSubmission;

    private List<PersonDTO> personsSubmitted;

    private List<PersonDTO> personsToCreate;

    private List<PersonDTO> personsToChange;

    private List<IdVersion> personsToDelete;

    // Other pages

    @InjectPage
    private AjaxFormLoop2 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 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 (personsSubmitted == 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.
            persons = new ArrayList<PersonDTO>();
            personsDisplayed = new ArrayList<IdVersion>();

            for (Person personInDB : personsInDB) {
                persons.add(new PersonDTO(personInDB.getId(), personInDB.getVersion(), personInDB.getFirstName(),
                        personInDB.getLastName(), personInDB.getRegion(), personInDB.getStartDate()));
                personsDisplayed.add(new IdVersion(personInDB.getId(), personInDB.getVersion()));
            }
        }

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

        else {
            persons = new ArrayList<PersonDTO>(personsSubmitted);
        }
    }

    PersonDTO onAddRow() {
        // Return a skeleton person DTO which AjaxFormLoop will overwrite.
        return new PersonDTO();
    }

    void onRemoveRow(PersonDTO person) {
        // Nothing to do.
    }

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

    void onPrepareForSubmit() {
        inFormSubmission = true;
        personsSubmitted = new ArrayList<PersonDTO>();
        personsDisplayed = new ArrayList<IdVersion>();

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

    void onValidateFromPersonsEdit() {

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

        personsToCreate = new ArrayList<PersonDTO>();
        personsToChange = new ArrayList<PersonDTO>();
        personsToDelete = new ArrayList<IdVersion>();

        // Error if any person submitted has a null id and non-null version - it means toValue(...) found they are no
        // longer in the database.

        for (PersonDTO personSubmitted : personsSubmitted) {
            if (personSubmitted.getId() == null && personSubmitted.getVersion() != null) {
                form.recordError("The list of persons is out of date. Please refresh and try again.");
                return;
            }
        }

        // Figure out which persons to delete, ie. see who you removed by comparing the submitted list to the displayed
        // list. It would not be correct to compare the submitted list to the database because we would end up deleting
        // persons that others have added since display.

        for (IdVersion personDisplayed : personsDisplayed) {
            boolean removed = true;
            for (PersonDTO personSubmitted : personsSubmitted) {
                if (personSubmitted.getId() != null && personSubmitted.getId().equals(personDisplayed.getId())) {
                    removed = false;
                    break;
                }
            }
            if (removed) {
                personsToDelete.add(personDisplayed);
            }
        }

        // Figure out which persons to create, ie. see which persons you have added. Treat the rest as persons to
        // change.

        for (PersonDTO personSubmitted : personsSubmitted) {
            if (personSubmitted.getId() == null) {
                personsToCreate.add(personSubmitted);
            }
            else {
                personsToChange.add(personSubmitted);
            }
        }

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

        for (PersonDTO personSubmitted : personsSubmitted) {
            if (personSubmitted.getFirstName() != null && personSubmitted.getFirstName().equals(BAD_NAME)) {
                form.recordError("First name cannot be " + BAD_NAME + ".");
                return;
            }
        }

        try {
            System.out.println(">>> personsToCreate = " + personsToCreate);
            System.out.println(">>> personsToChange = " + personsToChange);
            System.out.println(">>> personsToDelete = " + personsToDelete);
            // In a real application we would persist them to the database instead of printing them.
            // personManagerService.bulkEditPersonsByDTOs(personsToCreate, personsToChange, personsToDelete);
        }
        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, personsToChange, personsToDelete);
        return page2;
    }

    Object onFailure() {

        if (request.isXHR()) {
            if (personsToDelete != null && personsToDelete.size() > 0) {
                form.recordError("The submit button has been disabled to protect you from issue TAP5-1896.");
                disableSubmit = true;
            }
            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();
        disableSubmit = false;

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

    public PersonDTO getPerson() {
        return person;
    }

    public void setPerson(PersonDTO person) {
        this.person = person;

        if (inFormSubmission) {
            personsSubmitted.add(person);
        }
    }

    // This encoder is intended for use in a Loop or AjaxFormLoop:
    // - during render, to convert each person DTO to an id (AjaxFormLoop then stores the ids in the form, hidden).
    // - during form submission, to convert each id back to a person DTO. AjaxFormLoop will overwrite several fields of
    // the person DTO returned.

    private class PersonDTOEncoder implements ValueEncoder<PersonDTO> {

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

        @Override
        public PersonDTO toValue(String idAsString) {
            PersonDTO person = null;

            if (idAsString == null) {
                person = new PersonDTO();
            }
            else {
                Long id = new Long(idAsString);
                person = findPerson(id);

                // If person has since been deleted from the DB, create an empty person.
                if (person == null) {
                    person = new PersonDTO();
                }
            }

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

        private PersonDTO findPerson(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 personInDB : personsInDB) {
                    if (personInDB.getId().equals(id)) {
                        PersonDTO personDTO = new PersonDTO(personInDB.getId(), personInDB.getVersion(),
                                personInDB.getFirstName(), personInDB.getLastName(), personInDB.getRegion(),
                                personInDB.getStartDate());
                        return personDTO;
                    }
                }
            }

            // Else, find the person in the database.

            else {
                Person person = personFinderService.findPerson(id);
                if (person != null) {
                    PersonDTO personDTO = new PersonDTO(person.getId(), person.getVersion(), person.getFirstName(),
                            person.getLastName(), person.getRegion(), person.getStartDate());
                    return personDTO;
                }
            }

            return null;
        }

    }

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

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

PersonDTO.java


package jumpstart.business.domain.person.iface;

import java.util.Date;

import jumpstart.business.domain.person.Regions;

public class PersonDTO {
    private Long id;
    private Integer version;
    private String firstName;
    private String lastName;
    private Regions region;
    private Date startDate;
    private boolean delete;

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

    public PersonDTO() {
    }

    public PersonDTO(Long id) {
        this.id = id;
    }

    public PersonDTO(Long id, Integer version, String firstName, String lastName, Regions region, Date startDate) {
        super();
        this.id = id;
        this.version = version;
        this.firstName = firstName;
        this.lastName = lastName;
        this.region = region;
        this.startDate = startDate;
        this.delete = false;
    }

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

IdVersion.java


package jumpstart.business.commons;

import java.io.Serializable;

// A handy holder for an (id, version).

public class IdVersion implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private Integer version;

    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);
        buf.append("]");
        return buf.toString();
    }

    public IdVersion(Long id, Integer version) {
        super();
        this.id = id;
        this.version = version;
    }

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

}

IdVersionsEncoder.java


package jumpstart.web.encoders.examples;

import java.util.ArrayList;
import java.util.List;

import jumpstart.business.commons.IdVersion;

import org.apache.tapestry5.ValueEncoder;

// This encoder is intended for use storing a list of (id, version) in a single Hidden field:
// - during render, to convert a list of (id, version) to a String to be stored by the Hidden.
// - during form submission, to convert the String back to a list of (id, version) for use server-side.

public class IdVersionsEncoder implements ValueEncoder<List<IdVersion>> {
    private final String DELIMITER = ":"; 

    @Override
    public String toClient(List<IdVersion> idVersions) {
        String s = "";
        
        for (IdVersion idVersion : idVersions) {
            s += idVersion.getId() + DELIMITER + idVersion.getVersion() + DELIMITER;
        }
        
        return s;
    }

    @Override
    public List<IdVersion> toValue(String idVersionsAsString) {
        List<IdVersion> idVersions = new ArrayList<IdVersion>();

        String[] chunks = idVersionsAsString.split(DELIMITER);

        for (int i = 0; i < chunks.length; i = i + 2) {
            Long id = Long.parseLong(chunks[i]);
            Integer version = chunks[i + 1].equals("null") ? null : Integer.parseInt(chunks[i + 1]);
            idVersions.add(new IdVersion(id, version));
        }

        return idVersions;
    }

}

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