Author: hlship
Date: Tue May 6 09:47:43 2008
New Revision: 653834
URL: http://svn.apache.org/viewvc?rev=653834&view=rev
Log:
Update the Tutorial:
- Use simple field name (no leading underscore)
- Fill out page forms2, to discuss the submit event and configuring Hibernate
- Introduce the Grid component
Added:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/Address.java
- copied, changed from r647587,
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/data/Address.java
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/hibernate.cfg.xml
Removed:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v4.png
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/pom.xml
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Guess.java
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Index.java
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/log4j.properties
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/webapp/Index.tml
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms.apt
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms2.apt
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/hilo.apt
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v6.png
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v7.png
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v8.png
Modified: tapestry/tapestry5/trunk/tapestry-tutorial1/pom.xml
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/pom.xml?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-tutorial1/pom.xml (original)
+++ tapestry/tapestry5/trunk/tapestry-tutorial1/pom.xml Tue May 6 09:47:43 2008
@@ -25,10 +25,48 @@
<dependencies>
<dependency>
<groupId>org.apache.tapestry</groupId>
- <artifactId>tapestry-core</artifactId>
+ <artifactId>tapestry-hibernate</artifactId>
<version>${project.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>
+
+
<!-- A dependency on either JUnit or TestNG is required, or the
surefire plugin (which runs the tests)
will fail, preventing Maven from packaging the WAR. Tapestry includes a large
number
of testing facilities designed for use with TestNG (http://testng.org/), so
it's recommended. -->
Copied:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/Address.java
(from r647587,
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/data/Address.java)
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/Address.java?p2=tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/Address.java&p1=tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/data/Address.java&r1=647587&r2=653834&rev=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/data/Address.java
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/entities/Address.java
Tue May 6 09:47:43 2008
@@ -1,4 +1,4 @@
-// Copyright 2007 The Apache Software Foundation
+// Copyright 2007, 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,134 +12,157 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package org.apache.tapestry.tutorial.data;
+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;
+
[EMAIL PROTECTED]
public class Address
{
- private Honorific _honorific;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private Honorific honorific;
+
+ private String firstName;
+
+ private String lastName;
- private String _firstName;
+ private String street1;
- private String _lastName;
+ private String street2;
- private String _street1;
+ private String city;
- private String _street2;
+ private String state;
- private String _city;
+ private String zip;
- private String _state;
+ private String email;
- private String _zip;
+ private String phone;
- private String _email;
+ @NonVisual
+ public Long getId()
+ {
+ return id;
+ }
- private String _phone;
+ public void setId(Long id)
+ {
+ this.id = id;
+ }
public Honorific getHonorific()
{
- return _honorific;
+ return honorific;
}
@Validate("required")
public String getFirstName()
{
- return _firstName;
+ return firstName;
}
public String getLastName()
{
- return _lastName;
+ return lastName;
}
@Validate("required")
public String getStreet1()
{
- return _street1;
+ return street1;
}
public String getStreet2()
{
- return _street2;
+ return street2;
}
@Validate("required")
public String getCity()
{
- return _city;
+ return city;
}
@Validate("required")
public String getState()
{
- return _state;
+ return state;
}
@Validate("required,regexp")
public String getZip()
{
- return _zip;
+ return zip;
}
public String getEmail()
{
- return _email;
+ return email;
}
public String getPhone()
{
- return _phone;
+ return phone;
}
public void setCity(String city)
{
- _city = city;
+ this.city = city;
}
public void setEmail(String email)
{
- _email = email;
+ this.email = email;
}
public void setFirstName(String firstName)
{
- _firstName = firstName;
+ this.firstName = firstName;
}
public void setHonorific(Honorific honorific)
{
- _honorific = honorific;
+ this.honorific = honorific;
}
public void setLastName(String lastName)
{
- _lastName = lastName;
+ this.lastName = lastName;
}
public void setPhone(String phone)
{
- _phone = phone;
+ this.phone = phone;
}
public void setState(String state)
{
- _state = state;
+ this.state = state;
}
public void setStreet1(String street1)
{
- _street1 = street1;
+ this.street1 = street1;
}
public void setStreet2(String street2)
{
- _street2 = street2;
+ this.street2 = street2;
}
public void setZip(String zip)
{
- _zip = zip;
+ this.zip = zip;
}
}
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java
Tue May 6 09:47:43 2008
@@ -11,6 +11,19 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+// Copyright 2007, 2008 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
package org.apache.tapestry.tutorial.pages;
@@ -21,11 +34,11 @@
{
@Persist
@Property
- private int _count;
+ private int count;
Object initialize(int count)
{
- _count = count;
+ this.count = count;
return this;
}
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Guess.java
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Guess.java?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Guess.java
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Guess.java
Tue May 6 09:47:43 2008
@@ -14,52 +14,52 @@
package org.apache.tapestry.tutorial.pages;
-import org.apache.tapestry.annotations.Property;
import org.apache.tapestry.annotations.InjectPage;
import org.apache.tapestry.annotations.Persist;
+import org.apache.tapestry.annotations.Property;
public class Guess
{
@Persist
- private int _target;
+ private int target;
@Property
- private int _guess;
+ private int guess;
@Persist
@Property
- private String _message;
+ private String message;
@Persist
- private int _count;
+ private int count;
@InjectPage
- private GameOver _gameOver;
+ private GameOver gameOver;
Object onActionFromLink(int guess)
{
- _count++;
+ count++;
- if (guess == _target) return _gameOver.initialize(_count);
+ if (guess == target) return gameOver.initialize(count);
- if (guess < _target)
- _message = String.format("%d is too low.", guess);
+ if (guess < target)
+ message = String.format("%d is too low.", guess);
else
- _message = String.format("%d is too high.", guess);
+ message = String.format("%d is too high.", guess);
return null;
}
Object initialize(int target)
{
- _target = target;
- _count = 0;
+ this.target = target;
+ count = 0;
return this;
}
public int getTarget()
{
- return _target;
+ return target;
}
}
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Index.java
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Index.java?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Index.java
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/Index.java
Tue May 6 09:47:43 2008
@@ -15,20 +15,33 @@
package org.apache.tapestry.tutorial.pages;
import org.apache.tapestry.annotations.InjectPage;
+import org.apache.tapestry.ioc.annotations.Inject;
+import org.apache.tapestry.tutorial.entities.Address;
+import org.hibernate.Session;
+import java.util.List;
import java.util.Random;
public class Index
{
- private final Random _random = new Random();
+ private final Random random = new Random();
@InjectPage
- private Guess _guess;
+ private Guess guess;
+
+ @Inject
+ private Session session;
Object onAction()
{
- int target = _random.nextInt(10) + 1;
+ int target = random.nextInt(10) + 1;
+
+ return guess.initialize(target);
+ }
- return _guess.initialize(target);
+ @SuppressWarnings({ "unchecked" })
+ public List<Address> getAddresses()
+ {
+ return session.createCriteria(Address.class).list();
}
}
\ No newline at end of file
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/java/org/apache/tapestry/tutorial/pages/address/CreateAddress.java
Tue May 6 09:47:43 2008
@@ -1,4 +1,4 @@
-// Copyright 2007 The Apache Software Foundation
+// Copyright 2007, 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,11 +14,30 @@
package org.apache.tapestry.tutorial.pages.address;
+import org.apache.tapestry.annotations.InjectPage;
import org.apache.tapestry.annotations.Property;
-import org.apache.tapestry.tutorial.data.Address;
+import org.apache.tapestry.hibernate.annotations.CommitAfter;
+import org.apache.tapestry.ioc.annotations.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;
+ private Address address;
+
+ @Inject
+ private Session session;
+
+ @InjectPage
+ private Index index;
+
+ @CommitAfter
+ Object onSuccess()
+ {
+ session.persist(address);
+
+ return index;
+ }
}
Added:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/hibernate.cfg.xml
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/hibernate.cfg.xml?rev=653834&view=auto
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/hibernate.cfg.xml
(added)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/hibernate.cfg.xml
Tue May 6 09:47:43 2008
@@ -0,0 +1,23 @@
+<!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>
+
+ <!--
+
+ To create user, login a root and:
+
+ grant all on t5_tutorial1.* to 't5tutorialuser'@'localhost'
identified by 'secret';
+
+ -->
+ <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>
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/log4j.properties
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/log4j.properties?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/log4j.properties
(original)
+++
tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/resources/log4j.properties
Tue May 6 09:47:43 2008
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-log4j.rootCategory=WARN, A1
+log4j.rootCategory=info, A1
# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender
Modified: tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/webapp/Index.tml
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/webapp/Index.tml?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/webapp/Index.tml
(original)
+++ tapestry/tapestry5/trunk/tapestry-tutorial1/src/main/webapp/Index.tml Tue
May 6 09:47:43 2008
@@ -14,6 +14,8 @@
<h1>Address Book</h1>
+ <t:grid source="addresses"/>
+
<ul>
<li>
<t:pagelink page="address/create">Create new
address</t:pagelink>
Modified: tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms.apt
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms.apt?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms.apt
(original)
+++ tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms.apt Tue May
6 09:47:43 2008
@@ -10,135 +10,138 @@
Form support in Tapestry is deep and rich, more than can be covered in a
single chapter. However, we can show the basics, including
some very common development patterns. To get started, let's create a simple
address book application.
- We'll start with the data, a simple object to store the information we'll
need. By convention, these classes go in a <<<data>>> sub-package. Unlike
- the use of the <<<pages>>> sub-package (for page component classes), this is
not enforced by Tapestry; it's just a convention.
+ We'll start with the entity data, a simple object to store the information
we'll need. These classes go in an <<<entities>>> sub-package. Unlike
+ the use of the <<<pages>>> sub-package (for page component classes), this is
not enforced by Tapestry; it's just a convention (but
+ as we'll see shortly, a handy one).
- <<src/main/java/org/apache/tapestry/tutorial/data/Address.java:>>
+ <<src/main/java/org/apache/tapestry/tutorial/entities/Address.java:>>
----
-package org.apache.tapestry.tutorial.data;
+package org.apache.tapestry.tutorial.entities;
+
+import org.apache.tapestry.tutorial.data.Honorific;
public class Address
{
- private Honorific _honorific;
+ private Honorific honorific;
- private String _firstName;
+ private String firstName;
- private String _lastName;
+ private String lastName;
- private String _street1;
+ private String street1;
- private String _street2;
+ private String street2;
- private String _city;
+ private String city;
- private String _state;
+ private String state;
- private String _zip;
+ private String zip;
- private String _email;
+ private String email;
- private String _phone;
+ private String phone;
public String getCity()
{
- return _city;
+ return city;
}
public String getEmail()
{
- return _email;
+ return email;
}
public String getFirstName()
{
- return _firstName;
+ return firstName;
}
public Honorific getHonorific()
{
- return _honorific;
+ return honorific;
}
public String getLastName()
{
- return _lastName;
+ return lastName;
}
public String getPhone()
{
- return _phone;
+ return phone;
}
public String getState()
{
- return _state;
+ return state;
}
public String getStreet1()
{
- return _street1;
+ return street1;
}
public String getStreet2()
{
- return _street2;
+ return street2;
}
public String getZip()
{
- return _zip;
+ return zip;
}
public void setCity(String city)
{
- _city = city;
+ this.city = city;
}
public void setEmail(String email)
{
- _email = email;
+ this.email = email;
}
public void setFirstName(String firstName)
{
- _firstName = firstName;
+ this.firstName = firstName;
}
public void setHonorific(Honorific honorific)
{
- _honorific = honorific;
+ this.honorific = honorific;
}
public void setLastName(String lastName)
{
- _lastName = lastName;
+ this.lastName = lastName;
}
public void setPhone(String phone)
{
- _phone = phone;
+ this.phone = phone;
}
public void setState(String state)
{
- _state = state;
+ this.state = state;
}
public void setStreet1(String street1)
{
- _street1 = street1;
+ this.street1 = street1;
}
public void setStreet2(String street2)
{
- _street2 = street2;
+ this.street2 = street2;
}
public void setZip(String zip)
{
- _zip = zip;
+ this.zip = zip;
}
}
----
@@ -219,7 +222,7 @@
Another note: Index pages work in folders as well. A class named
org.apache.tapestry.tutorial.pages.address.AddressIndex would be given the name
"address/Index". However, Tapestry has special rules for pages named
"Index" and the render URL would be
- <<<http://localhost:8080/tutorial1/address"/>>>. In other words, you can
place Index pages in any folder and Tapestry will
+ http://localhost:8080/tutorial1/address/ . In other words, you can place
Index pages in any folder and Tapestry will
build a short URL for that page ... and you <don't> have to keep naming the
classes Index (it's confusing to have many classes with the same
name, even across multiple packages); instead, you can name each index page
after the package
that contains it. Tapestry users a smart <convention> to keep it all
straight and generate short, to the point URLs.
@@ -238,15 +241,18 @@
And match that up with a property in the CreateAddress class:
----
- @GenerateAcessors
- private Address _address
+ @Property
+ private Address address
----
When you refresh the page, you'll see the following:
[address-v1.png] Initial version of the create address form
- Tapestry's done quite a bit of work here. Its created a form that includes
a field for each property. Further, its seen that the
+ <There have been minor changes to the default CSS since this screenshot was
taken; for example, the labels are a bit
+ wider now. In addition, the Honorific field (being optional) would include
a blank option, rather than the first real selection, "Mr".>
+
+ Tapestry's done quite a bit of work here. It has created a form that
includes a field for each property. Further, its seen that the
honorific property is an enumerated type, and presented that as a drop-down
list.
In addition, Tapestry has converted the property names ("city", "email",
"firstName") to user presentable labels ("City", "Email", "First Name").
@@ -312,28 +318,10 @@
phone-label=Phone Number
----
- Since this is a <new> file (and not a change to an existing file), you will
have to restart Jetty to force Tapestry to pick up the change.
+ Since this is a <new> file (and not a change to an existing file), you may
have to restart Jetty to force Tapestry to pick up the change.
[address-v3.png] Create Address form with field labels corrected
- However, things are getting a little crowded in the form. That too is easy
to fix: we can just provide our own overriding
- Cascading Style Sheet (CSS) rules. Tapestry has automatically injected a
built-in CSS style sheet to provide the fonts and colors in the page, we
- just need to tweak it a bit. Add the following to CreateAddress.html:
-
-----
-<style>
- DIV.t-beaneditor LABEL {
- width: 200px;
- }
-</style>
-----
-
- The "t-beaneditor" CSS class refers to a \<div\> element around the
\<label\> element. Tapestry's style classes are all prefixed with "t-" (for
"Tapestry") so that
- they don't conflict with anything your own web designers may decide to use.
In any case, this CSS rule forces the label to be 200 pixels wide (rather than
- the default, which is 10% of the overall page width).
-
-[address-v4.png] Corrected width on labels
-
We can also customize the options in the drop down list. All we have to do
is add some more entries to the message catalog matching the enum names to the
desired labels.
Update CreateAddress.properties and add:
@@ -344,7 +332,7 @@
----
Notice that we don't have to include an option for MISS, because that is
converted to "Miss" anyway. You might just want to include it for
- consistencie's sake ... the point is, each option label is searched for
seperately.
+ sake of consistency ... the point is, each option label is searched for
seperately.
Lastly, the default label on the submit button is "Create/Update"
(BeanEditForm doesn't know how it is being used). Let's change that to "Create
Address".
@@ -370,7 +358,7 @@
label will go in the message catalog.
In Tapestry, when binding a parameter, the value you provide may include a
prefix. The prefix guides Tapestry in how to interpret the rest of the
- the parameter value ... is it the name of a property? The id of a
component? A message key? Most fields have a default prefix, usually "prop:"
that
+ the parameter value ... is it the name of a property? The id of a
component? A message key? Most fields have a default prefix, usually "prop:",
that
is used when you fail to provide one (this helps to make the templates as
terse as possible).
Here we want to reference a message from the catalog, so we use the
"message:" prefix:
@@ -402,7 +390,7 @@
@Validate("required")
public String getFirstName()
{
- return _firstName;
+ return firstName;
}
----
@@ -416,14 +404,15 @@
[address-v6.png] Form with client side validations visible
- <<TODO: Update this with a more recent image showing the new pop-up bubbles
style of displaying client-side error messages.>>
+ This is a shot just after hitting the submit button; all the fields have
been validated and pop-up error bubbles are displayed. This looks a bit
cluttered, but all the bubbles, except for the one
+ for the focus field (the field the user is actively typing into), will fade
out after a moment. As you tab from field to field, Tapestry will validate
your input and briefly display the error bubble. And <all> of this
+ is taking place on the client side, without any communication with the
application.
- What this picture doesn't show is that the validation occurred entirely <on
the client side>, using JavaScript. Look what's happened: a summary of <all> the
- errors in the form has scrolled down at the top of the form, and each form
in error has been highlighted (it's a bit subtle) and marked with a red "X".
Further, the label
+ Each field in error has been highlighted (it's a bit subtle) and marked with
a red "X". Further, the label
for each of the fields has also been highlighted in red, to even more
clearly identify what's in error. The cursor has also been moved to the first
field
that's in error.
- Again, all of this validation occured entirely on the client side, no
request was sent to the server. However, once all the errors are corrected,
and the form does submit,
+ Once all the errors are corrected, and the form does submit,
all validations are performed on the server side as well (just in
case the client has JavaScript disabled).
@@ -434,7 +423,7 @@
@Validate("required,regexp=\\d{5}(-\\d{4})?")
public String getZip()
{
- return _zip;
+ return zip;
}
----
@@ -442,7 +431,12 @@
[address-v7.png] Regexp validation
- That's the right behavior, but it's the wrong message. Fortunately, it's
easy to customize validation messages. All we need to know is the name of the
+ This is what you'll see after typing "abc" and tabbing out of the field,
then tabbing back in. It's a little hard to capture all the animation
+ effects in a still photo.
+
+ In any case, that's the right validation behavior, but it's the wrong
message. Your users are not going to know or care about regular expressions.
+
+ Fortunately, it's easy to customize validation messages. All we need to
know is the name of the
property ("zip") and the name of the validator ("regexp"). We can then put
an entry into the CreateAddress message catalog:
----
@@ -456,18 +450,18 @@
This trick isn't limited to just the regexp validator, it works equally well
with <any> validator.
Let's go one step further. Turns out, we can move the
- regexp pattern to the message catalog as well. If you only provide the name
of the validator in the @Validator annotation, Tapestry will search the
containing
+ regexp pattern to the message catalog as well. If you only provide the name
of the validator in the @Validate annotation, Tapestry will search the
containing
page's message catalog of the constraint value, as well as the validation
message. The constraint value for the regexp validator is the regular
expression to match against.
----
@Validate("required,regexp")
public String getZip()
{
- return _zip;
+ return zip;
}
----
- Now, just put the value into the CreateAddress message catalog:
+ Now, just put the regular expression into the CreateAddress message catalog:
----
zip-regexp=\\d{5}(-\\d{4})?
Modified: tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms2.apt
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms2.apt?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms2.apt
(original)
+++ tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/forms2.apt Tue May
6 09:47:43 2008
@@ -4,4 +4,470 @@
Chapter 5: Forms in Tapestry (Part Two)
- <Still working on it!>
+ 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;
+
[EMAIL PROTECTED]
+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.annotations.InjectPage;
+import org.apache.tapestry.annotations.Property;
+import org.apache.tapestry.hibernate.annotations.CommitAfter;
+import org.apache.tapestry.ioc.annotations.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();
+ }
+----
+
+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 event 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}.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Modified: tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/hilo.apt
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/hilo.apt?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/hilo.apt (original)
+++ tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/apt/hilo.apt Tue May
6 09:47:43 2008
@@ -77,11 +77,11 @@
public class Guess
{
- private int _target;
+ private int target;
Object initialize(int target)
{
- _target = target;
+ this.target = target;
return this;
}
@@ -92,14 +92,11 @@
method is package private, not public;
it is only expected to be invoked from the Index page (as we'll see in a
moment), so there's no need to make it public. Later we'll see that
there's more initialization to be done
- than just storing a value into the _target instance variable (which is why
we don't simply name the method setTarget() ).
-
- <The naming convention, using leading underscores for fields, is NOT a
requirement of Tapestry; that's
- just my personal coding style.>
+ than just storing a value into the target instance variable (which is why we
don't simply name the method setTarget() ).
Now we can move back to the Index page. What we want is to have the
ActionLink component invoke a method on the Index page. We can then generate
a random target number. We'll tell the Guess page what the target number is
and then make sure that it is the Guess page, and not the
- Index page,
+ Index page,
that renders the response into the user's web browser. That's actually
quite a few concepts to take in all at once.
Let's start with the code, and break it down:
@@ -115,16 +112,16 @@
public class Start
{
- private final Random _random = new Random();
+ private final Random random = new Random();
@InjectPage
- private Guess _guess;
+ private Guess guess;
Object onAction()
{
- int target = _random.nextInt(10) + 1;
+ int target = random.nextInt(10) + 1;
- return _guess.initialize(target);
+ return guess.initialize(target);
}
}
----
@@ -142,7 +139,7 @@
Injection can be a somewhat nebulous concept. In terms of Tapestry, it
means that some cooperating object needed by one class is provided to it. The
other
object is often referred to as a "dependency"; in this case, the Index page
<depends on> the Guess page to be fully functional, and an instance of the Guess
- page is made available as the _guess instance variable. The Index page
doesn't, and can't, <create> the Guess page, it can only
+ page is made available as the guess instance variable. The Index page
doesn't, and can't, <create> the Guess page, it can only
advertise, to Tapestry, that it needs the Guess page. Tapestry will take
care of the rest.
Let's see what we do with this injected page. It's used inside onAction().
You might guess that this method is invoked when the link ("Start guessing") is
clicked. But
@@ -204,7 +201,7 @@
---
public int getTarget()
{
- return _target;
+ return target;
}
---
@@ -248,7 +245,7 @@
The same page instance will be used for request after request.
So, inside the action request, the code inside the onAction() event handler
method <did> call the
- initialize() method, and a value between 1 and 10 was stored in the _target
instance variable. But
+ initialize() method, and a value between 1 and 10 was stored in the target
instance variable. But
at the end of that request, the value was lost, and in the subsequent render
request for the Guess page,
the value was zero.
@@ -257,7 +254,7 @@
----
@Persist
- private int _target;
+ private int target;
----
Now we can use the browser back button to return to the Start page, and
click the link again.
@@ -317,7 +314,7 @@
---
@Property
- private int _guess;
+ private int guess;
---
Tapestry will automatically create the methods needed so that the guess
property (it's smart
@@ -340,16 +337,16 @@
---
@Persist
@Property
- private String _message;
+ private String message;
String onActionFromLink(int guess)
{
- if (guess == _target) return "GameOver";
+ if (guess == target) return "GameOver";
- if (guess < _target)
- _message = String.format("%d is too low.", guess);
+ if (guess < target)
+ message = String.format("%d is too low.", guess);
else
- _message = String.format("%d is too high.", guess);
+ message = String.format("%d is too high.", guess);
return null;
}
@@ -361,7 +358,7 @@
method parameter.
We can then compare the guess from the user to the secret target number.
- Notice how Tapestry adapts to the return value. Here is may be null ...
Tapestry interprets that
+ Notice how Tapestry adapts to the return value. Here it may be null ...
Tapestry interprets that
as "stay on the same page". You may also return a string ("GameOver");
Tapestry interprets <that>
as the name of the page to render the response.
@@ -421,17 +418,17 @@
---
@Persist
@Property
- private int _count;
+ private int count;
---
- Next we modified initialize() to ensure that _count is set to 0. This is a
safety
+ Next we modified initialize() to ensure that count is set to 0. This is a
safety
precaution in case we add logic to play the game again.
---
Object initialize(int target)
{
- _target = target;
- _count = 0;
+ this.target = target;
+ this.count = 0;
return this;
}
@@ -444,14 +441,14 @@
---
Object onActionFromLink(int guess)
{
- _count++;
+ count++;
- if (guess == _target) return _gameOver.initialize(_count);
+ if (guess == target) return gameOver.initialize(count);
- if (guess < _target)
- _message = String.format("%d is too low.", guess);
+ if (guess < target)
+ message = String.format("%d is too low.", guess);
else
- _message = String.format("%d is too high.", guess);
+ message = String.format("%d is too high.", guess);
return null;
}
@@ -474,11 +471,11 @@
{
@Persist
@Property
- private int _count;
+ private int count;
Object initialize(int count)
{
- _count = count;
+ this.count = count;
return this;
}
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v6.png
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v6.png?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
Binary files - no diff available.
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v7.png
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v7.png?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
Binary files - no diff available.
Modified:
tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v8.png
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-tutorial1/src/site/resources/address-v8.png?rev=653834&r1=653833&r2=653834&view=diff
==============================================================================
Binary files - no diff available.