Handling A Bad Context

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.

For example, look at the URL of this page. The activation context is clearly visible as 1 and it is easy to change. Try replacing it with 2 and you will see person 2. Users may become accustomed to this facility.

Here is the requested person:
Id
1
Version
10
First Name
5666
Last Name
123456
Region
East Coast
Start Date
Dec 8, 2007
In this example a bad context could occur several ways: Here are the alternatives:
  1. Handle it on the same page - either display the person or display the error.
    This approach has the big advantage that it keeps the same URL - the user can see what they requested.
    The "exception" event can help simplify this. See References below.
  2. Return a new page, possibly passing it the exception or a message to display.
    The page could even return HTTP 404 as described here.
  3. Throw an exception and let Tapestry's exception reporting page catch it.
    This is the simplest approach but probably not suitable for production. See the Exception Reporting Page example.
  4. Throw an exception and catch it with your own exception reporting page as described in the Exception Reporting Page example.
    The exception reporting page could give certain exceptions special treatment eg. DoesNotExistException and NotAuthorisedException might get different treatment to unexpected exceptions.
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.

EventContext.
To handle a variable number of context parameters, or even an unexpected number of context parameters, use EventContext. We use it in Easy Object Select example and the One Page CRUD example.

References: Intercepting Event Exceptions, Overriding Exception Reporting, EventContext. If, BeanDisplay.

Home

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

HandlingABadContext.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">
<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/apidocs/org/apache/tapestry5/corelib/components/If.html">If</a>, 
    <a href="http://tapestry.apache.org/5.3/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>

HandlingABadContext.java


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).
    }
}

examples.css


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 */

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

}