Total Control CRUD

This example is like Easy CRUD but shows how CRUD can be done "by hand", ie. without using Tapestry's Grid, BeanEditor, and BeanDisplay components.
For example, instead of using the Grid component we use the Loop and Output components in a normal HTML table.

Id First Name Last Name Region Start Date Actions
3 acme fg East Coast Feb 28, 2008 Review Update Delete
4 Donald J Grump West Coast Feb 27, 2008 Review Update Delete
2 Mary An East Coast Apr 28, 2017 Review Update Delete
5 Michael 0 Spoon1966 East Coast Mar 29, 2017 Review Update Delete
1 uuuu Prod West Coast Apr 4, 2017 Review Update Delete

The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.
The source for CustomForm and CustomError is shown in the No Validation Bubbles example.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "">
<!-- 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="">
    <link rel="stylesheet" type="text/css" href="${context:css/together/totalcontrolcrud.css}"/>
    <h1>Total Control CRUD</h1>
    This example is like <em>Easy CRUD</em> but shows how CRUD can be done "by hand", ie. without using Tapestry's Grid, BeanEditor, and BeanDisplay components.<br/>
    For example, instead of using the Grid component we use the Loop and Output components in a normal HTML table.

    <div class="eg">
        <a t:type="pageLink" t:page="together/totalcontrolcrud/person/PersonCreate" href="#">Create...</a><br/><br/>
        <t:if test="errorMessage">
            <br/><span style="color:red">${errorMessage}</span><br/><br/>
        <table class="grid">
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Start Date</th>
                <tr t:type="Loop" t:source="persons" t:value="person" class="">
                    <td><t:output value="person.startDate" format="dateFormat"/></td>
                        <a t:type="pageLink" t:page="together/totalcontrolcrud/person/PersonReview" t:context="" href="#">Review</a>
                        <a t:type="pageLink" t:page="together/totalcontrolcrud/person/PersonUpdate" t:context="" href="#">Update</a>
                        <a t:type="eventLink" t:event="Delete" t:context="[,person.version]" 
                            t:mixins="Confirm" t:message="Delete ${person.firstName} ${person.lastName}?">Delete</a>

    <a t:type="pageLink" t:page="Index" href="#">Home</a><br/><br/>
    The source for PersonFinderService, @EJB handling, etc. is shown in the @EJB example.<br/>
    The source for CustomForm and CustomError is shown in the No Validation Bubbles example.<br/><br/>

    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/totalcontrolcrud/Persons.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/totalcontrolcrud/"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/together/totalcontrolcrud/"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/together/totalcontrolcrud.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/Confirm.js"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/"/>

## These enum conversions could be moved to the central message properties file called
Regions.EAST_COAST=East Coast
Regions.WEST_COAST=West Coast

package jumpstart.web.pages.together.totalcontrolcrud;

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

import javax.ejb.EJB;

import jumpstart.util.ExceptionUtil;
import jumpstart.web.commons.EvenOdd;

import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;

public class Persons {

    private final String demoModeStr = System.getProperty("jumpstart.demo-mode");
    static private final int MAX_RESULTS = 30;

    // Screen fields

    private List<Person> persons;

    private Person person;

    private String errorMessage;

    private EvenOdd evenOdd;

    // Generally useful bits and pieces

    private IPersonFinderServiceLocal personFinderService;

    private IPersonManagerServiceLocal personManagerService;

    private Messages messages;

    private Locale currentLocale;

    // The code

    // setupRender() is called by Tapestry right before it starts rendering the page.

    void setupRender() {
        persons = personFinderService.findPersons(MAX_RESULTS);
        evenOdd = new EvenOdd();

    // Handle event "delete"

    void onDelete(Long id, Integer version) {
        if (demoModeStr != null && demoModeStr.equals("true")) {
            errorMessage = "Sorry, but this function is not allowed in Demo mode.";

        try {
            personManagerService.deletePerson(id, version);
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a user-friendly message.
            errorMessage = ExceptionUtil.getRootCauseMessage(e);

    public String getPersonRegion() {
        // Follow the same naming convention that the Select component uses
        return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());

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


body, td        { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
                    line-height: 17px; }

.eg             { margin: 20px 0; padding: 20px; 
                    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; }

.grid           { border-collapse: collapse; border-spacing: 0; border: 1px solid #dddddd; }
.grid tr        { background-color: white; }
.grid tr.odd    { background-color: #f8f8f8; }
.grid tr:hover  { background-color: #eeeeee; }
.grid th        { padding: 3px 5px; text-align: left; width: 130px; border: 1px solid #dddddd; 
                    font-weight: normal; background-color: #eeeeee; 
                    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; }

// Based on

package jumpstart.web.mixins;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;

 * A simple mixin for attaching a javascript confirmation box to the onclick event of any component that implements
 * ClientElement.
 * @author <a href="">Chris Lewis</a> Apr 18, 2008
// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in. 
@Import(library = "Confirm.js")
public class Confirm {

    @Parameter(name = "message", value = "Are you sure?", defaultPrefix = BindingConstants.LITERAL)
    private String message;

    private JavaScriptSupport javaScriptSupport;

    private ClientElement clientElement;

    public void afterRender() {

        // Tell the Tapestry.Initializer to do the initializing of a Confirm, which it will do when the DOM has been
        // fully loaded.

        JSONObject spec = new JSONObject();
        spec.put("elementId", clientElement.getClientId());
        spec.put("message", message);
        javaScriptSupport.addInitializerCall("confirm", spec);



// Based on

// A class that attaches a confirmation box (with logic)  to
// the 'onclick' event of any HTML element.
// @author Chris Lewis Apr 18, 2008 <>

Confirm = Class.create({
    initialize: function(elementId, message) {
        this.message = message;
        Event.observe($(elementId), 'click', this.doConfirm.bindAsEventListener(this));
    doConfirm: function(e) {
        // Pop up a javascript Confirm Box (see
        if (!confirm(this.message)) {

// Extend the Tapestry.Initializer with a static method that instantiates a Confirm.

Tapestry.Initializer.confirm = function(spec) {
    new Confirm(spec.elementId, spec.message);


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.
public class Person implements Serializable {

    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private Long id;

    @Column(nullable = false)
    private Integer version;

    @Column(length = 10, nullable = false)
    private String firstName;

    @Column(length = 10, nullable = false)
    private String lastName;
    private Regions region;

    private Date startDate;

    public String toString() {
        final String DIVIDER = ", ";
        StringBuilder buf = new StringBuilder();
        buf.append(this.getClass().getSimpleName() + ": ");
        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);
        return buf.toString();

    // Default constructor is required by EJB3.
    public Person() {

    public Person(String firstName, String lastName, Regions region, Date startDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.region = region;
        this.startDate = startDate;

    // The need for an equals() method is discussed at
    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

    public int hashCode() {
        return id == null ? super.hashCode() : id.hashCode();

    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;



public enum Regions {