Person
|
Review
|
<!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
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>One Page CRUD</h1>
This example handles all the CRUD functions with a single page.<br/>
Notice how, despite being a single page, it produces a readable URL that can be bookmarked.<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">
<div id="listContainer">
<div id="listTitle">
Person
</div>
<div id="personList">
<t:grid t:id="list" source="listPersons" row="listPerson" class="table"
exclude="id,version,firstName,lastName,region,startDate" add="name"
rowsPerPage="4" pagerPosition="bottom"
empty="block:emptyPersons">
<p:nameCell>
<a t:type="eventLink" t:event="personSelected" t:context="listPerson.id" class="prop:linkCSSClass" href="#">
${listPerson.firstName} ${listPerson.lastName}
</a>
</p:nameCell>
</t:grid>
</div>
<t:block t:id="emptyPersons">
<div id="noPersons">
(No persons found)
</div>
</t:block>
</div>
</td>
<!-- This is the right side of the table: where a Person will be created, reviewed, or updated. -->
<td id="editorSide">
<t:if test="modeCreate">
<h1>Create</h1>
<t:form t:id="createForm" class="form-horizontal" validate="editorPerson">
<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="editorPerson">
<div t:type="if" t:test="deleteMessage" class="error">
${deleteMessage}
</div>
<t:beandisplay object="editorPerson" include="id,version" add="name,regionX,startDateX">
<p:name>
${editorPerson.firstName} ${editorPerson.lastName}
</p:name>
<p:regionX>
${editorPersonRegion}
</p:regionX>
<p:startDateX>
<t:output value="editorPerson.startDate" format="prop:dateFormat"/>
</p:startDateX>
</t:beandisplay>
<div class="buttons">
<t:eventlink event="toUpdate" context="editorPerson.id">Update...</t:eventlink>
<t:eventlink event="delete" context="[editorPerson.id,editorPerson.version]"
t:mixins="Confirm" Confirm.message="Delete ${editorPerson.firstName} ${editorPerson.lastName}?">Delete...</t:eventlink>
</div>
</t:if>
<t:if test="!editorPerson">
Person ${editorPersonId} 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="editorPerson">
<t:errors globalOnly="true"/>
<t:if test="editorPerson">
<!-- 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="editorPerson.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="editorPersonId" class="btn btn-default">Cancel</t:eventlink>
</div>
</div>
</t:if>
<t:if test="!editorPerson">
Person ${editorPersonId} does not exist.<br/><br/>
</t:if>
</t:form>
</t:if>
</td>
</tr>
</tbody>
</table>
<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="editorPerson.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="editorPerson.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="editorPerson.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="editorPerson.startDate" format="prop:dateFormat"/>
</div>
<div class="col-sm-4">
<p class="form-control-static">
(required, ${datePattern})
</p>
</div>
</div>
</t:block>
</div>
<t:pagelink page="Index">Home</t:pagelink><br/><br/>
The source for IPersonFinderServiceLocal, IPersonManagerServiceLocal, 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/onepagecrud/Persons.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/onepagecrud/Persons.properties"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/together/onepagecrud/Persons.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/together/onepagecrud.css"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/together/PersonPagedDataSource.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/Person.java"/>
<t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Regions.java"/>
</t:tabgroup>
</body>
</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.pages.together.onepagecrud;
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 jumpstart.web.models.together.PersonPagedDataSource;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
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.grid.GridDataSource;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
@Import(stylesheet = "css/together/onepagecrud.css")
public class Persons {
private final String demoModeStr = System.getProperty("jumpstart.demo-mode");
private enum Mode {
CREATE, REVIEW, UPDATE;
}
// The activation context
@Property
private Mode editorMode;
@Property
private Long editorPersonId;
// Screen fields
@Property
private GridDataSource listPersons;
@Property
private Person listPerson;
@Property
private Person editorPerson;
@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 Messages messages;
// 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() {
listPersons = new PersonPagedDataSource(personFinderService);
if (editorMode == Mode.REVIEW) {
if (editorPersonId == null) {
editorPerson = null;
// Handle null editorPerson in the template.
}
else {
if (editorPerson == null) {
editorPerson = personFinderService.findPerson(editorPersonId);
// Handle null editorPerson in the template.
}
}
}
}
void onToCreate() {
editorMode = Mode.CREATE;
editorPersonId = null;
}
void onPersonSelected(Long personId) {
editorMode = Mode.REVIEW;
editorPersonId = personId;
}
// /////////////////////////////////////////////////////////////////////
// CREATE
// /////////////////////////////////////////////////////////////////////
void onCancelCreate() {
editorMode = null;
editorPersonId = null;
}
void onPrepareForRenderFromCreateForm() throws Exception {
// If fresh start, make sure there's a Person object available.
if (createForm.isValid()) {
editorMode = Mode.CREATE;
editorPerson = new Person();
}
}
void onPrepareForSubmitFromCreateForm() throws Exception {
editorMode = Mode.CREATE;
// Instantiate a Person for the form data to overlay.
editorPerson = new Person();
}
void onValidateFromCreateForm() {
if (editorPerson.getFirstName() != null && editorPerson.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;
}
try {
editorPerson = personManagerService.createPerson(editorPerson);
}
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));
}
}
void onSuccessFromCreateForm() {
editorMode = Mode.REVIEW;
editorPersonId = editorPerson.getId();
}
// /////////////////////////////////////////////////////////////////////
// REVIEW
// /////////////////////////////////////////////////////////////////////
void onToUpdate(Long personId) {
editorMode = Mode.UPDATE;
editorPersonId = personId;
}
void onDelete(Long personId, Integer personVersion) {
editorMode = Mode.REVIEW;
editorPersonId = personId;
if (demoModeStr != null && demoModeStr.equals("true")) {
deleteMessage = "Sorry, but Delete is not allowed in Demo mode.";
return;
}
try {
personManagerService.deletePerson(personId, personVersion);
editorMode = null;
editorPersonId = null;
}
catch (Exception e) {
// Display the cause. In a real system we would try harder to get a user-friendly message.
deleteMessage = ExceptionUtil.getRootCauseMessage(e);
}
}
// /////////////////////////////////////////////////////////////////////
// UPDATE
// /////////////////////////////////////////////////////////////////////
void onCancelUpdate(Long personId) {
editorMode = Mode.REVIEW;
editorPersonId = personId;
}
void onPrepareForRenderFromUpdateForm() {
// If fresh start, make sure there's a Person object available.
if (updateForm.isValid()) {
editorMode = Mode.UPDATE;
editorPerson = personFinderService.findPerson(editorPersonId);
// Handle null editorPerson in the template.
}
}
void onPrepareForSubmitFromUpdateForm() {
editorMode = Mode.UPDATE;
// Get objects for the form fields to overlay.
editorPerson = personFinderService.findPerson(editorPersonId);
if (editorPerson == null) {
updateForm.recordError("Person has been deleted by another process.");
// Instantiate an empty person to avoid NPE in the Form.
editorPerson = new Person();
}
}
void onValidateFromUpdateForm() {
if (editorPerson.getFirstName() != null && editorPerson.getFirstName().equals("Acme")) {
updateForm.recordError(firstNameField, firstNameField.getLabel() + " must not be Acme.");
}
if (editorPersonId == 2 && !editorPerson.getFirstName().equals("Mary")) {
updateForm.recordError(firstNameField, firstNameField.getLabel() + " for this person must be Mary.");
}
if (updateForm.getHasErrors()) {
return;
}
try {
personManagerService.changePerson(editorPerson);
}
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));
}
}
void onSuccessFromUpdateForm() {
editorMode = Mode.REVIEW;
editorPersonId = editorPerson.getId();
}
// /////////////////////////////////////////////////////////////////////
// GETTERS ETC.
// /////////////////////////////////////////////////////////////////////
public String getLinkCSSClass() {
if (listPerson != null && listPerson.getId().equals(editorPersonId)) {
return "active";
}
else {
return "";
}
}
public boolean isModeCreate() {
return editorMode == Mode.CREATE;
}
public boolean isModeReview() {
return editorMode == Mode.REVIEW;
}
public boolean isModeUpdate() {
return editorMode == Mode.UPDATE;
}
public String getEditorPersonRegion() {
return messages.get(Regions.class.getSimpleName() + "." + editorPerson.getRegion().name());
}
public String getDatePattern() {
return "dd/MM/yyyy";
}
public Format getDateFormat() {
return new SimpleDateFormat(getDatePattern());
}
}
.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;
position: relative;
}
.eg a {
text-decoration: none;
color: #3D69B6;
}
.eg a:hover {
text-decoration: underline;
}
#listAndEditor {
width: 100%;
max-width: 800px;
bottom: 0;
border: none;
margin: 10px 0 0;
}
#listSide {
width: 25%;
border-right: 3px solid white;
vertical-align: top;
padding: 0;
}
#listContainer {
height: 319px;
background-color: #eee;
}
#listTitle {
width: 100%;
padding: 20px 0;
text-align: center;
vertical-align: middle;
background-color: #3d69b6;
color: white;
font-weight: bold;
border-bottom: 2px solid white;
}
#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;
}
package jumpstart.web.models.together;
import java.util.ArrayList;
import java.util.List;
import jumpstart.business.domain.person.Person;
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 PersonPagedDataSource implements GridDataSource {
private IPersonFinderServiceRemote personFinderService;
private int startIndex;
private List<Person> preparedResults;
public PersonPagedDataSource(IPersonFinderServiceRemote personFinderService) {
this.personFinderService = personFinderService;
}
@Override
public int getAvailableRows() {
return (int) personFinderService.countPersons();
}
@Override
public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
List<SortCriterion> sortCriteria = new ArrayList<SortCriterion>();
sortCriteria.add(new SortCriterion("firstName", SortDirection.ASCENDING));
sortCriteria.add(new SortCriterion("lastName", SortDirection.ASCENDING));
preparedResults = personFinderService.findPersons(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;
}
}
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.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;
}