| --- |
| Forms in Tapestry |
| --- |
| |
| Chapter 5: Forms in Tapestry (Part Two) |
| |
| So, you fill in all the fields, submit the form (without validation errors) and voila: you get back the same |
| form, blanked out. What happened, and where did the data go? |
| |
| What happened is that we haven't told Tapestry what to do after the form is succesfully submitted (by succesful, we mean, |
| with no validation errors). Tapestry's default behavior is to redisplay the active page, and that occurs in a new |
| request, with a new instance of the Address object (because the address field is not a peristent field). |
| |
| Well, since we're creating objects, we might as well store them somewhere ... in a database. We're going to |
| quickly integrate Tapestry with {{{http://hibernate.org}Hibernate}} as the object/relational mapping layer, |
| and ultimately store our data inside a {{{http://www.mysql.com/}MySQL}} database. You'll have to download |
| and install MySQL on your own. |
| |
| In addition, we need to create the database and user. The database will be "t5_tutorial1" and |
| the user will be "t5tutorialuser" with password "secret". This is accomplished from the |
| command line using the mysql shell. |
| |
| The two commands are: |
| |
| * create database t5_tutorial1; |
| |
| * grant all on t5_tutorial1.* to 't5tutorialuser'@'localhost' identified by 'secret'; |
| |
| [] |
| |
| Log into the MySQL monitor shell as user root to execute the commands: |
| |
| ---- |
| $ mysql -u root -p |
| Enter password: |
| Welcome to the MySQL monitor. Commands end with ; or \g. |
| Your MySQL connection id is 4 |
| Server version: 5.0.51a MySQL Community Server (GPL) |
| |
| Type 'help;' or '\h' for help. Type '\c' to clear the buffer. |
| |
| mysql> create database t5_tutorial1; |
| Query OK, 1 row affected (0.08 sec) |
| |
| |
| mysql> grant all on t5_tutorial1.* to 't5tutorialuser'@'localhost' identified by 'secret'; |
| Query OK, 0 rows affected (0.00 sec) |
| |
| mysql> |
| --- |
| |
| Re-configuring the Project |
| |
| We're going to bootstrap this project from a simple Tapestry project to one that uses Hibernate and MySQL. |
| |
| |
| * Updating the Dependencies |
| |
| First, we must update the POM to list a new set of dependencies, that includes Hibernate, |
| the Tapestry/Hibernate integration library, and the MySQL JDBC driver. |
| |
| <<src/pom.xml (partial):>> |
| |
| ---- |
| <dependencies> |
| <dependency> |
| <groupId>org.apache.tapestry</groupId> |
| <artifactId>tapestry-hibernate</artifactId> |
| <version>${tapestry-release-version}</version> |
| </dependency> |
| |
| <dependency> |
| <groupId>org.apache.geronimo.specs</groupId> |
| <artifactId>geronimo-jta_1.0.1B_spec</artifactId> |
| <version>1.1.1</version> |
| </dependency> |
| |
| <dependency> |
| <groupId>org.hibernate</groupId> |
| <artifactId>hibernate</artifactId> |
| <version>3.2.2.ga</version> |
| <exclusions> |
| <exclusion> |
| <groupId>javax.transaction</groupId> |
| <artifactId>jta</artifactId> |
| </exclusion> |
| </exclusions> |
| </dependency> |
| |
| <dependency> |
| <groupId>org.hibernate</groupId> |
| <artifactId>hibernate-annotations</artifactId> |
| <version>3.2.1.ga</version> |
| </dependency> |
| |
| <dependency> |
| <groupId>javax.persistence</groupId> |
| <artifactId>persistence-api</artifactId> |
| <version>1.0</version> |
| </dependency> |
| |
| <dependency> |
| <groupId>mysql</groupId> |
| <artifactId>mysql-connector-java</artifactId> |
| <version>5.1.5</version> |
| </dependency> |
| |
| <dependency> |
| <groupId>org.testng</groupId> |
| <artifactId>testng</artifactId> |
| <version>5.1</version> |
| <classifier>jdk15</classifier> |
| <scope>test</scope> |
| </dependency> |
| |
| <dependency> |
| <groupId>org.easymock</groupId> |
| <artifactId>easymock</artifactId> |
| <version>2.3</version> |
| <scope>test</scope> |
| </dependency> |
| |
| </dependencies> |
| ---- |
| |
| * Hibernate Configuration |
| |
| Hibernate has a master configuration file used to store connection and other data. |
| |
| <<src/main/resources/hibernate.cfg.xml:>> |
| |
| ---- |
| <!DOCTYPE hibernate-configuration PUBLIC |
| "-//Hibernate/Hibernate Configuration DTD 3.0//EN" |
| "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> |
| <hibernate-configuration> |
| <session-factory> |
| <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> |
| <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/t5_tutorial1</property> |
| <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> |
| <property name="hibernate.connection.username">t5tutorialuser</property> |
| <property name="hibernate.connection.password">secret</property> |
| <property name="hbm2ddl.auto">update</property> |
| <property name="hibernate.show_sql">true</property> |
| <property name="hibernate.format_sql">true</property> |
| </session-factory> |
| </hibernate-configuration> |
| ---- |
| |
| |
| Most of the configuration is to identify the JDBC driver and connection URL. |
| |
| In addition, we are configuring Hibernate to <update> the database schema; when Hibernate initializes |
| it will create or even modify tables to match the entities. Finally, we are configuring Hibernate |
| to output any SQL it executes, which is very useful when initially building an application. |
| |
| But what entities? Normally, the available entities are listed inside hibernate.cfg.xml, but that's not necessary |
| with Tapestry; in another example of convention over configuration, Tapestry locates all entity classes |
| inside the entities package and adds them to the configuration. Currently, that is just the Address entity. |
| |
| Adding Hibernate Annotations |
| |
| For an entity class to be used with Hibernate, some Hibernate annotations must be added to the class. |
| |
| Below is the updated Address class, with the Hibernate annotations (as well as the Tapestry ones). Hibernate |
| annotations can be applied to fields or to accessor methods, but the Tapestry annotations we use below |
| are for methods only. |
| |
| <<src/main/java/org/apache/tapestry/tutorial/entities/Address.java:>> |
| |
| ---- |
| package org.apache.tapestry.tutorial.entities; |
| |
| import org.apache.tapestry.beaneditor.NonVisual; |
| import org.apache.tapestry.beaneditor.Validate; |
| import org.apache.tapestry.tutorial.data.Honorific; |
| |
| import javax.persistence.Entity; |
| import javax.persistence.GeneratedValue; |
| import javax.persistence.GenerationType; |
| import javax.persistence.Id; |
| |
| @Entity |
| public class Address |
| { |
| @Id |
| @GeneratedValue(strategy = GenerationType.IDENTITY) |
| private Long id; |
| |
| private Honorific honorific; |
| |
| private String firstName; |
| |
| private String lastName; |
| |
| private String street1; |
| |
| private String street2; |
| |
| private String city; |
| |
| private String state; |
| |
| private String zip; |
| |
| private String email; |
| |
| private String phone; |
| |
| @NonVisual |
| public Long getId() |
| { |
| return id; |
| } |
| |
| public void setId(Long id) |
| { |
| this.id = id; |
| } |
| |
| public Honorific getHonorific() |
| { |
| return honorific; |
| } |
| |
| @Validate("required") |
| public String getFirstName() |
| { |
| return firstName; |
| } |
| |
| public String getLastName() |
| { |
| return lastName; |
| } |
| |
| @Validate("required") |
| public String getStreet1() |
| { |
| return street1; |
| } |
| |
| public String getStreet2() |
| { |
| return street2; |
| } |
| |
| @Validate("required") |
| public String getCity() |
| { |
| return city; |
| } |
| |
| @Validate("required") |
| public String getState() |
| { |
| return state; |
| } |
| |
| @Validate("required,regexp") |
| public String getZip() |
| { |
| return zip; |
| } |
| |
| public String getEmail() |
| { |
| return email; |
| } |
| |
| public String getPhone() |
| { |
| return phone; |
| } |
| |
| public void setCity(String city) |
| { |
| this.city = city; |
| } |
| |
| public void setEmail(String email) |
| { |
| this.email = email; |
| } |
| |
| public void setFirstName(String firstName) |
| { |
| this.firstName = firstName; |
| } |
| |
| public void setHonorific(Honorific honorific) |
| { |
| this.honorific = honorific; |
| } |
| |
| public void setLastName(String lastName) |
| { |
| this.lastName = lastName; |
| } |
| |
| public void setPhone(String phone) |
| { |
| this.phone = phone; |
| } |
| |
| public void setState(String state) |
| { |
| this.state = state; |
| } |
| |
| public void setStreet1(String street1) |
| { |
| this.street1 = street1; |
| } |
| |
| public void setStreet2(String street2) |
| { |
| this.street2 = street2; |
| } |
| |
| public void setZip(String zip) |
| { |
| this.zip = zip; |
| } |
| } |
| ---- |
| |
| Updating the Database |
| |
| So we have a database up and running, and Hibernate is configured to connect to it. Let's make use of that |
| to store our Address object in the database. |
| |
| What we need is to provide some code to be executed when the form is submitted. When a Tapestry form |
| is submitted, there is a whole series of events that get fired. The event we are interested in is the "success" |
| event, which comes late in the process, after all the values have been pulled out of the request and |
| applied to the page properties, and after all server-side validations have occured. |
| |
| The success event is only fired if there are no validation errors. |
| |
| Our event handler must do two things: |
| |
| * Use the Hibernate Session object to persist the new Address object. |
| |
| * Commit the transaction to force the data to be written to the database. |
| |
| [] |
| |
| <<src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java:>> |
| |
| ---- |
| package org.apache.tapestry.tutorial.pages.address; |
| |
| import org.apache.tapestry.annotation.InjectPage; |
| import org.apache.tapestry.annotation.Property; |
| import org.apache.tapestry.hibernate.annotations.CommitAfter; |
| import org.apache.tapestry.ioc.annotation.Inject; |
| import org.apache.tapestry.tutorial.entities.Address; |
| import org.apache.tapestry.tutorial.pages.Index; |
| import org.hibernate.Session; |
| |
| public class CreateAddress |
| { |
| @Property |
| private Address address; |
| |
| @Inject |
| private Session session; |
| |
| @InjectPage |
| private Index index; |
| |
| @CommitAfter |
| Object onSuccess() |
| { |
| session.persist(address); |
| |
| return index; |
| } |
| } |
| ---- |
| |
| The {{{../apidocs/org/apache/tapestry/ioc/annotations/Inject.html}Inject}} annotation tells Tapestry to inject a service into the |
| annotated field; |
| Tapestry includes a sophisticated |
| Inversion of Control container (similar in many ways to Spring) that is very good at locating available services |
| by type, rather than by a string id. In any case, the Hibernate Session object is exposed as a Tapestry IoC service, |
| ready to be injected (this is one of the things provided by the tapestry-hibernate module). |
| |
| Tapestry automatically starts a transaction as necessary; however that transaction will be <aborted> at the end |
| of the request. If we make changes to persistent objects, such as adding a new Address object, |
| then it is necessary to commit the transaction. |
| |
| The {{{../apidocs/org/apache/tapestry/hibernate/annotations/CommitAfter.html}CommitAfter}} annotation can be applied to any component method; if the method completes normally, the transaction |
| will be committed (and a new transaction started to replace the committed transaction). |
| |
| After persisting the new address, we return to the main Index page of the application. |
| |
| <Note: In real applications, it is rare to have pages and components directly use the Hibernate Session. It |
| is generally a better approach to define your own Data Access Object layer to perform common update operations |
| and queries.> |
| |
| Showing Addresses |
| |
| As a little preview of what's next, let's display all the Addresses entered by the user on the Index page |
| of the application. After you enter a few names, it will look something like: |
| |
| [index-grid-v1.png] Adding the Grid to the Index page |
| |
| So, how is this implemented? Primarily, its accomplished by the |
| {{{../tapestry-core/ref/org/apache/tapestry/corelib/components/Grid.html}Grid}} component. |
| |
| The Grid component is based on the same concepts as the BeanEditForm component; it can pull apart |
| a bean into columns. The columns are sortable, and when there are more entries than will fit on a single |
| page, page navigation is automatically added. |
| |
| A minimal Grid is very easy to add to the template: |
| |
| <<src/main/webapp/Index.tml (partial):>> |
| |
| ---- |
| <t:grid source="addresses"/> |
| ---- |
| |
| And all we have to do is supply the addresses property in the Java code: |
| |
| <<src/main/java/org/apache/tapestry/tutorial/pages/Index.java (partial):>> |
| |
| ---- |
| @Inject |
| private Session session; |
| |
| public List<Address> getAddresses() |
| { |
| return session.createCriteria(Address.class).list(); |
| } |
| ---- |
| |
| Here, we're using the Hibernate Session object to find all Address objects in the database. |
| Any sorting that takes place will be done in memory. This is fine for now (with only |
| a handful of Address objects in the database). Later we'll |
| see how to optimize this for very large result sets. |
| |
| What's Next? |
| |
| We have lots more to talk about: more components, more customizations, built-in Ajax support, |
| more common design and implementation patterns, |
| and even writing your own components (which is easy!). |
| |
| ... but Tapestry and this tutorial are a work in progress, so stay patient, and check out |
| the other Tapestry tutorials and resources available on the |
| {{{../index.html}Tapestry 5 home page}}. |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |