Edit (Using BeanEditForm) (1)

The BeanEditForm component is great for rapid prototyping. It inserts a Form for editing a bean.
In this example we are using it to edit a Person from the database.
[Show]

Refresh
Person is a JPA Entity Bean, retrieved from the database by PersonFinderService, which is an EJB3 Session Bean.

BeanEditForm automatically creates an "editor", ie. a label and an input field; for each property of the bean.
But where is Person's id property? BeanEditForm automatically hides properties annotated with @Id.
And where is Person's version property? We provided a p:version block which BeanEditForm used instead.

BeanEditForm is great for rapid prototyping: Naturally, BeanEditForm has limitations. The alternatives include: Caution: @NonVisual and Optimistic Locking
Never use @NonVisual on a version field - it will mess up optimistic locking. The problem is that when BeanEditForm renders its form, it excludes @NonVisual fields. Therefore, the form has no record of the version. Here is the resulting sequence:
  1. We get the person from the database.
  2. The page is rendered and sent to the browser. With @NonVisual, version will not be in the form.
  3. User makes changes and submits the form.
  4. We get the person from the database. We now have the latest version.
  5. Tapestry reads the fields from the form and overwrites the fields in person. With @NonVisual, version will not be in the form.
  6. We save the person. Optimistic locking exception will be thrown if the version in the database is different.
With @NonVisual, step 6 will fail if someone else has updated the person since step 4. This is not useful.
Without @NonVisual, step 6 will fail if someone else has updated the person since step 1. This is proper optimistic locking.

References: BeanEditForm, Using BeanEditForm, beaneditor package, Hidden.

Home

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

Edit1.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" xmlns:p="tapestry:parameter">
<head>
    <link rel="stylesheet" type="text/css" href="${context:css/examples/examples.css}"/>
</head>
<body>
    <h1>Edit (Using BeanEditForm) (1)</h1> 
    
    The BeanEditForm component is great for rapid prototyping. It inserts a Form for editing a bean.<br/>
    In this example we are using it to edit a Person from the database.<br/>

    <div class="eg">
        <t:beaneditform t:id="personForm" object="person" submitLabel="Save">[BeanEditForm here]
            <!-- If optimistic locking is not needed then comment out this next bit. It works because Hidden fields are part of the submit. -->
            <p:version>
                <t:hidden value="person.version"/>
            </p:version>
        </t:beaneditform><br/>
        
        <a t:type="eventlink" t:event="refresh" href="#">Refresh</a>
    </div>

    Person is a JPA Entity Bean, retrieved from the database by PersonFinderService, which is an EJB3 Session Bean.<br/><br/>
    
    BeanEditForm automatically creates an "editor", ie. a label and an input field; for each property of the bean.<br/> 
    <strong>But where is Person's <code>id</code> property?</strong> BeanEditForm automatically hides properties annotated with <code>@Id</code>.<br/>
    <strong>And where is Person's <code>version</code> property?</strong> We provided a <code>p:version</code> block which BeanEditForm used instead.<br/><br/>
    
    BeanEditForm is great for rapid prototyping:
    <ul>
        <li>Its parameters allow you to exclude fields, include fields, reorder fields, etc.</li>
        <li>Its styling can be overridden with CSS.</li>
        <li>Its editors can be overridden as we did above to hide <code>version</code>. See also the Property Editors example.</li>
        <li>It automatically hides properties annotated with <code>@NonVisual</code>, but read the Caution below.</li>
    </ul>
    
    Naturally, BeanEditForm has limitations. The alternatives include:
    <ul>
        <li>Use what BeanEditForm uses: Form and BeanEditor. See the More Control Edit (Using BeanEditor) example.</li>
        <li>Use a Form and all the usual input components. See the Total Control Edit example.</li>
        <li>Build components, like BeanEditForm, that suit your needs. See the Component examples and the Tapestry source for BeanEditForm.</li>
    </ul>
    
    <strong>Caution: @NonVisual and Optimistic Locking</strong><br/> 
    Never use <code>@NonVisual</code> on a version field - it will mess up optimistic locking. The problem is that when BeanEditForm renders 
    its form, it excludes <code>@NonVisual</code> fields. Therefore, the form has no record of the version. Here is the resulting sequence:
    <ol>
        <li>We get the person from the database.</li>
        <li>The page is rendered and sent to the browser. With @NonVisual, version will not be in the form.</li>
        <li>User makes changes and submits the form.</li>
        <li>We get the person from the database. We now have the latest version.</li>
        <li>Tapestry reads the fields from the form and overwrites the fields in person. With @NonVisual, version will not be in the form.</li>
        <li>We save the person. Optimistic locking exception will be thrown if the version in the database is different.</li>
    </ol>
    With @NonVisual, step 6 will fail if someone else has updated the person since step 4. This is not useful.<br/>
    Without @NonVisual, step 6 will fail if someone else has updated the person since step 1. This is proper optimistic locking.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/BeanEditForm.html">BeanEditForm</a>, 
    <a href="http://tapestry.apache.org/beaneditform-guide.html">Using BeanEditForm</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/beaneditor/package-summary.html">beaneditor package</a>, 
    <a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/Hidden.html">Hidden</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/input/Edit1.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/input/Edit1.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"/>
    <t:sourcecodedisplay src="/business/src/main/java/jumpstart/business/domain/person/PersonManagerService.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>

Edit1.java


package jumpstart.web.pages.examples.input;

import javax.ejb.EJB;

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

import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.BeanEditForm;

public class Edit1 {

    // The activation context

    private Long personId;

    // Screen fields

    @Property
    private Person person;

    // Work fields

    // This carries version through the redirect that follows a server-side validation failure.
    @Persist(PersistenceConstants.FLASH)
    private Integer versionFlash;

    // Other pages

    @InjectPage
    private Edit2 page2;

    // Generally useful bits and pieces

    @Component(id = "personForm")
    private BeanEditForm form;

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    // The code

    // onPassivate() is called by Tapestry to get the activation context to put in the URL.

    Long onPassivate() {
        return personId;
    }

    // onActivate() is called by Tapestry to pass in the activation context from the URL.

    void onActivate(Long personId) {
        this.personId = personId;
    }

    // Form bubbles up the PREPARE_FOR_RENDER event during form render.

    void onPrepareForRender() throws Exception {
        person = findPerson(personId);

        // If the form has errors then we're redisplaying after a redirect.
        // Form will restore your input values but it's up to us to restore Hidden values.

        if (form.getHasErrors()) {
            person.setVersion(versionFlash);
        }
    }

    // Form bubbles up the PREPARE_FOR_SUBMIT event during form submission.

    void onPrepareForSubmit() throws Exception {
        // Get objects for the form fields to overlay.
        person = findPerson(personId);
    }

    void onValidateFromPersonForm() {

        if (form.getHasErrors()) {
            // We get here only if a server-side validator detected an error.
            return;
        }

        try {
            personManagerService.changePerson(person);
        }
        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));
        }
    }

    Object onSuccess() {
        page2.set(personId);
        return page2;
    }
    
    void onFailure() {
        versionFlash = person.getVersion();
    }

    void onRefresh() {
        // By doing nothing the page will be displayed afresh.
    }

    private Person findPerson(Long personId) throws Exception {
        Person person = personFinderService.findPerson(personId);

        if (person == null) {
            if (personId < 4) {
                throw new IllegalStateException("Database data has not been set up!");
            }
            else {
                throw new Exception("Person " + personId + " does not exist.");
            }
        }

        return person;
    }
}

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

}

PersonManagerService.java


package jumpstart.business.domain.person;

import java.util.List;

import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceContext;

import jumpstart.business.commons.IdVersion;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceRemote;
import jumpstart.business.domain.person.iface.PersonDTO;

@Stateless
@Local(IPersonManagerServiceLocal.class)
@Remote(IPersonManagerServiceRemote.class)
public class PersonManagerService implements IPersonManagerServiceLocal, IPersonManagerServiceRemote {

    @PersistenceContext(unitName = "jumpstart")
    public EntityManager em;

    public Person createPerson(Person person) {
        em.persist(person);
        return person;
    }

    public void createPersons(List<Person> persons) {
        for (Person person : persons) {
            em.persist(person);
        }
    }

    public void changePerson(Person person) {
        Person p = em.merge(person);
        // Flush to work around OPENEJB issue https://issues.apache.org/jira/browse/OPENEJB-782
        em.flush();

        // If id is different it means the person did not exist so merge has created a new one.
        if (!p.getId().equals(person.getId())) {
            throw new EntityNotFoundException("Person no longer exists.");
        }
    }

    public void changePersons(List<Person> persons) {
        for (Person person : persons) {
            Person p = em.merge(person);

            // If id is different it means the person did not exist so merge has created a new one.
            if (!p.getId().equals(person.getId())) {
                throw new EntityNotFoundException("Person no longer exists.");
            }
        }
    }

    public void changePersonsByDTOs(List<PersonDTO> persons) {
        for (PersonDTO person : persons) {
            Person p = em.find(Person.class, person.getId());

            if (p == null) {
                throw new EntityNotFoundException("Person no longer exists.");
            }

            if (!p.getVersion().equals(person.getVersion())) {
                throw new OptimisticLockException();
            }

            p.setFirstName(person.getFirstName());
        }
    }

    public void bulkEditPersons(List<Person> personsToCreate, List<Person> personsToChange,
            List<IdVersion> personsToDelete) {
        for (Person person : personsToCreate) {
            em.persist(person);
        }
        for (Person person : personsToChange) {
            Person p = em.merge(person);

            // If id is different it means the person did not exist so merge has created a new one.
            if (!p.getId().equals(person.getId())) {
                throw new EntityNotFoundException("Person no longer exists.");
            }
        }
        for (IdVersion idVersion : personsToDelete) {
            Person p = em.find(Person.class, idVersion.getId());

            if (p == null) {
                throw new EntityNotFoundException("Person no longer exists.");
            }

            if (!p.getVersion().equals(idVersion.getVersion())) {
                throw new OptimisticLockException();
            }

            em.remove(p);
        }
    }

    public void bulkEditPersonsByDTOs(List<PersonDTO> personsToCreate, List<PersonDTO> personsToChange,
            List<IdVersion> personsToDelete) {
        for (PersonDTO p : personsToCreate) {
            Person person = new Person(p.getFirstName(), p.getLastName(), p.getRegion(), p.getStartDate());
            em.persist(person);
        }
        for (PersonDTO person : personsToChange) {
            Person p = em.find(Person.class, person.getId());

            if (p == null) {
                throw new EntityNotFoundException("Person no longer exists.");
            }

            if (!p.getVersion().equals(person.getVersion())) {
                throw new OptimisticLockException();
            }

            p.setFirstName(person.getFirstName());
            p.setLastName(person.getLastName());
            p.setRegion(person.getRegion());
            p.setStartDate(person.getStartDate());
        }
        for (IdVersion idVersion : personsToDelete) {
            Person p = em.find(Person.class, idVersion.getId());

            if (p == null) {
                throw new EntityNotFoundException("Person no longer exists.");
            }

            if (!p.getVersion().equals(idVersion.getVersion())) {
                throw new OptimisticLockException();
            }

            em.remove(p);
        }
    }

    public void deletePerson(Long id, Integer version) {
        Person p = em.find(Person.class, id);

        if (p == null) {
            throw new EntityNotFoundException("Person no longer exists.");
        }

        if (!p.getVersion().equals(version)) {
            throw new OptimisticLockException();
        }

        em.remove(p);
        // Flush to work around OPENEJB issue https://issues.apache.org/jira/browse/OPENEJB-782
        em.flush();
    }

}

Person.java


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

}

Regions.java


package jumpstart.business.domain.person;

public enum Regions {
    EAST_COAST, WEST_COAST;
}