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.
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 IPersonFinderServiceLocal, IPersonManagerServiceLocal, 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">
    <h3>Edit (Using BeanEditForm) (1)</h3> 
    
    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">
            <!-- 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>
        
        <t:eventlink event="refresh">Refresh</t:eventlink>
    </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.4/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.4/apidocs/org/apache/tapestry5/beaneditor/package-summary.html">beaneditor package</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Hidden.html">Hidden</a>.<br/><br/>

    <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/examples/input/Edit1.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/Edit1.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
        <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.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.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.BeanEditForm;

@Import(stylesheet = "css/examples/plain.css")
public class Edit1 {

    // The activation context

    private Long personId;

    // Screen fields

    @Property
    private Person person;

    // Other pages

    @InjectPage
    private Edit2 page2;

    // Generally useful bits and pieces

    @InjectComponent("personForm")
    private BeanEditForm form;

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    // The code

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

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

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

    Long onPassivate() {
        return personId;
    }

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

    void onPrepareForRender() throws Exception {

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

        if (form.isValid()) {
            person = findPerson(personId);

            // We'd like to handle null person in the template, but can't because we're in BeanEditForm. 
            // BeanEditForm doesn't handle null object well, so throw an exception to bypass it.

            if (person == null) {
                throw new Exception("Person " + personId + " does not exist.");
            }
        }

    }

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

    void onPrepareForSubmit() {

        // Get Person object for the form fields to overlay.
        person = findPerson(personId);

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

    void onValidateFromPersonForm() {

        if (person.getFirstName() != null && person.getFirstName().equals("Acme")) {
            form.recordError("First Name must not be Acme.");
        }

        if (personId == 2 && !person.getFirstName().equals("Mary")) {
            form.recordError("First Name for this person must be Mary.");
        }

        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 onRefresh() {
        // By doing nothing the page will be displayed afresh.
    }

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

        if (person == null && personId < 4) {
            throw new IllegalStateException("Database data has not been set up!");
        }

        return person;
    }
}


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


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