<!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_3.xsd">
<head>
<link rel="stylesheet" type="text/css" href="${context:css/examples/examples.css}"/>
</head>
<body>
<h1>Handling A Bad Context</h1>
With each page you design it's important to decide how to handle a bad activation context, because
a bad context can occur in various ways.<br/><br/>
For example, look at the URL of this page. The activation context is clearly visible as <em>1</em> and
it is easy to change. Try replacing it with <em>2</em> and you will see person 2. Users may become
accustomed to this facility.<br/><br/>
Here is the requested person:
<div class="eg">
<t:if test="person">
<t:beandisplay object="person"/>
</t:if>
<t:if negate="true" test="person">
<strong>Person ${personId} does not exist.</strong>
</t:if>
</div>
In this example a bad context could occur several ways:
<ul>
<li>You've removed the context from the URL.</li>
<li>You've chosen a person that does not exist, eg. <em>100</em>.</li>
<li>You are not authorised to the person (in JumpStart this is not checked).</li>
<li>The format of the context is incorrect, eg. <em>abc</em>.</li>
<li>You bookmark the page but when you return to it later the context is no longer valid
because data, authorization, or the application have changed.</li>
</ul>
Here are the alternatives:
<ol>
<li><strong>Handle it on the same page</strong> - either display the person or display the error.<br/>
This approach has the big advantage that it keeps the same URL - the user can see what they requested.<br/>
The <em>"exception"</em> event can help simplify this. See References below.</li>
<li><strong>Return a new page</strong>, possibly passing it the exception or a message to display.<br/>
The page could even return <a href="http://en.wikipedia.org/wiki/404_error">HTTP 404</a> as described
<a href="http://news.gmane.org/find-root.php?message_id=%3c48A4290A.7010407%40fsadev.com%3e">here</a>.</li>
<li>Throw an exception and let <strong>Tapestry's exception reporting page</strong> catch it. <br/>
This is the simplest approach but probably not suitable for production. See the Exception Reporting Page example.</li>
<li>Throw an exception and catch it with <strong>your own exception reporting page</strong> as described in the Exception Reporting Page example.<br/>
The exception reporting page could give certain exceptions special treatment eg. DoesNotExistException and NotAuthorisedException might
get different treatment to unexpected exceptions.</li>
</ol>
This page has been built to handle only one situation: person does not exist.
All other problems will be caught by the exception reporting page.<br/><br/>
<strong>EventContext</strong>. <br/>
To handle a variable number of context parameters, or even an unexpected number of context parameters, use
<a href="http://tapestry.apache.org/component-events.html#ComponentEvents-EventContext">EventContext</a>.
We use it in Easy Object Select example and the One Page CRUD example.<br/><br/>
References:
<a href="http://tapestry.apache.org/component-events.html#ComponentEvents-InterceptingEventExceptions">Intercepting Event Exceptions</a>,
<a href="http://tapestry.apache.org/overriding-exception-reporting.html">Overriding Exception Reporting</a>,
<a href="http://tapestry.apache.org/component-events.html#ComponentEvents-EventContext">EventContext</a>.
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/If.html">If</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/BeanDisplay.html">BeanDisplay</a>.<br/><br/>
<a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
The source for Person, @EJB handling, etc. is shown in the @EJB example.<br/><br/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/infrastructure/HandlingABadContext.tml"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/infrastructure/HandlingABadContext.java"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/examples.css"/>
<t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/PersonFinderService.java"/>
</body>
</html>
package jumpstart.web.pages.examples.infrastructure;
import javax.ejb.EJB;
import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import org.apache.tapestry5.annotations.Property;
public class HandlingABadContext {
// The activation context
@Property
private Long personId;
// Screen fields
@Property
private Person person;
// Generally useful bits and pieces
@EJB
private IPersonFinderServiceLocal personFinderService;
// The code
Long onPassivate() {
return personId;
}
void onActivate(Long personId) {
this.personId = personId;
}
void setupRender() {
// Get person - ask business service to find it (from the database)
person = personFinderService.findPerson(personId);
// Handle null person in the template (with an If component).
}
}
body { 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 */
form { margin: 0; }
.eg { margin: 20px 0; padding: 20px; color: #888;
border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }
a { text-decoration: none; color: #3D69B6; }
a:hover { text-decoration: underline; }
/* For BeanDisplay */
.eg dl { margin: 0; color: #333; }
.eg dl.t-beandisplay dd.id { display: inline; margin-left: 0px; } /* IE 7 hack */
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;
}
}
}