Components CRUD

This example is based on the Filter CRUD example, replacing the list and the editor with two custom components: PersonList and PersonEdit.
Create...

Review

Id
1
Version
2
Name
Hfwe Dumpty
Region
West Coast
Start Date
05/12/2007
Like all good components, PersonList and PersonEdit do not know about each other. They handle their own events, then bubble them up to their container which is, in this case, the page. The container is responsible for coordinating its components. For example, when you click on a person in the list, PersonList bubbles the SELECTED event up to the page, and the page sets editorMode = REVIEW, and editorPersonId = the selected id, so that on redisplay the PersonEdit will show that person for review.

A component can include data within an event that it bubbles up, which is what PersonList and PersonEdit do, but take note that a component can also return data in its parameters - the container can read the data because component parameters are bi-directional.

Splitting the page into custom components like PersonList and PersonEdit is very appealing because it provides a clear and very natural separation of concerns, but just be aware that it doesn't come for free: you have to put in a bit of extra effort to get the event handling right.

References: ComponentResourcesCommon, @ActivationRequestParameter.

Home

The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.


<!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_4.xsd" xmlns:p="tapestry:parameter">
<body class="container">
    <h1>Components CRUD</h1>
    
    This example is based on the <em>Filter CRUD</em> example, replacing the list and the editor with two custom components: 
    <em>PersonList</em> and <em>PersonEdit</em>.<br/>

    <div class="eg">
        <t:eventLink event="toCreate">Create...</t:eventLink><br/>
        
        <table id="listAndEditor">
            <tbody>
                <tr>
    
                    <!-- This is the left side of the table: a list of Persons -->
    
                    <td id="listSide">
                        <t:together.componentscrud.PersonList t:id="list" partialName="partialName" selectedPersonId="listPersonId"/>
                    </td>
                    
                    <!-- This is the right side of the table: where a Person will be created, reviewed, updated, or deleted. -->
            
                    <td id="editorSide">
                        <t:together.componentscrud.PersonEdit t:id="editor" mode="editorMode" personId="editorPersonId"/>
                    </td>
                    
                </tr>
            </tbody>
        </table>
    </div>

    Like all good components, PersonList and PersonEdit do not know about each other. They handle their own events, 
    then bubble them up to their container which is, in this case, the page. The container is responsible for coordinating its components. 
    For example, when you click on a person in the list, PersonList bubbles the SELECTED event up to the page, and the page sets 
    editorMode = REVIEW, and editorPersonId = the selected id, so that on redisplay the PersonEdit will show that person for review.<br/><br/>
     
    A component can include data within an event that it bubbles up, which is what PersonList and PersonEdit do, but take note that a 
    component can also return data in its parameters - the container can read the data because component parameters are bi-directional.<br/><br/>

    Splitting the page into custom components like PersonList and PersonEdit is very appealing because it provides a clear and very 
    natural separation of concerns, but just be aware that it doesn't come for free: you have to put in a bit of extra effort to get 
    the event handling right.<br/><br/>

    References: 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ComponentResourcesCommon.html">ComponentResourcesCommon</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/ActivationRequestParameter.html">@ActivationRequestParameter</a>.<br/><br/>

    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.<br/><br/>
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/componentscrud/Persons.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/componentscrud/Persons.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/together/filtercrud.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/componentscrud/PersonList.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/componentscrud/PersonList.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/componentscrud/PersonEdit.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/componentscrud/PersonEdit.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/componentscrud/PersonEdit.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/together/PersonFilteredDataSource.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Person.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Regions.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.together.componentscrud;

import jumpstart.business.domain.person.Person;
import jumpstart.web.components.together.componentscrud.PersonEdit.Mode;

import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.annotations.ActivationRequestParameter;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Property;

@Import(stylesheet = "css/together/filtercrud.css")
public class Persons {

    // The activation context

    @Property
    private Mode editorMode;

    @Property
    private Long editorPersonId;

    // Screen fields

    @Property
    @ActivationRequestParameter
    private String partialName;

    @Property
    private Long listPersonId;

    // The code

    void onActivate(EventContext ec) {

        if (ec.getCount() == 0) {
            editorMode = null;
            editorPersonId = null;
        }
        else if (ec.getCount() == 1) {
            editorMode = ec.get(Mode.class, 0);
            editorPersonId = null;
        }
        else {
            editorMode = ec.get(Mode.class, 0);
            editorPersonId = ec.get(Long.class, 1);
        }

    }

    Object[] onPassivate() {

        if (editorMode == null) {
            return null;
        }
        else if (editorMode == Mode.CREATE) {
            return new Object[] { editorMode };
        }
        else if (editorMode == Mode.REVIEW || editorMode == Mode.UPDATE) {
            return new Object[] { editorMode, editorPersonId };
        }
        else {
            throw new IllegalStateException(editorMode.toString());
        }

    }

    void setupRender() {
        listPersonId = editorPersonId;
    }

    void onToCreate() {
        editorMode = Mode.CREATE;
        editorPersonId = null;
    }

    void onPersonSelectedFromList(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    void onCancelCreateFromEditor() {
        editorMode = null;
        editorPersonId = null;
    }

    void onCreatedFromEditor(Person person) {
        editorMode = Mode.REVIEW;
        editorPersonId = person.getId();
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    void onToUpdateFromEditor(Long personId) {
        editorMode = Mode.UPDATE;
        editorPersonId = personId;
    }

    void onDeletedFromEditor(Long personId) {
        editorMode = null;
        editorPersonId = null;
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    void onCancelUpdateFromEditor(Long personId) {
        editorMode = Mode.REVIEW;
        editorPersonId = personId;
    }

    void onUpdatedFromEditor(Person person) {
        editorMode = Mode.REVIEW;
        editorPersonId = person.getId();
    }

}


.eg {
                margin: 20px 0;
                padding: 20px;
                color: #888;
                border: 1px solid #ddd;
                border-radius: 4px;
                -webkit-border-radius: 4px;
                -mox-border-radius: 4px;
                font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
                font-size: 13px;
                font-weight: normal;
                color: #333;
                line-height: 17px;
}

.eg a {
                text-decoration: none;
                color: #3D69B6;
}

.eg a:hover {
                text-decoration: underline;
}

#listAndEditor {
                width: 100%;
                max-width: 800px;
                border: none;
                margin: 10px 0 0;
}

#listSide {
                width: 25%;
                border-right: 3px solid white;
                vertical-align: top;
                padding: 0;
}

#listContainer {
                height: 328px;
                background-color: #eee;
}

#personFilter {
                width: 100%;
                padding: 5px 0 10px;
                text-align: center;
                vertical-align: middle;
                background-color: #3d69b6;
                color: white;
                font-weight: bold;
                border-bottom: 2px solid white;
}

#personFilter input[type="search"] {
                width: 90%;
                margin: 0 5%;
}

#personList {
                background-color: #eee;
}

#personList table {
                margin: 0;
}

#personList table th {
                display: none;
}

#personList table td {
                padding: 0;
                border: 0;
                border-bottom: 2px solid white;
                background-color: #eee;
}

#personList table a {
                width: 100%;
                line-height: 50px;
                display: block;
                text-align: center;
                text-decoration: none;
                color: black;
}

#personList table a:visited {
                color: inherit;
}

#personList table a:hover {
                background: #ccc;
                color: #fff;
                text-decoration: none;
}

#personList table a.active {
                background: #999;
                color: #fff;
}

#personList .pagination {
            margin: 9px 9px 7px;                
}

#noPersons {
                text-align: center;
                padding-top: 20px;
}

#editorSide {
                width: 75%;
                height: 100%;
                vertical-align: top;
                background-color: #eee;
                padding: 20px;
}

#editorSide h1 {
                font-size: large;
                text-align: center;
                margin: 0;
}

#editorSide > dl.well {
                background-color: transparent;
                border: none;
                box-shadow: none;
                margin-bottom: 0;
                padding: 19px;              
}

#editorSide form {
                padding-top: 19px;              
}

#editorSide .buttons {
                text-align: center;
}

.error {
                color: red;
                text-align: center;
                padding-top: 19px;
}

#editorSide form .error {
                padding-top: 0;
                padding-bottom: 19px;
}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <div id="listContainer">

        <div id="personFilter">
            <t:form t:id="filterForm">
                <div>
                    Person
                </div>
                <div>
                    <t:textfield t:id="partialName" type="search" placeholder="Filter"/>
                    <t:submit value="Filter" title="Filter" style="display: none;"/>
                </div>
            </t:form>
        </div>
        
        <div id="personList">
            <t:grid t:id="list" source="persons" row="person" class="table"
                exclude="id,version,firstName,lastName,region,startDate" add="name"
                rowsPerPage="4" pagerPosition="bottom"
                empty="block:emptyPersons">
                <p:nameCell>
                    <t:eventLink event="personSelected" context="person.id" class="prop:linkCSSClass">
                        ${person.firstName} ${person.lastName}
                    </t:eventLink>
                </p:nameCell>
            </t:grid>
        </div>
        
        <t:block t:id="emptyPersons">
            <div id="noPersons">
                (No persons found)
            </div>
        </t:block>
    
    </div>

</t:content>
</html>


package jumpstart.web.components.together.componentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.web.models.together.PersonFilteredDataSource;

import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.grid.GridDataSource;

/**
 * This component will trigger the following events on its container (which in
 * this example is the page): {@link PersonList#PERSON_SELECTED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonList.PERSON_SELECTED })
public class PersonList {
    public static final String PERSON_SELECTED = "personSelected";

    // Parameters

    @Parameter(required = true)
    @Property
    private String partialName;

    @Parameter(required = true)
    @Property
    private Long selectedPersonId;

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    // The code

    boolean onPersonSelected(Long personId) {
        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

    public GridDataSource getPersons() {
        return new PersonFilteredDataSource(personFinderService, partialName);
    }

    public String getLinkCSSClass() {
        if (person != null && person.getId().equals(selectedPersonId)) {
            return "active";
        } else {
            return "";
        }
    }
}


<!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_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <t:if test="modeCreate">

        <h1>Create</h1>
        
        <t:form t:id="createForm" class="form-horizontal" validate="person">
            <t:errors globalOnly="true"/>
            <t:delegate to="block:editor"/>

            <div class="form-group">
                <div class="col-sm-4 col-sm-offset-4">
                    <t:submit value="Save" />
                    <t:eventlink event="cancelCreate" class="btn btn-default">Cancel</t:eventlink>
                </div>
            </div>
        </t:form>

    </t:if>

    <t:if test="modeReview">

        <h1>Review</h1>
        
        <t:if test="person">
            <div t:type="if" t:test="deleteMessage" class="error">
                ${deleteMessage}
            </div>
            
            <t:beandisplay object="person" include="id,version" add="name,regionX,startDateX">
                <p:name>
                    ${person.firstName} ${person.lastName}
                </p:name>
                <p:regionX>
                    ${personRegion}
                </p:regionX>
                <p:startDateX>
                <t:output value="person.startDate" format="prop:dateFormat"/>
                </p:startDateX>
            </t:beandisplay>

            <div class="buttons">
                <t:eventlink event="toUpdate" context="person.id">Update...</t:eventlink>
                <t:eventlink event="delete" context="[person.id,person.version]" 
                    t:mixins="Confirm" Confirm.message="Delete ${person.firstName} ${person.lastName}?">Delete...</t:eventlink>
            </div>
        </t:if>

        <t:if test="!person">
            Person ${personId} does not exist.<br/><br/>
        </t:if>
        
    </t:if>

    <t:if test="modeUpdate">

        <h1>Update</h1>
        
        <t:form t:id="updateForm" class="form-horizontal" validate="person">
            <t:errors globalOnly="true"/>
        
            <t:if test="person">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <t:hidden value="person.version"/>
                <t:delegate to="block:editor"/>

                <div class="form-group">
                    <div class="col-sm-4 col-sm-offset-4">
                        <t:submit value="Save" />
                        <t:eventlink event="cancelUpdate" context="personId" class="btn btn-default">Cancel</t:eventlink>
                    </div>
                </div>
            </t:if>

            <t:if test="!person">
                Person ${personId} does not exist.<br/><br/>
            </t:if>
        </t:form>
                
    </t:if>

    <t:block t:id="editor">
        <div class="form-group">
            <t:label for="firstName" class="col-sm-4"/>
            <div class="col-sm-4">
                <t:textfield t:id="firstName" value="person.firstName" />
            </div>
            <div class="col-sm-4">
                <p class="form-control-static">(required)</p>
            </div>
        </div>
        <div class="form-group">
            <t:label for="lastName" class="col-sm-4"/>
            <div class="col-sm-4">
                <t:textfield t:id="lastName" value="person.lastName" />
            </div>
            <div class="col-sm-4">
                <p class="form-control-static">(required)</p>
            </div>
        </div>
        <div class="form-group">
            <t:label for="region" class="col-sm-4"/>
            <div class="col-sm-4">
                <t:select t:id="region" value="person.region" blankOption="always" />
            </div>
            <div class="col-sm-4">
                <p class="form-control-static">(required)</p>
            </div>
        </div>
        <div class="form-group">
            <t:label for="startDate" class="col-sm-4"/>
            <div class="col-sm-4">
                <t:datefield t:id="startDate" value="person.startDate" format="prop:dateFormat" />
            </div>
            <div class="col-sm-4">
                <p class="form-control-static">(required, ${datePattern})</p>
            </div>
        </div>
    </t:block>

</t:content>
</html>


regionX-label=Region
startDateX-label=Start Date

region-blankLabel=Choose...

## These enum conversions could be moved to the central message properties file called app.properties
## The structure we've chosen (enum class name, dot, enum value) is the same as expected by the Select component.
Regions.EAST_COAST=East Coast
Regions.WEST_COAST=West Coast


package jumpstart.web.components.together.componentscrud;

import java.text.Format;
import java.text.SimpleDateFormat;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Parameter;
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.TextField;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link PersonEdit#CANCEL_CREATE}, {@link PersonEdit#CREATED}(Long personId), {@link PersonEdit#TO_UPDATE} (Long
 * personId), {@link PersonEdit#CANCEL_UPDATE}, {@link PersonEdit#UPDATED}(Long personId), {@link PersonEdit#DELETED}
 * (Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonEdit.CANCEL_CREATE, PersonEdit.CREATED, PersonEdit.TO_UPDATE, PersonEdit.CANCEL_UPDATE,
        PersonEdit.UPDATED, PersonEdit.DELETED })
public class PersonEdit {
    public static final String CANCEL_CREATE = "cancelCreate";
    public static final String CREATED = "created";
    public static final String TO_UPDATE = "toUpdate";
    public static final String CANCEL_UPDATE = "cancelUpdate";
    public static final String UPDATED = "updated";
    public static final String DELETED = "deleted";

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

    public enum Mode {
        CREATE, REVIEW, UPDATE;
    }

    // Parameters

    @Parameter(required = true)
    @Property
    private Mode mode;

    @Parameter(required = true)
    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

    @Property
    @Persist(PersistenceConstants.FLASH)
    private String deleteMessage;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @InjectComponent
    private Form createForm;

    @InjectComponent
    private Form updateForm;

    @InjectComponent("firstName")
    private TextField firstNameField;

    @Inject
    private ComponentResources componentResources;

    @Inject
    private Messages messages;

    // The code

    void setupRender() {

        if (mode == Mode.REVIEW) {
            if (personId == null) {
                person = null;
                // Handle null person in the template.
            }
            else {
                if (person == null) {
                    person = personFinderService.findPerson(personId);
                    // Handle null person in the template.
                }
            }
        }

    }

    // /////////////////////////////////////////////////////////////////////
    // CREATE
    // /////////////////////////////////////////////////////////////////////

    boolean onCancelCreate() {
        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

    void onPrepareForRenderFromCreateForm() throws Exception {

        // If fresh start, make sure there's a Person object available.

        if (createForm.isValid()) {
            person = new Person();
        }
    }

    void onPrepareForSubmitFromCreateForm() throws Exception {
        // Instantiate a Person for the form data to overlay.
        person = new Person();
    }

    boolean onValidateFromCreateForm() {

        if (person.getFirstName() != null && person.getFirstName().equals("Acme")) {
            createForm.recordError(firstNameField, firstNameField.getLabel() + " must not be Acme.");
        }

        if (demoModeStr != null && demoModeStr.equals("true")) {
            createForm.recordError("Sorry, but Create is not allowed in Demo mode.");
        }

        if (createForm.getHasErrors()) {
            return true;
        }

        try {
            person = personManagerService.createPerson(person);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            createForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }

        return true;
    }

    boolean onSuccessFromCreateForm() {
        // We want to tell our containing page explicitly what person we've created, so we trigger a new event with a
        // parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(CREATED, new Object[] { person }, null);

        // We don't want the original event to bubble up, so we return true to say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // REVIEW
    // /////////////////////////////////////////////////////////////////////

    boolean onToUpdate(Long personId) {
        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

    boolean onDelete(Long personId, Integer personVersion) {
        this.personId = personId;

        if (demoModeStr != null && demoModeStr.equals("true")) {
            deleteMessage = "Sorry, but Delete is not allowed in Demo mode.";

            // We don't want the event to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

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

            // We don't want the event to bubble up, so we return true to say
            // we've handled it.
            return true;
        }

        // Trigger new event which will bubble up.
        componentResources.triggerEvent(DELETED, new Object[] { personId }, null);
        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // UPDATE
    // /////////////////////////////////////////////////////////////////////

    boolean onCancelUpdate(Long personId) {
        this.personId = personId;

        // Return false, which means we haven't handled the event so bubble it
        // up.
        // This method is here solely as documentation, because without this
        // method the event would bubble up anyway.
        return false;
    }

    void onPrepareForRenderFromUpdateForm() {

        // If fresh start, make sure there's a Person object available.

        if (updateForm.isValid()) {
            person = personFinderService.findPerson(personId);
            // Handle null person in the template.
        }
    }

    void onPrepareForSubmitFromUpdateForm() {
        // Get objects for the form fields to overlay.
        person = personFinderService.findPerson(personId);

        if (person == null) {
            updateForm.recordError("Person has been deleted by another process.");
            // Instantiate an empty person to avoid NPE in the Form.
            person = new Person();
        }
    }

    boolean onValidateFromUpdateForm() {

        if (person.getFirstName() != null && person.getFirstName().equals("Acme")) {
            updateForm.recordError(firstNameField, firstNameField.getLabel() + " must not be Acme.");
        }

        if (personId == 2 && !person.getFirstName().equals("Mary")) {
            updateForm.recordError(firstNameField, firstNameField.getLabel() + " for this person must be Mary.");
        }

        if (updateForm.getHasErrors()) {
            return true;
        }

        try {
            person = personManagerService.changePerson(person);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            updateForm.recordError(ExceptionUtil.getRootCauseMessage(e));
        }

        return true;
    }

    boolean onSuccessFromUpdateForm() {
        // We want to tell our containing page explicitly what person we've
        // updated, so we trigger a new event
        // with a parameter. It will bubble up because we don't have a handler
        // method for it.
        componentResources.triggerEvent(UPDATED, new Object[] { person }, null);

        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

    // /////////////////////////////////////////////////////////////////////
    // GETTERS ETC.
    // /////////////////////////////////////////////////////////////////////

    public boolean isModeCreate() {
        return mode == Mode.CREATE;
    }

    public boolean isModeReview() {
        return mode == Mode.REVIEW;
    }

    public boolean isModeUpdate() {
        return mode == Mode.UPDATE;
    }

    public String getPersonRegion() {
        return messages.get(Regions.class.getSimpleName() + "." + person.getRegion().name());
    }

    public String getDatePattern() {
        return "dd/MM/yyyy";
    }

    public Format getDateFormat() {
        return new SimpleDateFormat(getDatePattern());
    }
}


package jumpstart.web.models.together;

import java.util.List;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceRemote;

import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.SortConstraint;

public class PersonFilteredDataSource implements GridDataSource {
    private IPersonFinderServiceRemote personFinderService;
    private String partialName;

    private int startIndex;
    private List<Person> preparedResults;

    public PersonFilteredDataSource(IPersonFinderServiceRemote personFinderService, String partialName) {
        this.personFinderService = personFinderService;
        this.partialName = partialName;
    }

    @Override
    public int getAvailableRows() {
        return (int) personFinderService.countPersons(partialName);
    }

    @Override
    public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
        preparedResults = personFinderService.findPersons(partialName, startIndex, endIndex - startIndex + 1);
        this.startIndex = startIndex;
    }

    @Override
    public Object getRowValue(final int index) {
        return preparedResults.get(index - startIndex);
    }

    @Override
    public Class<Person> getRowType() {
        return Person.class;
    }

}


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;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;


/**
 * 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)
    @NotNull
    @Size(max = 10)
    private String firstName;

    @Column(length = 10, nullable = false)
    @NotNull
    @Size(max = 10)
    private String lastName;
    
    @Enumerated(EnumType.STRING)
    @NotNull
    private Regions region;

    @Temporal(TemporalType.DATE)
    @NotNull
    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 JPA.
    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 {

    }

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