<!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
We use a "strict" DTD to make IE follow the alignment rules. -->
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd" xmlns:p="tapestry:parameter">
<head>
<link rel="stylesheet" type="text/css" href="${context:css/examples/ajaxformloop.css}"/>
</head>
<body>
<h1>AjaxFormLoop With Holders (1)</h1>
<noscript class="js-required">
${message:javascript_required}
</noscript>
This example of AjaxFormLoop operates directly on Person entities instead of DTOs and it works around <a href="https://issues.apache.org/jira/browse/TAP5-1896">TAP5-1896</a>.<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 differentiate between rows added, rows removed, and rows that others have deleted.<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>
<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" t: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"/>
<!-- If optimistic locking is not needed then comment out this next bit. -->
<t:hidden value="personHolder.version"/>
<t:hidden value="personHolder.person.version"/>
</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"/>
<a t:type="eventlink" t:event="refresh" t:zone="^" href="#" style="margin-left: 5px;">Refresh</a>
</form>
</div>
Features:
<ul>
<li>Similar technique to Editable Loop For Update, but this time all interaction with the server is via AJAX.</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, 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.</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.7/apidocs/org/apache/tapestry5/corelib/components/AjaxFormLoop.html">AjaxFormLoop</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/AddRowLink">AddRowLink</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/RemoveRowLink.html">RemoveRowLink</a>,
<a href="http://tapestry.apache.org/5.3.7/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.7/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.7/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/AjaxFormLoopWithHolders1.tml"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFormLoopWithHolders1.java"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/ajaxformloop.css"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/encoders/examples/IdVersionsEncoder.java"/>
<t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/commons/IdVersion.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>
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.annotations.Inject;
import org.apache.tapestry5.services.Request;
public class AjaxFormLoopWithHolders1 {
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;
private List<Person> personsToChange;
private List<IdVersion> personsToDelete;
// Other pages
@InjectPage
private AjaxFormLoopWithHolders2 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 (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>();
personsToChange = new ArrayList<Person>();
personsToDelete = new ArrayList<IdVersion>();
// Error if any original person has been deleted by someone else (ie. toValue(...) returned an empty person.
for (PersonHolder holder : personHoldersReconstructed) {
if (holder.getId() >= 0 && holder.getPerson().getId() == null) {
if (holder.isRemoved()) {
// You removed it and we can't tell if it was also deleted - no problem - the business layer will
// detect it.
}
else {
form.recordError("The list of persons is out of date. Please refresh and try again.");
return;
}
}
}
// Figure out which persons to create, change, and delete.
for (PersonHolder holder : personHoldersReconstructed) {
if (holder.getId() > 0) {
if (holder.isRemoved()) {
personsToDelete.add(new IdVersion(holder.getId(), holder.getVersion()));
}
else {
personsToChange.add(holder.getPerson());
}
}
else {
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);
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.bulkEditPersons(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()) {
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 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 Format getDateFormat() {
return DateFormat.getDateInstance(DateFormat.SHORT, currentLocale);
}
public String getHideIfRemoved() {
return personHolder.isRemoved() ? "display: none;" : "";
}
}
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; }
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;
}
}
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;
}
}
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;
}
}
}
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;
}
}
package jumpstart.business.domain.person;
public enum Regions {
EAST_COAST, WEST_COAST;
}