AJAX Filtered Grid

Here we filter, sort, and page a Grid with AJAX requests.
The 3 filter Selects trigger an AJAX Form submit that updates a zone containing the Grid.
The sorting and paging links of the Grid send AJAX requests because we specified t:inplace="true".
:
:
:
IdVersionFirst NameLast NameRegionStart Date
1 76 Acme2 456 East Coast Jun 19, 1952
2 12 Mary w big lamb West Coast Feb 29, 2008
3 15 uyuy2 km West Coast Feb 28, 2007
4 26 yuno 3clover West Coast Feb 12, 2008
The challenge is to make the filter values available server-side for a sorting or paging request without using @Persist.
Unfortunately, Grid's sorting and paging links don't carry a context, so we can't pass the filter values that way.
Instead, we persist the filter values in the generated URLs by using @ActivationRequestParameter.

References: Select, Grid, @ActivationRequestParameter, Ajax and Zones, Zone, Request, AjaxResponseRenderer, @Inject, @InjectComponent, t5/core/zone.

Home


<!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">
<body class="container">
    <h3>AJAX Filtered Grid</h3>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>
    
    Here we filter, sort, and page a Grid with AJAX requests.<br/>
    The 3 filter Selects trigger an AJAX Form submit that updates a zone containing the Grid.<br/>
    The sorting and paging links of the Grid send AJAX requests because we specified <em>t:inplace="true"</em>.
    
    <div class="eg">
        <t:form t:id="filterCriteria" async="true" class="form-inline">
        
            <!-- On change of a Select, we do an javascript click of an invisible submit button.
                Note: do not use "this.form.performSubmit()" because on an AJAX Form it fails in Firefox. -->
    
            <div class="form-group">
                <t:label for="firstInitial"/>: 
                <t:select t:id="firstInitial" model="firstInitials" onchange="document.getElementById('filterSubmit').click()" secure="never"/>
            </div>
    
            <div class="form-group">
                <t:label for="lastInitial"/>: 
                <t:select t:id="lastInitial" model="lastInitials" onchange="document.getElementById('filterSubmit').click()" secure="never"/>
            </div>
    
            <div class="form-group">
                <t:label for="region"/>: 
                <t:select t:id="region" onchange="document.getElementById('filterSubmit').click()" secure="never"/>
            </div>
    
            <div class="form-group">
                <t:submit t:id="filterSubmit" id="filterSubmit" style="display: none;"/>
            </div>
        </t:form>

        <div>
            <t:zone t:id="personsZone" id="personsZone">
                <t:grid id="grid" source="persons" rowsPerPage="4" pagerPosition="bottom" inplace="true"/>
            </t:zone>
        </div>
    </div>

    The challenge is to make the filter values available server-side for a sorting or paging request without using <em>@Persist</em>.<br/>
    Unfortunately, Grid's sorting and paging links don't carry a context, so we can't pass the filter values that way.<br/>
    Instead, we persist the filter values in the generated URLs by using <em>@ActivationRequestParameter</em>.<br/><br/>
    
    References:
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Select.html">Select</a>,
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Grid.html">Grid</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/ActivationRequestParameter.html">@ActivationRequestParameter</a>, 
    <a href="http://tapestry.apache.org/ajax-and-zones.html">Ajax and Zones</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Zone.html">Zone</a>,
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/Request.html">Request</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/ajax/AjaxResponseRenderer.html">AjaxResponseRenderer</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">@Inject</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/InjectComponent.html">@InjectComponent</a>, 
    <a href="http://tapestry.apache.org/5.4/coffeescript/zone.html">t5/core/zone</a>.<br/><br/> 
    
    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFilteredGrid.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFilteredGrid.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AjaxFilteredGrid.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/ajaxfilteredgrid.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/examples/ajax/PersonFilteredDataSource.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/util/query/SortCriterion.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/util/query/SortDirection.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/PersonFinderService.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>


firstInitial-blankLabel=All
lastInitial-blankLabel=All
region-blankLabel=All


package jumpstart.web.pages.examples.ajax;

import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

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.web.models.examples.ajax.PersonFilteredDataSource;

import org.apache.tapestry5.annotations.ActivationRequestParameter;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

@Import(stylesheet = "css/examples/ajaxfilteredgrid.css")
public class AjaxFilteredGrid {
    static private final int MAX_RESULTS = 30;

    // Screen fields

    @Property
    private List<String> firstInitials;

    @Property
    @ActivationRequestParameter("first")
    private String firstInitial;

    @Property
    private List<String> lastInitials;

    @Property
    @ActivationRequestParameter("last")
    private String lastInitial;

    @Property
    private List<Regions> regions;

    @Property
    @ActivationRequestParameter("region")
    private Regions region;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @InjectComponent
    private Zone personsZone;

    @Inject
    private Request request;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    // The code

    void setupRender() {

        // Build 2 of the filters from the names found in a list of all persons.

        TreeSet<String> firstInitialsSet = new TreeSet<String>();
        TreeSet<String> lastInitialsSet = new TreeSet<String>();

        List<Person> allPersons = personFinderService.findPersons(MAX_RESULTS);

        for (Person person : allPersons) {
            if (person.getFirstName().length() >= 1) {
                firstInitialsSet.add(person.getFirstName().substring(0, 1).toUpperCase());
            }
            if (person.getLastName().length() >= 1) {
                lastInitialsSet.add(person.getLastName().substring(0, 1).toUpperCase());
            }
        }

        firstInitials = new ArrayList<String>(firstInitialsSet);
        lastInitials = new ArrayList<String>(lastInitialsSet);
    }

    void onValidateFromFilterCriteria() {
        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(personsZone);
        }
    }

    public GridDataSource getPersons() {
        return new PersonFilteredDataSource(personFinderService, firstInitial, lastInitial, region);
    }

}


.eg {
                margin: 20px 0;
                padding: 14px;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}

.eg form {
                margin-bottom: 14px;
}

.eg select {
                width: auto;
                margin-right: 14px;
}


package jumpstart.web.models.examples.ajax;

import java.util.ArrayList;
import java.util.List;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.Regions;
import jumpstart.business.domain.person.iface.IPersonFinderServiceRemote;
import jumpstart.util.query.SortCriterion;
import jumpstart.util.query.SortDirection;

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

public class PersonFilteredDataSource implements GridDataSource {
    private IPersonFinderServiceRemote personFinderService;
    private String firstInitial;
    private String lastInitial;
    private Regions region;

    private int startIndex;
    private List<Person> preparedResults;

    public PersonFilteredDataSource(IPersonFinderServiceRemote personFinderService, String firstInitial,
            String lastInitial, Regions region) {
        this.personFinderService = personFinderService;
        this.firstInitial = firstInitial;
        this.lastInitial = lastInitial;
        this.region = region;
    }

    @Override
    public int getAvailableRows() {
        return (int) personFinderService.countPersons(firstInitial, lastInitial, region);
    }

    @Override
    public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {

        // Get a filtered page of persons - ask business service to find them (from the database)
        
        List<SortCriterion> sortCriteria = toSortCriteria(sortConstraints);
        preparedResults = personFinderService.findPersons(firstInitial, lastInitial, region, startIndex, endIndex
                - startIndex + 1, sortCriteria);

        this.startIndex = startIndex;
    }

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

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

    /**
     * Converts a list of Tapestry's SortConstraint to a list of our business tier's SortCriterion. The business tier
     * does not use SortConstraint because that would create a dependency on Tapestry.
     */
    private List<SortCriterion> toSortCriteria(List<SortConstraint> sortConstraints) {
        List<SortCriterion> sortCriteria = new ArrayList<SortCriterion>();

        for (SortConstraint sortConstraint : sortConstraints) {

            String propertyName = sortConstraint.getPropertyModel().getPropertyName();
            SortDirection sortDirection = SortDirection.UNSORTED;

            switch (sortConstraint.getColumnSort()) {
            case ASCENDING:
                sortDirection = SortDirection.ASCENDING;
                break;
            case DESCENDING:
                sortDirection = SortDirection.DESCENDING;
                break;
            default:
            }

            SortCriterion sortCriterion = new SortCriterion(propertyName, sortDirection);
            sortCriteria.add(sortCriterion);
        }

        return sortCriteria;
    }

}


package jumpstart.util.query;

import java.io.Serializable;

@SuppressWarnings("serial")
public class SortCriterion implements Serializable {

    private String propertyName;
    private SortDirection sortDirection;

    public SortCriterion(String propertyName, SortDirection sortDirection) {
        this.propertyName = propertyName;
        this.sortDirection = sortDirection;
    }

    public String getPropertyName() {
        return propertyName;
    }

    public SortDirection getSortDirection() {
        return sortDirection;
    }

}


package jumpstart.util.query;

public enum SortDirection {
    ASCENDING, DESCENDING, UNSORTED;
    
    public String toStringForJpql() {
        if (this == ASCENDING) {
            return "";
        }
        else if (this == DESCENDING) {
            return " desc";
        }
        else {
            return "";
        }
    }
}


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

    /**
     * Finds persons who match the criteria.
     * If counting == true, returns the count of persons found, as a Long.
     * Else, returns the persons found, as a List<Person>.
     */
    @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;
        }
    }

}


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