You are on page 1of 130

Enjoying Web Development

with Tapestry
Copyright © 2005-2006
Ka Iok 'Kent' Tong

Publisher: TipTec Development


Author's email: freemant2000@yahoo.com
Book website: http://www.agileskills2.org
Notice: All rights reserved. No part of this publication may be reproduced, stored in a retrieval system or
transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or
otherwise, without the prior written permission of the publisher.
Edition: Third edition 2006
Enjoying Web Development with Tapestry 3

Foreword

How to create AJAX web-based application easily?


If you'd like to create AJAX web-based applications easily, then this book is for you. More importantly, it shows you
how to do that with joy and feel good about your own work! You don't need to know servlet or JSP while your
productivity will be much higher than using servlet or JSP directly. This is possible because we're going to use a
library called "Tapestry" that makes complicated stuff simple and elegant.
How does it do that? First, it allows the web designer to work on the static contents and design of a page while
allowing the developer to work on the dynamic contents of that page without stepping on each other's toes;
Second, it allows developers to work with high level concepts such as objects and properties instead of HTTP
URLs, query parameters or HTML string values; Third, it comes with powerful components such as calendar, tree
and data grid and it allows you to create your own components for reuse in your own project.
However, don't take our word for it! This book will quickly walk you through real world use cases to show you how
to use Tapestry and leave it up to you to judge. It is best said by Geoff Longman, a Tapestry expert and the creator
of a famous Eclipse plugin for Tapestry, "this is material designed to get your feet dirty *right away* and not really
intended to debate whether Tapestry is right, wrong, the best, or the worst framework for you, me, or my brother."

How this book can help you learn Tapestry?


• It has a tutorial style that walks you through in a step-by-step manner.
• It is concise. There is no lengthy, abstract description.
• Many diagrams are used to show the flow of processing and high level concepts so that you get a whole picture
of what's happening.
• Free sample chapters are available on http://www.agileskills2.org. You can judge it yourself.
4 Enjoying Web Development with Tapestry

Unique contents in this book


This book covers the following topics not found in other books on Tapestry:
• How to work with Tapestry 4.1, including the AJAX features.
• How to use FireBug to debug AJAX effects.
• How to do test-driven development (TDD) with Tapestry and HtmlUnit.
• How to integrate Struts with Tapestry.
• How to use PostgreSQL and DBCP connection pooling with Tapestry.
• How to apply the four layered architecture in a Tapestry application.

Target audience and prerequisites


This book is suitable for those learning how to develop web-based applications and those who are experienced in
servlet, JSP, Struts and would like to see if Tapestry can make their jobs easier.
In order to understand what's in the book, you need to know Java, HTML and some simple SQL. However, you do
NOT need to know servlet, JSP, Tomcat, HtmlUnit or Hibernate. The chapter on Struts integration does assume
that you know Struts. If not, you may skip that chapter.

Acknowledgments
I'd like to thank:
• Howard Lewis Ship for creating Tapestry.
• Mike Bowler, the creator of HtmlUnit, for reviewing the chapter on HtmlUnit.
• Helena Lei for proofreading this book.
• Eugenia Chan Peng U for doing book cover and layout design.
Enjoying Web Development with Tapestry 5

Table of Contents
Foreword......................................................................................................................................3
How to create AJAX web-based application easily?................................................................3
How this book can help you learn Tapestry?...........................................................................3
Unique contents in this book....................................................................................................4
Target audience and prerequisites..........................................................................................4
Acknowledgments....................................................................................................................4
Chapter 1 Getting Started with Tapestry....................................................................................11
What's in this chapter?..........................................................................................................12
Developing a Hello World application with Tapestry..............................................................12
Installing Eclipse....................................................................................................................12
Installing Tomcat....................................................................................................................12
Installing Tapestry..................................................................................................................14
Creating a Hello Word application.........................................................................................15
Generating dynamic content..................................................................................................21
Disabling caching in Tapestry................................................................................................24
Making changes to Java code take effect..............................................................................25
Other ways to set the value...................................................................................................25
Debugging a Tapestry application.........................................................................................26
Summary...............................................................................................................................29
Chapter 2 Using Forms..............................................................................................................31
What's in this chapter?..........................................................................................................32
Developing a stock quote application....................................................................................32
Creating the result page........................................................................................................37
Displaying the Result page in the listener..............................................................................37
Easier way to get access to another page.............................................................................40
Instance variables may breach security.................................................................................40
Using Java annotations to inject pages and properties.........................................................44
Using implicit components.....................................................................................................46
Using a combo box................................................................................................................47
Using the DatePicker.............................................................................................................48
Using the API doc..................................................................................................................51
Using the component reference.............................................................................................52
Summary...............................................................................................................................53
Chapter 3 Validating Input.........................................................................................................55
What's in this chapter?..........................................................................................................56
Postage calculator.................................................................................................................56
Accepting integer input..........................................................................................................57
What if the input is invalid?....................................................................................................60
Using validators.....................................................................................................................66
What if the translator can't translate the string?....................................................................69
Handling null input.................................................................................................................69
Setting the display message..................................................................................................70
Using a FieldLabel.................................................................................................................71
Creating your own validator...................................................................................................72
Showing all the errors............................................................................................................74
Using informal parameters.....................................................................................................77
Performing validation using Javascript..................................................................................77
6 Enjoying Web Development with Tapestry

Errors that don't belong to any input field..............................................................................78


Validation for a DatePicker component and a TextArea component.....................................79
Other validators.....................................................................................................................81
Summary...............................................................................................................................82
Chapter 4 Creating an e-Shop...................................................................................................83
What's in this chapter?..........................................................................................................84
Creating an e-shop................................................................................................................84
Showing the product details...................................................................................................86
Setting the packages to look for page classes......................................................................91
Implementing a shopping cart................................................................................................92
Distinguishing which button was clicked................................................................................93
Adding a product to the shopping cart...................................................................................96
How Tomcat and the browser maintain the session............................................................103
Another way to maintain a session......................................................................................107
Unified method to let a page remember its data..................................................................108
Storing persistent property into the session.........................................................................112
Implementing checkout........................................................................................................112
Letting the Confirm page protect itself.................................................................................123
Calling back a page that takes parameters.........................................................................125
Passwords are exposed.......................................................................................................127
Implementing logout............................................................................................................128
Summary.............................................................................................................................129
Chapter 5 Creating Custom Components................................................................................131
What's in this chapter..........................................................................................................132
Displaying a copyright notice on all pages...........................................................................132
Should Copyright.html be a complete page?.......................................................................135
Stating that the body will be discarded................................................................................135
Creating a Box component..................................................................................................136
Customizing the Box component using informal parameters...............................................139
Customizing the Copyright component using formal parameters........................................140
Making a parameter optional...............................................................................................141
Using annotation to declare a parameter.............................................................................141
Looking for the component class in the specified packages................................................142
Creating a component that takes input................................................................................142
Documenting a component..................................................................................................146
Reusing components in another project..............................................................................147
Summary.............................................................................................................................154
Chapter 6 Supporting Other Languages..................................................................................157
What's in this chapter..........................................................................................................158
A sample application............................................................................................................158
Supporting Chinese.............................................................................................................158
How to internationalize an implicit component.....................................................................163
An easier way to insert a message......................................................................................164
Internationalize the page content.........................................................................................164
Letting the user change the locale.......................................................................................166
Selecting the current locale in the combo box.....................................................................175
Localizing the full stop.........................................................................................................176
Displaying a logo..................................................................................................................178
Localizing the logo...............................................................................................................184
Putting the images into other places....................................................................................186
Enjoying Web Development with Tapestry 7

Creating a license page.......................................................................................................187


Observing the output encoding............................................................................................191
Creating a Logo component................................................................................................191
Setting the ALT attribute of the logo....................................................................................193
Packaging the Logo component..........................................................................................194
Automating the package process........................................................................................196
How can the browser access the GIF files?.........................................................................198
Summary.............................................................................................................................198
Chapter 7 Using the Table Component...................................................................................201
What's in this chapter?........................................................................................................202
Creating a phone book........................................................................................................202
List the entries in alternating colors.....................................................................................206
Storing the styles in a file.....................................................................................................209
Sorting the entries................................................................................................................212
Customizing how to get the cell value..................................................................................217
Customizing the column titles..............................................................................................218
Making the styles work again...............................................................................................220
Making the first name a link.................................................................................................222
Listing more entries.............................................................................................................225
Tuning the performance of the Table component................................................................226
Session is used....................................................................................................................234
Caching the entries..............................................................................................................234
Adding a delete button.........................................................................................................236
Sort by Delete?....................................................................................................................244
Moving the page links to the bottom....................................................................................244
Summary.............................................................................................................................248
Chapter 8 Handling File Downloads and Uploads...................................................................251
What's in this chapter?........................................................................................................252
Downloading a photo...........................................................................................................252
Using a service....................................................................................................................256
Generating the link to call the service..................................................................................263
Displaying a photo...............................................................................................................266
Using friendly URL...............................................................................................................267
Downloading a photo using a form......................................................................................273
Telling the size of the download..........................................................................................274
Uploading a photo................................................................................................................274
Summary.............................................................................................................................278
Chapter 9 Providing a Common Layout...................................................................................281
What's in this chapter?........................................................................................................282
Providing a common layout..................................................................................................282
Setting the page title............................................................................................................287
Disabling the link for the current page.................................................................................288
Using a header....................................................................................................................289
Summary.............................................................................................................................292
Chapter 10 Using Javascript....................................................................................................293
What's in this chapter?........................................................................................................294
Are you sure to delete it?.....................................................................................................294
Reusing the script................................................................................................................296
Generating a unique function name.....................................................................................298
Encapsulating the use of scripts in a component................................................................302
8 Enjoying Web Development with Tapestry

Summary.............................................................................................................................304
Chapter 11 Building Interactive Forms with AJAX...................................................................307
What's in this chapter?........................................................................................................308
A sample AJAX application..................................................................................................308
Creating the Home page......................................................................................................310
Loading a single customer...................................................................................................311
Using a DataSet to store the Customer objects...................................................................316
Listing all the customers......................................................................................................321
Implementing the Edit function............................................................................................324
Skipping validation for form cancellation.............................................................................331
Refreshing the current row only...........................................................................................334
Refreshing the city list when the country is changed...........................................................337
Preventing multiple forms....................................................................................................340
Implementing the Delete function........................................................................................341
Implementing the Add and the Commit function..................................................................342
Using FireBug......................................................................................................................344
Summary.............................................................................................................................348
Chapter 12 Test Driven Development with HtmlUnit................................................................349
What's in this chapter?........................................................................................................350
Developing a calculator using test driven development.......................................................350
Setting up HtmlUnit..............................................................................................................351
Setting up the web application context................................................................................352
Implementing the add operation..........................................................................................354
Providing a list of operations................................................................................................361
Using the setUp() method....................................................................................................362
Implementing minus.............................................................................................................364
Implementing the History link...............................................................................................365
Fixing the problems revealed by manual inspection............................................................371
Running all the tests............................................................................................................373
Implementing validation.......................................................................................................373
Implementing the Help link..................................................................................................375
Refactoring..........................................................................................................................379
Summary.............................................................................................................................381
Chapter 13 Database and Concurrency Issues.......................................................................383
What's in this chapter?........................................................................................................384
Developing a banking application........................................................................................384
Setting up PostgreSQL........................................................................................................384
Hard coding some bank accounts.......................................................................................392
Transferring some money....................................................................................................393
Using a transaction..............................................................................................................396
Connection pooling..............................................................................................................398
Concurrency issues.............................................................................................................402
Long transaction..................................................................................................................416
Dividing the application into layers.......................................................................................426
Summary.............................................................................................................................434
Chapter 14 Using Hibernate.....................................................................................................435
What's in this chapter?........................................................................................................436
Setting up Hibernate............................................................................................................436
Adding an id not exposed to the user..................................................................................439
Specifying the mapping.......................................................................................................440
Enjoying Web Development with Tapestry 9

Accessing objects with Hibernate........................................................................................440


Updating the database schema...........................................................................................443
Hard coding some customers programmatically..................................................................443
Do NOT access objects loaded after its session is closed..................................................444
Editing a Customer object....................................................................................................449
Adding a Customer object...................................................................................................451
Deleting a Customer object.................................................................................................453
Handling concurrency issues...............................................................................................454
Separating UI code and database code..............................................................................456
Summary.............................................................................................................................460
Chapter 15 Integrating with Struts............................................................................................461
What's in this chapter?........................................................................................................462
Integrating Tapestry with Struts...........................................................................................462
Running a sample Struts application...................................................................................462
Rewriting the Logon page in Tapestry.................................................................................468
Invoking a Tapestry page from JSP and invoking a Struts action from Tapestry................470
Implementing rendering part of the Logon page..................................................................472
Implementing the rewinding part of the Logon page............................................................477
Rewriting a JSP include file as a Tapestry component........................................................479
Using localized messages...................................................................................................481
Supporting an alternate message resource bundle.............................................................485
Summary.............................................................................................................................487
References...............................................................................................................................489
Alphabetical Index....................................................................................................................490
11

Chapter 1
Chapter 1 Getting Started with Tapestry
12 Chapter 1 Getting Started with Tapestry

What's in this chapter?


In this chapter you'll learn to how to setup a development environment and develop a Hello World application with
Tapestry.

Developing a Hello World application with Tapestry


Suppose that you'd like to develop an application like this:

Installing Eclipse
First, you need to make sure you have Eclipse installed. If not, go to http://www.eclipse.org to download the Eclipse
platform (e.g., eclipse-platform-3.1-win32.zip) and the Eclipse Java Development Tool (eclipse-JDT-3.1.zip). Unzip both
into c:\eclipse. Then, create a shortcut to run "c:\eclipse\eclipse -data c:\workspace". This way, it will store your projects
under the c:\workspace folder. To see if it's working, run it and then you should be able to switch to the Java
perspective:

Installing Tomcat
Next, you need to install Tomcat. Go to http://jakarta.apache.org to download a binary package of Tomcat. Download
the zip version instead of the Windows exe version. Suppose that it is jakarta-tomcat-5.5.7.zip. Unzip it into a folder say
c:\tomcat. If you're going to use Tomcat 5.5 with JDK 1.4 or 1.3, you also need to download the compat package and
unzip it into c:\tomcat.
Before you can run it, make sure the environment variable JAVA_HOME is defined to point to your JDK folder (e.g.,
C:\Program Files\Java\jdk1.5.0_02):
Getting Started with Tapestry 13

If you don't have it, define it now. Now, run open a command line, change to c:\tomcat\bin and then run startup.bat. If it
is working, you should see:

Open a browser and go to http://localhost:8080 and you should see:


14 Chapter 1 Getting Started with Tapestry

Let's shut it down by changing to c:\tomcat\bin and running shutdown.bat.

Installing Tapestry
Next, go to http://tapestry.apache.org to download a binary package of Tapestry (e.g., tapestry-project-4.1.1-bin.zip)
and unzip it into a folder say c:\tapestry. It contains a quite some jar files in different sub-folders (each folder is a
"module"):

It's quite difficult to use these jar files as they're in different sub-folders. To solve the problem, you'll copy them available
in one folder. Create a folder c:\tapestry\jars, then choose "Search" on the Start Menu to search for files named *.jar
inside c:\tapestry. The result should be like:
Getting Started with Tapestry 15

Copy all of the files and paste them into c:\tapestry\jars. That's it. You can't run it yet because Tapestry is a library, not
an application.

Creating a Hello Word application


Now, create a new Java project. Name it "HelloWorld" and make sure it uses a separate output folder:

Set the output folder as shown below:


16 Chapter 1 Getting Started with Tapestry

Finally, you should see the project structure:

The bin folder is useless so you can delete it. Then right click the project and choose "Properties", choose "Java Build
Path" on the left hand side, choose the "Libraries" tab:

Click "Add Library" and choose "User Library":


Getting Started with Tapestry 17

If you see a "Tapestry Framework" library as shown above, Do NOT choose it! It is Tapestry 3.0 coming with Spindle.
Click "Next":

Click "User Libraries" to define your own Tapestry library:

Click "New" to define a new one and enter "Tapestry 4" as the name of the library:

Click "Add JARs", browse to c:\tapestry\jars and add all the jar files there:
18 Chapter 1 Getting Started with Tapestry

Then close all the dialog boxes. Next, create a new file Home.html in context\WEB-INF in the project. It will act as the
home page of your application. Next, use DreamWeaver, FrontPage or your favorite web page editor to modify it. But
where is it located? It is in context/WEB-INF in your project and the whole project is in c:\workspace:

So, its full path is c:\workspace\HelloWorld\context\WEB-INF\Home.html. Knowing its full path, you can modify it to look
like:

If you'd like, you can edit the HTML code directly in Eclipse:

Next, create a new file Home.page in the same folder as Home.html with the following content:
<?xml version="1.0"?>
<!DOCTYPE page-specification PUBLIC
"-//Apache Software Foundation//Tapestry Specification 4.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<page-specification>
</page-specification>
Next, you need to make the Tapestry jar files available to this application. To do that, copy all the tapestry jar files in
c:\tapestry\jars into c:\tomcat\shared\lib:
Getting Started with Tapestry 19

This way, they will be available to all applications running in Tomcat, including your own. Next, create a file web.xml in
context\WEB-INF with the following content:
<?xml version="1.0"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/TR/xmlschema-1/"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>HelloWorld</display-name>
<servlet>
<servlet-name>HelloWorld</servlet-name>
<servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>HelloWorld</servlet-name>
<url-pattern>/app</url-pattern>
</servlet-mapping>
</web-app>
You can ignore its meaning for now.
To make this application run in Tomcat, you must register it with Tomcat. To do that, create a file HelloWorld.xml in
c:\tomcat\conf\Catalina\localhost:
20 Chapter 1 Getting Started with Tapestry

This file is called the "context descriptor".


It tells Tomcat that you have a web
application (yes, a web application is
called a "context").

HelloWorld.xml
<Context
docBase="c:/workspace/HelloWorld/context"
path="/HelloWorld"/>

This is called the "context path". It Tell Tomcat that the application's
is telling Tomcat that users should files can be found in
access this application using c:\workspace\HelloWorld\context.
http://localhost:8080/HelloWorld.

Actually, this is no longer used in


Tomcat 5.5. In Tomcat 5.5, it uses
the filename of the context HelloWorld.xml /HelloWorld
descriptor to determine the path:
Foo.xml /Foo

Bar.xml /Bar

Now, start Tomcat (by running startup.bat). To run your application, run a browser and try to go to
http://localhost:8080/HelloWorld/app?service=page&page=Home. You should see:

What does this URL mean? It is interpreted this way:


Getting Started with Tapestry 21

<Context
docBase="c:/workspace/HelloWorld/context"
path="/HelloWorld"/>

Context path Please show a page The page to show is named


"Home"

http://localhost:8080/HelloWorld/app?service=page&page=Home

It represents your Tapestry


application
In fact, if you don't request any particular service, your Tapestry application will show the Home page by default. So,
you can just enter http://localhost:8080/HelloWorld/app and the same page will be displayed.

Generating dynamic content


Displaying "Hello World" is not particularly interesting. Next, you'll generate the message dynamically in Java. First,
modify Home.html as:
<html>
Hello <span>world</span>!
</html>
<span> is just a regular HTML element. It is used to enclose a section of HTML code. Next, add an attribute to this
span:
<html>
Hello <span jwcid="subject">world</span>!
</html>
"jwcid" stands for "Java Web Component id". It is a Tapestry thing. You are saying that this span is a Tapestry
component. The id of the component is "subject". What is the effect of marking it as a component? When Tapestry
displays (i.e., renders) the Home page, it will basically output the code in Home.html (check the diagram below).
However, when it finds that this span is a Tapestry component, it will create this component and ask it to generate any
HTML code that it likes. Tapestry will use whatever the component generates to completely replace the <span>
element. That is, the process is like:

1: Look, just regular HTML code


Tapestry 2: Output it
<html>
Hello John !
3: Look, we have a </html>
component here 7: Regular 8: Output it
HTML code
again
<html>
Hello <span jwcid="subject">world</span> !
</html>

4: Create the component. It 5: Generate HTML for


is just a Java object. yourself

Component
6: Output HTML code for
"subject" itself such as "John"

However, in order for Tapestry to create the component, it needs to know what type of component it is. Therefore, you
need specify its type. This is done in the Home.page file:
22 Chapter 1 Getting Started with Tapestry

We're talking about the It is an Insert component.


component named It will output some plain
"subject" here text as the HTML.

<page-specification>
<component id="subject" type="Insert">
<binding name="value" value="ognl:greetingSubject"/>
The whole thing will be
</component>
evaluated as the value
</page-specification>
of the "value"
parameter and output
Each Insert component has a few This is the "prefix" This is an OGNL
"parameters". In particular, it will of the expression. It expression. But what
evaluate its parameter named is saying that what does it mean?
"value" and output the result as the is following is an
plain text to output. "OGNL" expression.
What is OGNL? It stands for Object Graph Navigation Language. What does this particular expression
"greetingSubject" mean? Before the page is rendered, actually, Tapestry will first create a Java object to represent the
page. What is the Java class of this object? By default, it is the class org.apache.tapestry.html.BasePage provided by
Tapestry. Let's call this object the page object. Then it will create all the components listed here in Home.page and put
them into the page object (imagine a page object contains an array to store the components). In this case there is only
one Insert component named "subject":
Page object
(a BasePage
object)

Component
"subject"

...

It is this page object that is reading Home.html and generating the output. As mentioned before, when it sees the
"subject" component in Home.html, it will ask the "subject" component to output HTML code for itself (see the diagram
below). Then this component will evaluate the expression "greetingSubject", which means that it will call a
getGreetingSubject() method on the page object and expect that the result to be some plain text for it to output:
Page object
(a BasePage 3: Result is
"John"
object)
1: Generate
HTML for
yourself
2: call the
getGreetingSubject()
method

Component
"subject" 4: Output
"John"

...

As you can see, when displaying the Home page, Home.html is acting as a template. So it is called a "template" in
Tapestry. Home.page is specifying the Java class for the page object and lists the components to be created in the
Getting Started with Tapestry 23

page object, so it is called a "page specification".


Since BasePage is a class coming with Tapestry, it doesn't have such a getGreetingSubject() method. So, you need to
create a subclass from it. Let's call it Home (because it is used along with Home.html and Home.page) and put it into
package com.ttdev.helloworld:

Define a getGreetingSubject() method that returns a string:

Why it is an abstract class? For the moment just remember


that it must be abstract. Tapestry will create a subclass for it
automatically.

package com.ttdev.helloworld;

import org.apache.tapestry.html.BasePage;

public abstract class Home extends BasePage {


public String getGreetingSubject() {
return "John";
}
}
Modify Home.page to use your subclass instead of the default BasePage:
<page-specification class="com.ttdev.helloworld.Home">
<component id="subject" type="Insert">
<binding name="value" value="greetingSubject"/>
</component>
</page-specification>
Now, you are about to run the application again. If you run it now, you'll still see "Hello World", not "Hello John":

Why? You have made changes to your Home.html, Home.page and Home.java files, but Tapestry will cache HTML
files and .page files in memory once they're read. In addition, Tomcat will cache Java class files once they're read. So,
these changes won't take effect. To make them take effect, you need to reload the application. To do that, go to
http://localhost:8080 and choose "Tomcat Manager", but it requires you to enter a user name and password:
24 Chapter 1 Getting Started with Tapestry

Therefore, you need to create a user account first. To do that, edit c:\tomcat\conf\tomcat-users.xml:
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="tomcat"/>
<role rolename="role1"/>
<role rolename="manager"/>
<user username="tomcat" password="tomcat" roles="tomcat"/>
<user username="role1" password="tomcat" roles="role1"/>
<user username="both" password="tomcat" roles="tomcat,role1"/>
<user username="tomcatAdmin" password="123456" roles="manager"/>
</tomcat-users>
Then, restart Tomcat so that it can see the user account. Then, using this account to access the Tomcat Manager:

To restart the application, just click "Reload" for /HelloWorld. However, as you have already restarted Tomcat, all the
applications have been reloaded in the process. Anyway, run the application and you should see the changes taking
effect.

Disabling caching in Tapestry


It is troublesome to reload the application before each test run. To solve the first part of the problem, you can tell
Tapestry to not to cache HTML and .page files. To do that, you need to set a JVM system property
org.apache.tapestry.disable-caching to true. If you were starting the JVM yourself, you would run it like:
java -Dorg.apache.tapestry.disable-caching=true ...
However, as the JVM is started by Tomcat, you need to setup a environment variable JAVA_OPTS before running
startup.bat:
Getting Started with Tapestry 25

Now, you can change say Home.html and the change will take effect immediately. For example, modify it as:
<html>
Hello <span jwcid="subject">world</span>! Good!
</html>
Then run the application:

Now, let's change it back.

Making changes to Java code take effect


What about changes to Java code? If you modify Home.java like:
public abstract class Home extends BasePage {
public String getGreetingSubject() {
return "JohnPaul";
}
}
Run the application again. You will still see "Hello John". To solve this problem, you can tell Tomcat to reload the whole
application if any of its classes is changed. This is done by marking the application as reloadable in the context
descriptor (c:\tomcat\conf\Catalina\localhost\HelloWorld.xml):
<Context
docBase="c:/workspace/HelloWorld/context"
path="/HelloWorld"
reloadable="true"/>

Now, reload the application so that Tomcat reads the context descriptor (and learns that the application is now
reloadable). Then try to change the Java code again and reload the page. It will work (it may take a few seconds
though, so be patient).

Other ways to set the value


Instead of calling getGreetingSubject(), there are other ways to achieve the same output effect. For example, modify
Home.page:
Now run it again it should say "Hello Paul". If not, the change is cached by Windows. In that case, you can make any
26 Chapter 1 Getting Started with Tapestry

<page-specification>
<component id="subject" type="Insert">
<binding name="value" value="ognl:'Paul'"/>
</component>
</page-specification>
It is still an OGNL expression, but
the string is quoted, so it is a
string constant.
change the Home.page file again such as adding a space and deleting it again. Finally, save it again. Then the change
should take effect.
There is yet another alternative:
<page-specification>
<component id="subject" type="Insert">
<binding name="value" value="literal:Judy"/>
</component>
</page-specification>
What is following is no It is just a string
longer an OGNL literal. No need to
expression, but a string quote it.
literal.
In fact, you don't have to provide a prefix:
<page-specification>
<component id="subject" type="Insert">
<binding name="value" value="greetingSubject"/>
</component>
</page-specification>
No prefix is specified. In that case the Tapestry
will assume that it is an OGNL expression as if
ognl prefix was used.
Debugging a Tapestry application
To debug your application in Eclipse, you need to set two more environment variables for Tomcat and launch it in a
special way:

Note that you're now launching it using catalina.bat instead of startup.bat. This way Tomcat will run the JVM in debug
mode so that the JVM will listen for connections on port 8000. Later you'll tell Eclipse to connect to this port.
Now, set a breakpoint here:
Getting Started with Tapestry 27

Change the OGNL expression back:


<page-specification class="com.ttdev.helloworld.Home">
<component id="subject" type="Insert">
<binding name="value" value="greetingSubject"/>
</component>
</page-specification>
Choose "Debug":

The following window will appear:

Right click "Remote Java Application" and choose "New". Browse to select your HelloWorld project and make sure the
port is 8000:
28 Chapter 1 Getting Started with Tapestry

Click "Debug" to connect to the JVM in Tomcat. Now go to the browser to load the page again. Eclipse will stop at the
breakpoint:

Then you can step through the program, check the variables and whatever. To stop the debug session, choose the
process and click the Stop icon:
Getting Started with Tapestry 29

Having to set all those environment variables every time is not fun. So, you may create a batch file
c:\tomcat\bin\tap.bat:
set JAVA_OPTS="-Dorg.apache.tapestry.disable-caching=true"
set JPDA_ADDRESS=8000
set JPDA_TRANSPORT=dt_socket
catalina jpda start
Then in the future you can just run it to start Tomcat.

Summary
To develop a Tapestry, you can install Tomcat and Eclipse.
To install Tapestry, just unzip it into a folder. It is just a bunch of jar files. Copy the jar files into Tomcat's shared lib
folder so that they are available to all web applications.
To register a web application with Tomcat, you need to create a web.xml file and a context descriptor to tell Tomcat
where the application's files can be found.
To use a Tapestry application, you can enter a URL to ask Tapestry to display a certain page. If you don't specify
anything particular in the URL, it will display the Home page.
When displaying a certain page, Tapestry will check the .page file (page specification) to find out the Java class of the
page object and create it. The page object will read its HTML file (the template) and basically output what's in the HTML
file. But if there is a component in the HTML file, it will check the .page file to find out the type of the component, then
create the component (a Java object) and ask it to output HTML for itself.
An Insert component will output some plain text as HTML code. It evaluates its "value" parameter which is bound to a
certain expression. The expression may have an OGNL expression (with an ognl prefix) or a string literal (with a literal
prefix). If no prefix is specified, Tapestry will always assume that it is an OGNL expression. For an OGNL expression, if
the expression is "foo", it will call getFoo() on the page object and expect the return value to be a string to output.
If you make changes to your HTML file or .page files, your application won't see the changes by default because
Tapestry is caching them. To solve the problem, set a JVM parameter to disable the cache. Similarly, if you make
changes to your Java class, your application won't see the changes because Tomcat is caching them. To solve the
problem, mark the application as reloadable in the context descriptor so that the application is reloaded automatically if
any of its Java code is changed.
To debug a Tapestry application, tell Tomcat to run the JVM in debug mode, set a breakpoint in the Java code and
make a Debug configuration in Eclipse to connect to that JVM.
31

Chapter 2
Chapter 2 Using Forms
32 Chapter 2 Using Forms

What's in this chapter?


In this chapter you'll learn how to use forms to get input from the user.

Developing a stock quote application


Suppose that you'd like to develop an application like this:

That is, the user can enter the stock id and click "OK", then the stock value will be displayed. To do that, create a new
Java application named "StockQuote":

Set the output folder to StockQuote/context/WEB-INF/classes and then add the user library "Tapestry 4" to it:
Using Forms 33

Create web.xml in context/WEB-INF:


<?xml version="1.0"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/TR/xmlschema-1/"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>StockQuote</display-name>
<servlet>
<servlet-name>StockQuote</servlet-name>
<servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>StockQuote</servlet-name>
<url-pattern>/app</url-pattern>
</servlet-mapping>
</web-app>
Create a context descriptor StockQuote.xml in c:\tomcat\conf\Catalina\localhost:
<Context
docBase="c:/workspace/StockQuote/context"
path="/StockQuote"
reloadable="true"/>
Create Home.html in the context/WEB-INF to display the HTML form:
<html>
<form jwcid="stockQuoteForm">
<input type="text" jwcid="stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
The HTML form is marked as a component, so is the HTML text field. To check if it is correct, open this file
(c:\workspace\StockQuote\context\WEB-INF\Home.html) using a browser. It should look like:
34 Chapter 2 Using Forms

Next, define the components in Home.page:


<?xml version="1.0"?>
<!DOCTYPE page-specification PUBLIC
"-//Apache Software Foundation//Tapestry Specification 4.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<page-specification>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="ognl:stockId"/>
</component>
</page-specification>
There are two components in this page: one is named "stockQuoteForm" and the other is named "stockId". The
"stockQuoteForm" component is a Form component. It has a parameter named "listener" that is bound to the
expression "listener:onOk". The prefix here is listener. Let's ignore what this expression means for now. When the page
object asks this component to render itself (see the diagram below), it will output a start tag <form action="..."> and
then render everything inside its template (called its "body"). The "stockId" component is a TextField component. It will
generate an HTML element like <input type="text" value="???">. What's the "value" attribute? It will evaluate its "value"
parameter (the OGNL expression "stockId", that is, call getStockId() on the page object) and use the result as the
"value" attribute. Then the Form component will render the submit button which is just a regular HTML element. Finally
it will output the end tag </form>. That is, the whole process is like:

Page object 1: Render yourself

2: Output the start tag


for an HTML form
element
stockQuoteForm <form action="...">
<input type="text" value="MSFT"/>
<input type="submit" value="OK"/>
7: Output regular</form>
HTML code
3: Look, what I
need to render?
8: Output the end tag

<html> 4: Render yourself


<form jwcid="stockQuoteForm">
<input type="text" jwcid="stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
5: Call getStockId() on
the page object.
Suppose the return 6: Output an HTML text
value is "MSFT". input element like <input
type="text" value="MSFT"/>

stockId

Note that in the process the "listener" parameter of the Form component is not used yet. It is used when the HTML
Using Forms 35

form is submitted. Suppose that the user enters "SUN" as the stock id and then clicks "OK". Then an HTTP request will
be sent to your Tapestry application (see the diagram below). The "SUN" value is also included in the request. To
handle the request, Tapestry will create the page object again (and the components in it) and then asks the Form
component to handle the form submission (see the diagram below). The Form component will ask all the components
in its body (there is only one here, the "stockId" TextField) to handle the form submission. The "stockId" component will
get the value of the text input entered by the user ("SUN") and set its "value" parameter, i.e., call setStockId("SUN") on
the page object. Finally, the Form component evaluates its "listener" parameter ("listener:onOk"). In this case, this will
create an action listener object. All action listener objects must define an actionTriggered() method. In this case, the
actionTriggered() method of this action listener object will just call the onOk() method of the page object. Finally, the
Form component will call the actionTriggered() method of that action listener object. The effect is, onOk() of the page
object is called:
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
6: Look, what expression to
evaluate? Oh, it is a listener
Tapestry expression.

Page object
void onOk() { 1: Handle the form
... submission
} 7: Create it Action listener
void actionTriggered() {
page.onOk();
}

9: Call it

8: Call
actionTriggered()
stockQuoteForm

5: Call setStockId
("SUN") on the HTTP request
page object.
3: What is the
2: Handle value of the
the form HTML text
submission input?

4: It's "SUN"
stockId

As the BasePage doesn't have a getStockId(), a setStockId() or an onOk() methods, you need to create a subclass.
Let's create a Home class in package com.ttdev.stockquote:
public abstract class Home extends BasePage {
private String stockId;

public String getStockId() {


return "MSFT";
}
public void setStockId(String stockId) {
this.stockId = stockId;
}
public void onOk() {
System.out.println("Listener called. Stock id is: " + stockId);
}
}
Then use it for the page:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockQuoteForm" type="Form">
36 Chapter 2 Using Forms

<binding name="listener" value="listener:onOk"/>


</component>
<component id="stockId" type="TextField">
<binding name="value" value="ognl:stockId"/>
</component>
</page-specification>
Now, run the application and it should be like:

Check the HTML code to verify that it is not just your Home.html:

Enter "SUN" as the stock id and click "OK". In the Tomcat console window you should see the output message:

In the browser you should see the original page is displayed again:

This is because after handling the form submission, by default Tapestry will render the original page (Home) again.
Because ognl is the default prefix, Home.page can be simplified as:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="ognl:stockId"/>
</component>
</page-specification>
Using Forms 37

Creating the result page


Next, you'll display the result page. Create Result.html in the same folder as Home.html:
<html>
The stock value is: <span jwcid="stockValue"/>.
</html>
You'll output the stock value using the "stockValue" component. It should be an Insert component. Create Result.page
in the same folder to define this component (Note that from now on the <?xml> line and the <!DTD> line will not be
shown):
Result.page
<page-specification class="com.ttdev.stockquote.Result">
<component id="stockValue" type="Insert">
<binding name="value" value="stockValue"/>
</component>
</page-specification>

An OGNL expression (remember that ognl is the


default prefix). It will call getStockValue() on the
Result.java
page object
public abstract class Result extends BasePage {
public int getStockValue() {
return 100;
}
}

You're just returning a hard coded stock value of 100. Note that it is an int, not a string. Can the Insert component
handle an int? Yes, it can. If the value is not a string, it will call toString() on it to get a string.
To see if the page is working, use the browser to display
http://localhost:8080/StockQuote/app?service=page&page=Result. You should see:

Displaying the Result page in the listener


Next, let's display the Result page in the listener method. Modify Home.java:
38 Chapter 2 Using Forms

Action listener
An argument is added. It is a "request void actionTriggered() {
cycle". It represents the request from page.onOk(cycle);
the browser and the response of your }
application.

public abstract class Home extends BasePage {


private String stockId;
The action listener object created by
public String getStockId() {
return "MSFT"; Tapestry is very smart. It will check if the
} method has a request cycle formal
public void setStockId(String stockId) { argument, if yes, it will provide one to it.
this.stockId = stockId;
}
public void onOk(IRequestCycle cycle) {
System.out.println("Listener called. Stock id is: " + stockId);
cycle.activate("Result");
}
}

Display the page named "Result" as the


response after this listener method returns
Note that calling the activate() method will NOT display the Result page immediately. The request cycle will simply keep
a reference to the Result page for later display. Only after the listener is finished, will it display it. It means that, for
example, if you wrote:
public abstract class Home extends BasePage {
...
public void onOk(IRequestCycle cycle) {
cycle.activate("Home");
cycle.activate("Result");
}
}
Then it would still just display the Result page. It would NOT display the Home page and then the Result page. What if
you didn't call activate() at all? Then it would display the current page again (Home in this case) as if you had activated
it:

public abstract class Home extends BasePage { public abstract class Home extends BasePage {
... ...
public void onOk(IRequestCycle cycle) { public void onOk(IRequestCycle cycle) {
//don't call activate() at all cycle.activate("Home");
} }
} }

The effect is the same

Now, run the application and click "OK" to see if it can bring you to the Result page:

Instead of calling activate() explicitly, you could achieve exactly the same effect by returning the page name from the
listener method:
Using Forms 39

public abstract class Home extends BasePage {


...
public String onOk(IRequestCycle cycle) {
return "Result";
}
}
Displaying a hard coded stock value is not that interesting. Let's calculate the stock value using the stock id. For
simplicity, just use the hash code of stock id modulo 100 as the stock value. So, modify Home.java:
public abstract class Home extends BasePage {
private String stockId;

public String getStockId() {


return "MSFT";
}
public void setStockId(String stockId) {
this.stockId = stockId;
}
public String onOk(IRequestCycle cycle) {
int stockValue = stockId.hashCode() % 100;
return "Result";
}
}
Remember that the stockId instance variable has been set by the TextField component before the Form component
calls the listener. However, after calculating the stock value, how to pass it to the Result page for display? This can be
done this way:
public abstract class Home extends BasePage {
private String stockId;
...
public IPage onOk(IRequestCycle cycle) {
int stockValue = stockId.hashCode() % 100;
Result resultPage = (Result) cycle.getPage("Result");
resultPage.setStockValue(stockValue);
return resultPage;
}
}
Here, you call getPage() on the request cycle to load the Result page object. This method returns an IPage, an
interface implemented by BasePage and thus the Result page. Then you typecast it to a Result page. Then you store
the stock value into it. You don't have this method yet, so you'll need to write it later. Finally, you return the Result page
object instead of just the page name. This is also OK. Then Tapestry will display exactly this page object as the
response page. This way of passing information from one page to another is called the "bucket brigade" pattern in
Tapestry.
Next, modify Result.java to define that method:
public abstract class Result extends BasePage {
int stockValue;

public int getStockValue() {


return 100stockValue;
}
public void setStockValue(int stockValue) {
this.stockValue = stockValue;
}
}
It's simple. You just store the stock value into an instance variable and return it in the getter so that the Insert
component will display it. Now run the application and it should be like:
40 Chapter 2 Using Forms

Easier way to get access to another page


At the moment you're calling getPage() on the request cycle to load the Result page. Actually there is a slightly easier
way:

1: Create a page object for Home


Tapestry
Home

3: It is requested that a property 4: Create a


named "resultPage" be added to 2: Basically just create an subclass of Home
the class. instance of this Home class, to provide a getter
for the property Extends
but...
Home.page
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/> HomeEnhanced
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component> Result getResultPage() {
<component id="stockId" type="TextField"> return cycle.getPage("Result");
<binding name="value" value="stockId"/> }
</component>
</page-specification>

As the type of the property is


Home.java "page", the getter will just load
public abstract class Home extends BasePage { the page from the request cycle.
private String stockId; The name of the page is given as
the "object". If the type were not
abstract public Result getResultPage(); "page", then how to interpret the
"object" would be different.
public String getStockId() {
return "MSFT";
}
public void setStockId(String stockId) {
this.stockId = stockId; Tapestry will make sure the return type of
} the getter will match of the getter is
public IPage onOk(IRequestCycle cycle) { declared as abstract in the parent.
int stockValue = stockId.hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
}

What you have done is called "injecting a page" into the Home page. Now run the application and it should continue to
work.

Instance variables may breach security


Let's run an experiment. First, let's remove the disable-caching JVM property. To do that, shutdown Tomcat, open new
command prompt (so that the JAVA_OPTS environment variable is no longer there), then start Tomcat using
startup.bat (so that JAVA_OPTS remains undefined). Then get the stock value of MSFT:
Using Forms 41

Now, open a new browser window to simulate another user. Enter


http://localhost:8080/StockQuote/app?service=page&page=Result. You'll see:

It means one user is seeing the result of another user. Why is it happening? It is because after using a page object
(e.g., the Result page object), Tapestry will not throw it away. Instead, it will put it into a pool for reuse (see the diagram
below). Later when it needs to use the Result page object again, it will check if the pool has one. If so, just take it out
from the pool and use it. Only when the pool has no such page object, will it create a new page object. That is, the
whole process is like:
4: Render Result
page (again)

Tapestry
5: Give me a Result
1: Render yourself page object
7: Render yourself
2: Done
Page pool

3: Moved into the pool


Result page Result page
object object
6: Taken out of the
pool

The problem here is that the Result page object in the pool is still keeping the stock value of 24. To solve this problem,
you can modify Result.java:
public abstract class Result extends BasePage implements PageDetachListener {
int stockValue;

public void pageDetached(PageEvent event) {


stockValue = 0;
}
public int getStockValue() {
return stockValue;
}
public void setStockValue(int stockValue) {
this.stockValue = stockValue;
}
}
Noting that the page implements PageDetachListener, Tapestry will call the pageDetached() method just before it is put
into the pool (step 3 in the graph above). This way, a page object taken from the pool will always have a zero value in
the stockValue, exactly the same as a new page object.
42 Chapter 2 Using Forms

Now try the experiment again and the second user will only see:

The take home message is that generally it is dangerous to have instance variables in a page object. Whenever you
see them, you probably should implement the PageDetachListener interface to clear them to null or 0. As an
alternative, you may let Tapestry do all that for you. Modify Result.page:
<page-specification class="com.ttdev.stockquote.Result">
<property name="stockValue"/>
<component id="stockValue" type="Insert">
<binding name="value" value="stockValue"/>
</component>
</page-specification>
Then at runtime Tapestry will create a subclass of your Result class that looks like:
public class ResultEnhanced extends Result implements PageDetachListener {
private XXX stockValue;

public void pageDetached(PageEvent event) {


stockValue = <default value for type XXX>;
}
public XXX getStockValue() {
return stockValue;
}
public void setStockValue(XXX stockValue) {
this.stockValue = stockValue;
}
}
By looking at the .page file Tapestry doesn't know the type of "stockValue". But let's assume it is XXX. Then it will use
this subclass for the Result page. In initialize(), the stock value is set to some default value according to the type XXX.
For example, if XXX is int, then the default value is 0. If XXX is Object, then the default value is null. Now, you don't
need the instance variable in Result.java:
Using Forms 43

1: Create a page object for Result


Tapestry
Result

3: It is requested that a property named


"stockValue" be added to the class.
2: Basically just create an
instance of this Result class, Extends
but... 4: Create a subclass of
Result.page Result to provide a getter,
setter and an initialize()
<page-specification class="com.ttdev.stockquote.Result">
<property name="stockValue"/> method for the property
<component id="stockValue" type="Insert">
<binding name="value" value="stockValue"/>
</component>
</page-specification> ResultEnhanced
int stockValue;
How does Tapestry know that it is an int? protected void initialize() {
It knows that from this method signature. If stockValue = 0;
there weren't such a method, it would just }
declare it as an Object. public int getStockValue() {
return stockValue;
}
Result.java public void setStockValue(int v) {
public abstract class Result extends BasePage { this.stockValue = v;
int stockValue; }

public int getStockValue() {


return stockValue;
}
public void setStockValue(int stockValue) {
this.stockValue = stockValue;
}
abstract public void setStockValue(int stockValue);
}

This method is kept here so that


Home.java can call it. Otherwise, you
wouldn't have to use this method.

What you have done is called "injecting a property" into the page. The most important effect is that the property is
cleaned up automatically when it is returned to the pool, so security is ensured. Now run the application and it should
continue to work.
Let's check the Home class above. It also has an instance variable. Is it dangerous? It should be OK as the stock id will
be set by the TextField component before it is used. But if you prefer being safe than sorry, you may use a property
specification in its place:
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<property name="stockId"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Then, modify Home.java:
public abstract class Home extends BasePage {
private String stockId;

abstract public Result getResultPage();


abstract public String getStockId(); {
return "MSFT";
}
public void setStockId(String stockId) {
44 Chapter 2 Using Forms

this.stockId = stockId;
}
public IPage onOk(IRequestCycle cycle) {
int stockValue = stockIdgetStockId().hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
}
Note that as you can't access the stock id as a variable, the onOk() method needs to call the getter. That's why you
need to keep the getter and declare it as abstract.
Now you're about to run the application. But you have removed the disable-caching JVM property and thus the .page
files are cached. So, reload the application in the Tomcat manager. Then run the application and it should continue to
work. However, you'll notice that the initial value is no longer MSFT:

This is by default the stock id property is set to null. To set the initial value back to MSFT, modify Home.page:
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<property name="stockId" initial-value="literal:MSFT"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Of course you could use an OGNL expression if you needed to. Now reload the application and run it. You should see
MSFT again:

You have finished this experiment. Restart Tomcat using tap.bat to get back the disable-caching JVM property.

Using Java annotations to inject pages and properties


At the moment you're injecting a page and a property into the Home page:
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<property name="stockId" initial-value="literal:MSFT"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
In fact, these can be done directly in the Home class using Java annotations. As annotation is available only in Java 5
or later, you need to tell Eclipse to use Java 5 to compile your project. To do that, right click the project and choose
"Properties", choose "Java Compiler" on the left hand side and choose "5.0" as the complier:
Using Forms 45

Then modify Home.page and Home.java:


Home.page
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<property name="stockId" initial-value="literal:MSFT"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField"> The <inject> element
<binding name="value" value="stockId"/> there has exactly the
</component> same effect as the
</page-specification> @InjectPage
annotation here.
InjectPage is a Java interface is
defined in the
Home.java org.apache.tapestry.annoations
import org.apache.tapestry.annotations.*; package. So you must import the
package.
public abstract class Home extends BasePage {
@InjectPage("Result")
abstract public Result getResultPage();

abstract public String getStockId(); Simply delete the <property> element. When
Tapestry sees an unimplemented getter
public IPage onOk(IRequestCycle cycle) { (either the method is declared abstract or the
int stockValue = getStockId().hashCode() % 100; class is implementing an interface but doesn't
Result resultPage = getResultPage(); provide an implementation for a method) like
resultPage.setStockValue(stockValue); getStockId() here, it will create a property for
return resultPage; it. However, then there is no way to set the
} initial value.
}
How to set the initial stock id to MSFT? Do it this way:
46 Chapter 2 Using Forms

public abstract class Home extends BasePage { After creating a new page object, Tapestry will
@InjectPage("Result") evaluate this expression to get the value and
abstract public Result getResultPage(); store it into the property. In this case you're
using the literal prefix. If required, you can use
@InitialValue("literal:MSFT") the ognl prefix and specify an OGNL
abstract public String getStockId(); expression.
public IPage onOk(IRequestCycle cycle) {
int stockValue = getStockId().hashCode() % 100; Before Tapestry returns the page object back
Result resultPage = getResultPage(); to the pool, it will evaluate the expression and
resultPage.setStockValue(stockValue); store the value into the property again so that
return resultPage; the next time when it's taken out of the pool,
} the value will have already been set.
}

Using implicit components


You've been defining the components in .page files. In fact, if you'd like, you could define them in the HTML files. For
example, you could modify Home.html:
It says that the type of the Write the binding here directly
component is Form
<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<input type="text" jwcid="stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
Now, you don't need to define this component in Home.page:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Such a component is called an "implicit component". In contrast, those defined in .page files are called "declared
components". As you don't need the component id anymore, you could delete it too:
<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<input type="text" jwcid="stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
Now it is an anonymous component. If you'd like, you could turn the "stockId" component into an implicit component
too:
<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<input type="text" jwcid="stockId@TextField" value="ognl:stockId"/>
<input type="submit" value="OK"/>
</form>
</html> You must write the ognl prefix here,
otherwise Tapestry will assume that it
is a literal. That is, in a template, the
default prefix is literal. In a page
specification, the default prefix is ognl.
Delete it from Home.page:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Run the application and it should continue to work.
You may wonder which way is better: implicit or declared? Implicit components are easier to write and read because
you only need to look at one file, not two. However, if you let a web designer modify your HTML files, then he may
Using Forms 47

delete your components by mistake. If you're using declared components, all you need to do is to add the component
ids back. If you're using implicit components, you'll have to specify all the bindings again.

Using a combo box


Suppose that you'd like to change the application so that the user will choose from a list of stock ids instead of typing in
one:

To do that, modify Home.html:


<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<select jwcid="stockId">
<option value="0">IBM</option>
<option value="1">RHAT</option>
</select>
<input type="submit" value="OK"/>
</form>
</html>
Actually the HTML <select> element is just for preview only. It will be completely replaced by the HTML code generated
by the "stockId" component. Define that component in Home.page:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockId" type="PropertySelection">
<binding name="model" value="availStockIds"/>
<binding name="value" value="stockId"/>
</component>
</page-specification>
It is a PropertySelection component. It will render itself as a HTML <select> element. It will get the options from the
"model" parameter. In this case, it will call getAvailStockIds() on the page object, which should return a list of options. It
check its "value" parameter to see which option is currently selected. When the form is submitted, it will set the "value"
parameter to the option selected by the user. In this case you're using the "stockId" property to hold the selected option.
As you don't have the getAvailStockIds() method yet, define it in Home.java:
public abstract class Home extends BasePage {
@InjectPage("Result")
abstract public Result getResultPage();

abstract public String getStockId();

public IPage onOk(IRequestCycle cycle) {


int stockValue = getStockId().hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
public IPropertySelectionModel getAvailStockIds() {
return new StringPropertySelectionModel(new String[] { "IBM", "MSFT",
"RHAT" });
}
}
It returns a StringPropertySelectionModel object, which is just a list of strings. You specify the strings in a string array
and pass that array to its constructor. The return type of the method is IPropertySelectionModel, not
StringPropertySelectionModel. IPropertySelectionModel is an interface presenting a list of <select> options.
StringPropertySelectionModel implements IPropertySelectionModel and uses an array of string constants as the list of
options. As there are ways to provide the list of options, it is better to use IPropertySelectionModel.
Now run the application and it should work:
48 Chapter 2 Using Forms

Using the DatePicker


Suppose that you'd like to allow the user to query the stock value on a particular date:

The user can click on the calendar icon to choose a date:

To do that, modify Home.html:


<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<select jwcid="stockId">
<option value="0">IBM</option>
<option value="1">RHAT</option>
</select>
on <span jwcid="quoteDate">May 3, 2005</span>
<input type="submit" value="OK"/>
</form>
</html>
Define the "quoteDate" component in Home.page:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockId" type="PropertySelection">
<binding name="model" value="availStockIds"/>
<binding name="value" value="stockId"/>
</component>
<component id="quoteDate" type="DatePicker">
<binding name="value" value="quoteDate"/>
Using Forms 49

</component>
</page-specification>
It's a DatePicker component. It will allow the user choose a date. It will store the selected date into its "value" parameter
when the form is submitted. In this case, it'll call setQuoteDate() on the page object. Of course, it will also call
getQuoteDate() and display the result as the initial value. For this to work, define a property by making an abstract
getter in Home.java:
public abstract class Home extends BasePage {
@InjectPage("Result")
abstract public Result getResultPage();

abstract public String getStockId();


abstract public Date getQuoteDate();

public IPage onOk(IRequestCycle cycle) {


int stockValue = (getStockId() + getQuoteDate().toString()).hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
public IPropertySelectionModel getAvailStockIds() {
return new StringPropertySelectionModel(new String[] { "IBM", "MSFT",
"RHAT" });
}
}
Here you just concatenate the stock id and the string representation of the quote date and then get the hash code.
Now, run the application. It will look fine, but clicking on the calender button will do nothing and won't let you choose a
date:

The button will not be functioning

This is because the DatePicker needs to generate some Javascript in order for it to function. In addition, the script
generated makes use of a Javascript library called "dojo". Therefore, for it to work, first you need to bring in dojo.js
(included in Tapestry) by something like:
<html>
<head>
<script type="text/javascript" src="...">
</script>
</head>
...
</html> The path to the dojo.js file. This file is
included in a tapestry jar file. What is
the path exactly? You don't need to
worry about that. Read on.

To generate this code, you can use a Shell component:


50 Chapter 2 Using Forms

Note that you're using an implicit component here.


The Shell component will generate the <html> and
<head> elements.
<html jwcid="@Shell" title="Stock Quote">
<form jwcid="stockQuoteForm@Form" ...> You must specify the title
...
</form> Bring in the dojo library
</html>

<html>
<head>
<title>Stock Quote</title>
<script type="text/javascript" src="...">
</script>
</head>
<form jwcid="stockQuoteForm@Form" ...>
...
</form>
</html>

Second, the script generated by the DatePicker needs to be collected and put into the <body> element:
<html>
<head>
...
</head>
<body>
<form jwcid="stockQuoteForm@Form" ...>
...
</form>
<script ....>
</body>
</html>
To do that, you can use the Body component:
<html jwcid="@Shell" title="Stock Quote">
<body jwcid="@Body">
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<select jwcid="stockId">
<option value="0">IBM</option>
<option value="1">RHAT</option>
</select>
on <span jwcid="quoteDate">May 3, 2005</span>
<input type="submit" value="OK"/>
</form>
</body>
</html>
Now, run the application again and it should work:
Using Forms 51

Using the API doc


You have seen some classes from Tapestry such as BasePage, RequestCycle, IPropertySelection. If you'd like find out
more information about them, you can go to http://tapestry.apache.org and choose Project Reports | JavaDocs on the
left hand side:

Then search for the class (e.g., BasePage):


52 Chapter 2 Using Forms

Using the component reference


There are so many components and each component has quite some parameters. You may wonder how one can
remember all these? It's OK. You don't need to memorize all these. You can look them up. Just locate the component
on the left hand side:
Using Forms 53

You will find a description about what it does and a list of its parameters. For each parameter, it describes its name, the
type of value expected, whether it is required or optional and its default value if it is not bound.

Summary
To get input from the user, use a Form component and put some TextField components in it. When the form is
rendered, each TextField will get the data from the page object and display it in an HTML input field. When the form is
submitted, each TextField will store the input into the page object and finally the Form's listener will be called so that
you can perform further calculation on the data already stored in the page object.
To tell Tapestry which page is the result page, call activate() or return the name of the page or the page object itself.
Before that, you can pass information to it by calling its setters (bucket brigade pattern). To let a page load another
page easily, you can inject the page in the .page file using <inject> or directly in the Java class using annotations.
In addition to TextField components, you can also use PropertySelection components and DatePicker components in a
Form. Some components like DatePicker need to generate Javascript to function. Most likely they also need the dojo
library. For them to work, you need a Shell component to bring in dojo and a Body component to collect the scripts.
To create a listener object that calls your listener method on your page object, you can use the listener prefix.
It is dangerous to use instance variables in your page objects because they're reused for different users. You should
use a property by having a <property> in the .page file or by declaring an abstract getter instead. To access such
properties, you can declare abstract getters and setters in your page class and make the class abstract.
You can specify your components in the .page file (declared component) or in the HTML file (implicit component). The
former provides a cleaner separation but the latter is easier to read and write. When specifying a binding in an HTML
file, the default prefix is literal instead of ognl.
To lookup the parameters of a certain component type, use the documentation on the component.
To see what a class does, check the API doc.
55

Chapter 3
Chapter 3 Validating Input
56 Chapter 3 Validating Input

What's in this chapter?


In this chapter you'll learn how to validate the input from the user, how to display the errors (if any). In addition, you'll
learn how to render a component in a loop.

Postage calculator
Suppose that you'd like to develop an application to calculate the postage for sending a package from some place to
another. The user will enter the weight of the package in kg (check the screenshots below). Optionally, he can enter a
"patron code" identifying himself as a patron to get a certain discount. After clicking "OK", it will display the postage:

To do that, create an application named Postage. Setup the class path, output folder and web.xml as usual. Then
modify Home.html:
<html>
<form jwcid="form">
<table>
<tr>
<td>Weight:</td>
<td><input type="text" jwcid="weight"/></td>
</tr>
<tr>
<td>Patron code:</td>
<td><input type="text" jwcid="patronCode"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit"/></td>
</tr>
</table>
</form>
</html>
Define the components in Home.page:
<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
</component>
</page-specification>
Home.java is like:
Validating Input 57

Inject the Result page

public abstract class Home extends BasePage {


private Map patronCodeToDiscount;
@InjectPage("Result")
public abstract IPage getResult();
public abstract String getWeight();
public abstract String getPatronCode(); Two properties
Convert the "weight" property
public Home() { from a string to an int
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90)); For the patron whose code is
patronCodeToDiscount.put("p2", new Integer(95)); "p1", the discount is 90% (i.e.,
} 10% off).
public IPage onSubmit() {
int weight = Integer.parseInt(getWeight());
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
int postagePerKg = 10;
int postage = weight * postagePerKg; Lookup the map to find out his
if (discount != null) { discount
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
For simplicity, assume the
Load the Result page and pass postage per kg is $10 to
the postage value to it and set it calculate the postage
as the response page.
PropertyUtils is a class coming with a jar file
used by Tapestry. The write() method here
will write the postage Integer into the
"postage" property of the resultPage object.
Next, create the Result page. Result.html is like:
<html>
The postage is <span jwcid="@Insert" value="ognl:postage"/>.
</html>
Result.page is like:
<page-specification>
<property name="postage"/>
</page-specification>
As the page class is not specified, the BasePage will be used. Tapestry will create a subclass of it to host the property.
Next, create a context descriptor Postage.xml in c:\tomcat\conf\Catalina\localhost:
<Context
docBase="c:/workspace/Postage/context"
path="/Postage"
reloadable="true"/>
Now run the application and it should work:

Accepting integer input


At the moment the "weight" property of the Home page is a string. This is no good. Ideally, it should be an int. It is a
58 Chapter 3 Validating Input

string because the TextField component expects a string as the value of its "value" parameter. Fortunately this can be
changed. The idea is (see the diagram below), when it renders itself, let it accepts any object as its "value" parameter,
then give it a "translator" object which can translate the object into a string. Then it can output that string into the HTML
code:

Page object
2: call getWeight(). Assume
that it returns an Integer(5)
object.
1:Render
yourself <input type="text" value="5">

5: Output the string "5"

3: Convert the object


Component Integer(5) to a string
"weight" translator
4: The string is "5"
When the form is submitted (see the diagram below), the TextField component can get the string from the request and
ask the translator object to translate it into an object, then it can store that object into its "value" parameter:

Page object
6: call setWeight() and
pass the Integer(5) object HTTP request
1: Handle
the form 2: What is the
submission value of the
HTML text input?

3: It's a string "5"

4: Convert string "5" to an


Component object
"weight" translator
5: The object is Integer(5)

To implement this idea, modify Home.page:


<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/> A few predefined types of
</component> translators are available.
</page-specification> "number" is one of them.
It can convert between a
number (an int, a double,
etc.) and a string.
Now you can change the "weight" property into an int in Home.java:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;
@InjectPage("Result")
public abstract IPage getResult();
public abstract String int getWeight();
Validating Input 59

public abstract String getPatronCode();

public Home() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}
public IPage onSubmit() {
int weight = Integer.parseInt(getWeight());
int weight = getWeight();
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
int postagePerKg = 10;
int postage = weight * postagePerKg;
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Now run the application and it should continue to work. What if you'd like to allow a floating number as the weight? By
default the number translator assumes that it is an int. To tell it to accept a floating number, set its pattern to "#.#". This
means a decimal point is allowed and any number of digits are allowed on both side:
<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/> This sets a property named "pattern" of the
</component> number translator. If you'd like to set other
</page-specification> properties, just write something like:

<binding name="translator"
value="translator:number,pattern=#.#,foo=xxx,bar=yyy"/>

The whole thing is called an


"initializer". The property values
must be constant values.
For it to work the weight should be a double, not an int:
public abstract class Home extends BasePage {
...
public abstract int double getWeight();
...
public IPage onSubmit() {
int double weight = getWeight();
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg);
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Now you can enter a floating point number like:
60 Chapter 3 Validating Input

In addition to the number translator, another common translator is a translator named "date". It supports patterns like
MM/dd/yyyy.

What if the input is invalid?


At the moment if the user enters a negative number as the weight (e.g., -20), it will go ahead and return a negative
postage:

This is no good. Instead, you'd like the application to tell the user that the weight is invalid:

Similarly, it should also check if the patron code is valid or not. For example, if the user enters "p3", it should tell him
that this code is not found:
Note that as the patron code is optionally, if he doesn't enter anything, it shouldn't be treated as an error.
Validating Input 61

Let's do it. Modify Home.java:


You can consider
validation delegate a list
public abstract class Home extends BasePage {
of error messages
private Map patronCodeToDiscount;
private ValidationDelegate delegate;
Create a new Field name Old value Error msg
@InjectPage("Result") one for each weight "-20" "Weight must be >= 0"
public abstract IPage getResult(); request (form
public abstract double getWeight(); submission)
public abstract String getPatronCode();

public Home() {
patronCodeToDiscount = new HashMap(); Record the
patronCodeToDiscount.put("p1", new Integer(90)); (invalid)
patronCodeToDiscount.put("p2", new Integer(95)); value input
} Set the "weight" by the user
public IPage onSubmit() { TextField as the Record the
delegate = new ValidationDelegate(); current component error
double weight = getWeight(); (input field) message
if (weight < 0) {
delegate.setFormComponent((IFormComponent) getComponent("weight"));
delegate.recordFieldInputValue(Double.toString(weight));
delegate.record("Weight must be >=0",
ValidationConstraint.TOO_SMALL);
} Add another error
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode()); to the validation
delegate
Patron code if (discount == null && getPatronCode() !=null) {
not found but delegate.setFormComponent((IFormComponent) getComponent("patronCode"));
was indeed delegate.recordFieldInputValue(getPatronCode());
input by the delegate.record("Patron not found", null);
user }
if (delegate.getHasErrors()) { The type of the error. There are
return null; Returning null means other types such as
It contains at using this page (Home) REQUIRED (a required input is
least one }
int postagePerKg = 10; as the response page not provided), TOO_LARGE
error? (value is too large) and etc.
int postage = (int) (weight * postagePerKg);
if (discount != null) { This is not used at all and you
postage = postage * discount.intValue() / 100; may just pass a null to
} represent an unknown type of
IPage resultPage = getResult(); error.
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
To display the errors stored in the validation delegate, modify Home.html:
<html>
<span jwcid="errors"/>
<form jwcid="form">
<table>
<tr>
<td>Weight:</td>
<td><input type="text" jwcid="weight"/></td>
</tr>
<tr>
<td>Patron code:</td>
<td><input type="text" jwcid="patronCode"/></td>
</tr>
<tr>
<td></td>
62 Chapter 3 Validating Input

<td><input type="submit"/></td>
</tr>
</table>
</form>
</html>
Define this component in Home.page:
<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
</component>
<component id="errors" type="Delegator">
<binding name="delegate" value="delegate.firstError"/>
</component>
</page-specification>
This component is a Delegator component. What it does is simple: When it is asked to render itself, it will ask another
object to render. You provide that other object to its "delegate" parameter. Here, you get the validation delegate from
the page object and then call getFirstError() on it. This will return the first error message in the validation delegate. Or
more precisely, actually the validation delegate doesn't simply store the error message, it stores a Java object that can
render the error message (see below). Such a Java object is called an "error renderer":
Field name Old value Error renderer
weight "-20" An error render that will render itself as a string "The weight must be >=0".
... ... ...
... ... ...
The idea is that you could provide an error renderer that renders itself as some fancy formatted HTML, a graphics or
anything you want instead of just a plain string.
If there is no error in the validation delegate, getFirstError() will return a null. Then the Delegator component will not
output anything.
As the Delegator component needs to get the validation delegate from the page object, you need to provide a getter:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;
private ValidationDelegate delegate;

public ValidationDelegate getDelegate() {


return delegate;
}
...
}
Now run the application. Unfortunately you'll see:
Validating Input 63

An exception trace with three exceptions is shown. They are all is saying that the source is null when it's trying to get
the "firstError" property of the delegate. If you click on the first Exception, you'll see that it is line 17 in Home.page is
causing this error:

It means the validation delegate it has got is null. This is because you're creating it only in onSubmit(). So when the
Home page (in particular, the "errors" component) is rendered, it is still null. To solve this problem you could create it in
64 Chapter 3 Validating Input

the constructor:
public abstract class Home extends BasePage {
...
private ValidationDelegate delegate;

public Home() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
delegate = new ValidationDelegate();
}
public ValidationDelegate getDelegate() {
return delegate;
}
public void onSubmit() {
delegate = new ValidationDelegate();
...
}
}
But it doesn't smell right as you're also creating it again in onSubmit(). Do you have to create it again there? If you
don't, the errors will be accumulating in the validation delegate. To solve this problem, note that all you want to create it
on demand and destroy it after handling the request. This can be done using a "bean":
<page-specification class="com.ttdev.postage.Home">
<bean name="delegate" class="org.apache.tapestry.valid.ValidationDelegate"/>
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component> Create it from
<component id="weight" type="TextField"> this class
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
</component>
<component id="patronCode" type="TextField"> Lookup this bean by name. If
<binding name="value" value="patronCode"/> it's not there yet, it will be
</component> created automatically.
<component id="errors" type="Delegator">
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
</page-specification>
Conceptually, each BasePage object has a Map named "beans" to store all the beans created for it so far. To lookup a
bean named XXX in OGNL, just write "beans.XXX". Usually this will call getBeans() first, which will return a Map of the
beans. Then it should call getXXX() on the Map. However, OGNL notes that it is a Map, so it will lookup the Map by
calling get("XXX") on it instead. If such a bean doesn't exist yet in the Map, the Map will create it automatically. When
the request cycle is ended and the page is about to be returned to the pool, the bean will be destroyed.
As you're now using a bean as the validation delegate, update Home.java:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;
private ValidationDelegate delegate;

public ValidationDelegate getDelegate() {


return delegate;
}
@InjectPage("Result")
public abstract IPage getResult();
public abstract double getWeight();
public abstract String getPatronCode();

public Home() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}

public IPage onSubmit() {


delegate = new ValidationDelegate();
ValidationDelegate delegate = (ValidationDelegate) getBeans().getBean("delegate");
double weight = getWeight();
if (weight < 0) {
delegate.setFormComponent((IFormComponent) getComponent("weight"));
delegate.recordFieldInputValue(Double.toString(weight));
delegate.record("Weight must be >=0",
ValidationConstraint.TOO_SMALL);
}
Validating Input 65

Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());


if (discount == null && getPatronCode() != null) {
delegate.setFormComponent((IFormComponent) getComponent("patronCode"));
delegate.recordFieldInputValue(getPatronCode());
delegate.record("Patron not found", null);
}
if (delegate.getHasErrors()) {
return null;
}
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg);
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Now run the application again and it should work:

Again, you don't have to declare the bean in the .page file. You can do it using Java annotations in Home.java:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;

@InjectPage("Result")
public abstract IPage getResult();
public abstract double getWeight();
public abstract String getPatronCode();

@Bean
public abstract ValidationDelegate getDelegate();

public Home() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}
public IPage onSubmit() {
ValidationDelegate delegate = (ValidationDelegate) getBeans().getBean(
"delegate");
ValidationDelegate delegate = getDelegate();
double weight = getWeight();
if (weight < 0) {
delegate.setFormComponent((IFormComponent) getComponent("weight"));
delegate.recordFieldInputValue(Integer.toString(weight));
delegate.record("Weight must be >=0",
ValidationConstraint.TOO_SMALL);
}
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
if (discount == null && getPatronCode() != null) {
delegate.setFormComponent((IFormComponent) getComponent("patronCode"));
delegate.recordFieldInputValue(getPatronCode());
delegate.record("Patron not found", null);
}
if (delegate.getHasErrors()) {
return null;
}
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg);
66 Chapter 3 Validating Input

if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Delete it from Home.page:
<page-specification class="com.ttdev.postage.Home">
<bean name="delegate" class="org.apache.tapestry.valid.ValidationDelegate"/>
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
</component>
<component id="errors" type="Delegator">
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
</page-specification>
The application will continue to work.

Using validators
Even though it is working, there is still something to desire for: Checking if a number is positive is something that you
do frequently. Having to do it yourself is too much trouble. To solve these problems, you can use a validator. The idea
is that when the "weight" component is handling the form submission (see the diagram below), it will get the string value
from the HTTP request and ask the translator to translate the string into an object. Then it can ask a list of validators to
validate that object in turn. If any one of them considers the object invalid, that validator will record an error in the
validation delegate and the processing will finish:
Field name Old value Error msg
weight "-20" "Weight must be >=0"

Page object
HTTP request 8: Record an error

1: Handle
the form 2: What is the validator validator ...
submission value of the
HTML text input?
6: Check if Integer(5) 7: Check if Integer(5)
3: It's a string "5"
is valid. Assume it's is valid. Assume it's
considered valid. considered invalid.

4: Convert string "5" to an


Component object
"weight" translator
5: The object is Integer(5)
Only if all the validators consider the object valid, will the "weight" component call setWeight() on the page object and
pass that Integer object to it. To implement this idea, modify Home.page:
Validating Input 67

<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
<binding name="delegate" value="beans.delegate"/> This prefix means what is following is a list
</component> of validators
<component id="weight" type="TextField"> The first and only one
<binding name="value" value="weight"/> validator. It checks the
<binding name="translator" value="translator:number,pattern=#.#"/> object (a number) is
<binding name="validators" value="validators:min=0"/> >= 0.
<binding name="displayName" value="literal:Weight"/>
</component>
<component id="patronCode" type="TextField"> What if the object is found
<binding name="value" value="patronCode"/> to be < 0? It will record an
</component> error into the validation
<component id="errors" type="Delegator"> delegate. But how can it
<binding name="delegate" value="beans.delegate.firstError"/> find the validation
</component> delegate? It tries to get it
</page-specification> from the enclosing Form
Set the display name to "Weight". This way, if the
component.
value is found to be invalid, the validator can
compose an error message like "Weight must be >=
0".
Now you no longer need to validate the weight in Home.java:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;
@InjectPage("Result")
public abstract IPage getResult();
public abstract double getWeight();
public abstract String getPatronCode();
@Bean
public abstract ValidationDelegate getDelegate();

public Home() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}
public IPage onSubmit() {
ValidationDelegate delegate = getDelegate();
double weight = getWeight();
if (weight < 0) {
delegate.setFormComponent((IFormComponent) getComponent("weight"));
delegate.recordFieldInputValue(Double.toString(weight));
delegate.record("Weight must be >=0",
ValidationConstraint.TOO_SMALL);
}
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
if (discount == null && getPatronCode() !=null) {
delegate
.setFormComponent((IFormComponent) getComponent("patronCode"));
delegate.recordFieldInputValue(getPatronCode());
delegate.record("Patron not found", null);
}
if (delegate.getHasErrors()) {
return null;
}
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg);
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Now run the application and it should continue to work:
68 Chapter 3 Validating Input

Note that the error message is now generated by the validator and it is using the display name. In addition, two red
stars are displayed after the input field to indicate that it is in error. This is done by the TextField. When it is asked to
render itself (see the diagram below), it will check with the validation delegate to see if it is in error. If yes, it will display
its "old value" from the validation delegate and ask the validation delegate to any extra error indication (which is two red
stars):

Page object Field name Old value Error msg


weight "abc" "Weight is not a number

1: Render
yourself
s
Ye

"
bc
3:

"a 8: Output two


t's
r? 5 :I red stars
ro
er d 7: Output any
Ii
n ol
m y error
m
2 :A t's indication
ha
W ?
4: lue
va
Component 6: Output an input field with "abc"
"weight"

If there is no error for the "weight" component in the validation delegate, then it will get the object by calling getWeight()
(see the diagram below) and ask the validator to convert the object back into a string. Then it will output a text field with
the string as the value:
Validating Input 69

Page object Field name Old value Error msg

5: It's int 5

1: Render
yourself

No
3:
r?
ro
4: Call er
in
getWeight() m I
A
2:

6: Convert Integer(5) to a
Component string
"weight" translator
7: The string is "5"

8: Output a text field with "5"


as the value

What if the translator can't translate the string?


What if the user enters some garbage like "abc" as the weight? Then the number translator will fail to translate it into an
int. In that case, it will act like a validator and record an error into the validation delegate:

That's the reason why the old value must be stored in the error in the validation delegate. You simply can't retrieve the
invalid value by calling getWeight() on the page object.

Handling null input


What if the user doesn't input anything as the weight? In principle the number translator should fail to translate it
because the input is an empty string. However, in Tapestry most of the translators treat this case as no input and return
a null value as the result. Then what will happen to the min validator? Surprisingly it will consider it valid! Why? This
design is to allow the case when some input is optional, but if the user doesn't provide some input, then it must be
validated. This design will allow null (no input) to pass through all the validators:
70 Chapter 3 Validating Input

The weight is turned into 0 when it is converted from an Integer object (null) into an int, so the postage calculated is
also 0.
However, in this particular application, the weight is not optional. So, how to enforce this? You can add a validator that
explicitly rejects null. To do that, modify Home.page:
<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
<binding name="delegate" value="beans.delegate"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
<binding name="validators" value="validators:required,min=0"/>
<binding name="displayName" value="literal:Weight"/>
</component>
<component id="patronCode" type="TextField"> Add a new validator here.
<binding name="value" value="patronCode"/> It will check to ensure the
</component> object is not null.
<component id="errors" type="Delegator">
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
</page-specification>
Now, run the application and it should work (BUG ALERT: In Tapestry 4.1.1 there is a bug in NumberTranslator. It will
convert an empty string into 0, not null. This will get pass both the required and the min validators):

Setting the display message


If you'd like, you can set the error message used by the validator. For example, modify Home.page:
Validating Input 71

<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
<binding name="delegate" value="beans.delegate"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
<binding name="validators" value="validators:required[The {0} is missing!],min=0"/>
<binding name="displayName" value="literal:Weight"/>
</component>
<component id="patronCode" type="TextField"> This is the error
<binding name="value" value="patronCode"/> message The {0} will be replaced by
</component> the display name
<component id="errors" type="Delegator"> ("Weight" in this case).
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
</page-specification>
Using a FieldLabel
In addition to the two red stars, you could turn the label into red too. To do that, use a FieldLabel component:
<html>
<span jwcid="errors"/>
<form jwcid="form">
<table>
<tr>
<td><span jwcid="weightLabel">Weight:</span></td>
<td><input type="text" jwcid="weight"/></td>
</tr>
<tr>
<td>Patron code:</td>
<td><input type="text" jwcid="patronCode"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit"/></td>
</tr>
</table>
</form>
</html>
Define it in Home.page:
<page-specification class="com.ttdev.postage.Home">
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
<binding name="delegate" value="beans.delegate"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
<binding name="validators" value="validators:required[The {0} is missing!],min=0"/>
<binding name="displayName" value="literal:Weight"/>
</component>
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
</component>
<component id="errors" type="Delegator">
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
<component id="weightLabel" type="FieldLabel"> When it renders itself, it will check with
<binding name="field" value="component:weight"/> the validation delegation to see if the
</component> "weight" component is in error. If so, it
</page-specification> will render itself in red.
This prefix says that what is following is the name
of a component in this page. The value of the
whole expression is that component object. In this
case it's the "weight" component.
Now, run the application and it will be like:
72 Chapter 3 Validating Input

Note that it uses the display name of the ValidField to render itself.

Creating your own validator


How to validate the patron code? You can do it in a similar way. However, there is no suitable validator. It's OK. Just
create your own:
Implement the Validator coming
public class PatronCodeValidator implements Validator { with Tapestry in the
private KnownPatrons knownPatrons; You must call this to let it org.apache.tapestry.form.validator
know the KnownPatrons package.
public void setKnownPatrons(KnownPatrons knownPatrons) {
this.knownPatrons = knownPatrons;
}
public void validate( The object is a patron id
IFormComponent field,
ValidationMessages messages,
Object object) throws ValidatorException {
String patronId = (String) object;
if (!knownPatrons.isKnown(patronId)) {
throw new ValidatorException("Patron not found", null);
}
}
public boolean getAcceptsNull() {
return false; The most important code here. Check if the patron
} code is unknown. If so, throw a ValidatorException.
public void renderContribution( The TextField will catch it and record it as an error in
IMarkupWriter writer, IRequestCycle cycle, the validation delegate.
FormComponentContributorContext context, IFormComponent field) {
}
public boolean isRequired() { If the validator could use Javascript to perform
return false; validation in the browser, it should output the
} script here.
} Will this validator validate a null value just like any
Must the user enter a value? If so, return
other value or always treat a null value as valid so that true here. Tapestry itself doesn't use this
the input can be made optional? Most validators method at all. It allows you to query the
should return false including this one so that the input input components to see if they're
can be made optional. An exception is the validator required. For each required input
named required. It will validate a null value and treat it component you can say put a star next to
as invalid. it to indicate this fact to the user.
The KnownPatrons class is:
public class KnownPatrons {
private Map patronCodeToDiscount;

public KnownPatrons() {
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}
public Integer getDiscount(String patronCode) {
return (Integer) patronCodeToDiscount.get(patronCode);
}
public boolean isKnown(String patronCode) {
return patronCodeToDiscount.containsKey(patronCode);
Validating Input 73

}
}
Modify Home.java to create a KnownPatrons object:
public abstract class Home extends BasePage {
private Map patronCodeToDiscount;
private KnownPatrons knownPatrons;

@InjectPage("Result")
public abstract IPage getResult();
public abstract double getWeight();
public abstract String getPatronCode();
@Bean
public abstract ValidationDelegate getDelegate();

public Home() {
knownPatrons = new KnownPatrons();
patronCodeToDiscount = new HashMap();
patronCodeToDiscount.put("p1", new Integer(90));
patronCodeToDiscount.put("p2", new Integer(95));
}
public KnownPatrons getKnownPatrons() {
return knownPatrons;
}
public IPage onSubmit() {
ValidationDelegate delegate = getDelegate();
double weight = getWeight();
Integer discount = knownPatrons.getDiscount(getPatronCode());
Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode());
if (discount == null && !getPatronCode().equals("")) {
delegate.setFormComponent((IFormComponent) getComponent("patronCode"));
delegate.recordFieldInputValue(getPatronCode());
delegate.record("Patron not found", null);
}
if (delegate.getHasErrors()) {
return null;
}
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg);
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = getResult();
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
In Home.page, you can't use the validators prefix to create this validator because you need to call setKnownPatrons()
on it. So, declare it as a bean:
<page-specification class="com.ttdev.postage.Home">
<bean name="patronCodeValidator" class="com.ttdev.postage.PatronCodeValidator">
<set name="knownPatrons" value="ognl:knownPatrons"/> Call getKnownPatrons() on the
</bean> page object and store the result
<component id="form" type="Form"> into the "knownPatrons"
<binding name="listener" value="listener:onSubmit"/> property of the validator
<binding name="delegate" value="beans.delegate"/>
</component>
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator" value="translator:number,pattern=#.#"/>
<binding name="validators" value="validators:required[The {0} is missing!],min=0"/>
<binding name="displayName" value="literal:Weight"/>
</component> The bean prefix says what is
<component id="patronCode" type="TextField"> following is the name of a bean.
<binding name="value" value="patronCode"/> The value of the whole expression
<binding name="validators" value="bean:patronCodeValidator"/> is that bean object. If you'd like,
</component> you could write
<component id="errors" type="Delegator"> "ognl:beans.patronCodeValidator"
<binding name="delegate" value="beans.delegate.firstError"/> and achieve the same effect.
</component>
<component id="weightLabel" type="FieldLabel">
<binding name="field" value="component:weight"/>
</component>
</page-specification>

Now run the application and the validator should be working:


74 Chapter 3 Validating Input

Again, you could use a FieldLabel for the patron code so that it is turned into red. For the moment you're using the
"bean" binding prefix to access the bean as the validator. What if you need to specify two validators? For example, if
the patron code is required, you'd like to add the "required" validator. How to do that? First, it is incorrect to write:
<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
<binding name="validators" value="validators:required,bean:patronCodeValidator"/>
</component>

Incorrect! You can't have two binding prefixes.

The correct way is:


<component id="patronCode" type="TextField">
<binding name="value" value="patronCode"/>
<binding name="validators" value="validators:required,$patronCodeValidator"/>
</component>

When the "validators" prefix sees the $, it


knows what's following is not the name of a
validator, but the name of a bean. So it will
look up the bean by the name and use it as a
validator.

Showing all the errors


At the moment if both the weight and the patron code are invalid, only the first error (about the weight) is shown. To
show all the errors, modify Home.html:
<html>
<span jwcid="errors"/>
<ul>
<span jwcid="errors">
<li><span jwcid="error"/></li>
</span>
</ul>
<form jwcid="form">
<table>
<tr>
<td><span jwcid="weightLabel">Weight:</span></td>
<td><input type="text" jwcid="weight"/></td>
</tr>
<tr>
<td>Patron code:</td>
<td><input type="text" jwcid="patronCode"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit"/></td>
</tr>
</table>
</form>
</html>
The idea is that this "errors" component will loop through each error in the validation delegate. For each error, it will
output its body, i.e., an <li> element containing an error message. The "error" component can be just a Delegator. Next,
modify Home.page:
Validating Input 75

A For component will loop and


The "source" is a list, an render its body multiple times.
array or any collection. The How many times? This
For component will loop for depends on its "source"
each element. parameter.

<page-specification class="com.ttdev.postage.Home">
...
<component id="errors" type="Delegator"> The getFieldTracking()
<binding name="delegate" value="beans.delegate.firstError"/>
method of the
</component>
validation delegate will
<property name="currentFieldTracking"/>
return a list of its rows.
<component id="errors" type="For">
Each row is called a
<binding name="source" value="beans.delegate.fieldTracking"/>
"field tracking".
<binding name="value" value="currentFieldTracking"/>
</component>
<component id="error" type="Delegator">
<binding name="delegate" value="currentFieldTracking.errorRenderer"/>
</component>
</page-specification>

Get the error renderer For each element, the For


from the field tracking component will set its "value"
and render it parameter to that element. In this
case, it will call
setCurrentFieldTracking() on the
page object and pass that element
to it. This will store that field tracking
into a property named
"currentFieldTracking".
Now run the application and it will seem to be working:

However, if only one input field is invalid, then something is wrong:

Note that there is an empty <li> there. Why? This is because due to the current implementation of TextField, it will
76 Chapter 3 Validating Input

create a field tracking even if there is no error at all. For example, for the above case, the validation delegate would be
like:
Field name Old value Error renderer
weight "5" null
patronCode "p3" An error render that will render itself as a string "Patron not found".
To solve this problem, modify Home.html:
<html>
<ul>
<span jwcid="errors">
<span jwcid="isInError">
<li><span jwcid="error"/></li>
</span>
</span>
</ul>
...
</html>
The idea is that this "isInError" will check the current field tracking to see if its render is not null. If yes, it will go ahead
to render its body. Otherwise it will render nothing (so the <li> will not be displayed). Define this component in
Home.page:
<page-specification class="com.ttdev.postage.Home">
...
<property name="currentFieldTracking"/>
<component id="errors" type="For">
<binding name="source" value="beans.delegate.fieldTracking"/>
<binding name="value" value="currentFieldTracking"/>
</component>
<component id="error" type="Delegator">
<binding name="delegate" value="currentFieldTracking.errorRenderer"/>
</component>
<component id="isInError" type="If">
<binding name="condition" value="currentFieldTracking.inError"/>
</component>
</page-specification>
To decide to render its body or not, the If component will evaluate its "condition" parameter. In this case, it will call
isInError() method on the current field tracking (it will try getInError() first but there is no such method, so it will try
isInError() next). The isInError() will return true if the error renderer is not null. Now try it again and it should work:

Even though it is working, something can be simplified:


Before After
<html> <html>
<ul> <ul>
<span jwcid="errors"> <span jwcid="errors">
<span jwcid="isInError"> <li jwcid="isInError">
<li><span jwcid="error"/></li> <span jwcid="error"/>
</span> </li>
</span> </span>
</ul> </ul>
... ...
</html> </html>
The idea is, by default the If component will render itself as the HTML element it is associated with (<li> in this case).
Validating Input 77

Now run it again and it should continue to work. In addition to the Conditional component, by default the For component
will also render itself as its associated HTML element. For example, the following For component will generate a <tr>
for each value in the source before rendering its body:
<table>
<tr jwcid="XXX">...</tr>
</table>

<component id="XXX" type="For">


<binding name="source" value="..."/>
<binding name="value" value="..."/>
</component>

Using informal parameters


Suppose that you'd like to display the errors in red color. To do that, modify Home.html:
<html>
<ul>
<span jwcid="errors">
<li jwcid="isInError" style="color: red">
<span jwcid="error"/>
</li>
</span>
</ul>
...
</html>
Note that this "style" attribute is a standard HTML attribute. Usually, when a component like "isInError" outputs HTML,
the generated code will completely replace the original element (the <li> element in this case) and therefore the
attribute such as "style" will be lost. However, some components such as For, it will treat the attributes in the original
element as its own extra parameters. Then it will output these extra parameters as attributes in the generated element
(such as the <li> generated). Therefore, the generated code will be like:
<html>
<ul>
<li style="color: red">
Weight must not be smaller than 0.
</li>
...
This kind of extra parameters is called "informal parameters". The other ordinary parameters are called "formal
parameters".
Not all Tapestry components support informal parameters. However, most do. The rule of thumb is, if a component will
generate an HTML element (e.g., a TextField component will generate an <input> element but an Insert component
usually doesn't generate any element), then most likely it will support informal parameters. To be sure, check the
Component documentation coming with Tapestry.

Performing validation using Javascript


At the moment the validation is done on the server. It works but it takes a while before the user can see the errors. It'd
be good to perform the validation in the browser (using Javascript). It is very easy: Just tell the Form to enable client
side validation:
<page-specification class="com.ttdev.postage.Home">
...
<component id="form" type="Form">
<binding name="listener" value="listener:onSubmit"/>
<binding name="delegate" value="beans.delegate"/>
<binding name="clientValidationEnabled" value="true"/>
</component>
</page-specification>
Note that the expression "true" is an OGNL expression. Now, run the application. Unfortunately, it won't work and will
have no effect. Why? As mentioned before, whenever you need to use Javascript, you probably need a Shell
component and definitely a Body component:
<html jwcid="@Shell" title="Postage">
<body jwcid="@Body">
<ul>
<span jwcid="errors">
<li jwcid="isInError" style="color: red">
<span jwcid="error"/>
78 Chapter 3 Validating Input

</li>
</span>
</ul>
...
</body>
</html>
Try it again and enter -20 as the weight:

Errors that don't belong to any input field


Suppose that for a particular patron p1 you will never ship a package that is weighted more than 50kg. To do that,
modify Home.java:
This combination is invalid. Must check if the
patron code is not null. It will be null if it is not a
public abstract class Home extends BasePage { known patron because it will not be set and
... remain at its initial value.
public IPage onSubmit() {
ValidationDelegate delegate = getDelegate();
double weight = getWeight();
Integer discount = knownPatrons.getDiscount(getPatronCode());
if (getPatronCode() != null && getPatronCode().equals("p1") && weight > 50) {
delegate.setFormComponent(null);
delegate.record("Can't ship 50kg or more for p1", ValidationConstraint.CONSISTENCY);
}
if (delegate.getHasErrors()) {State that this error is not
return null; associated with any input field.
}
The error tracking will be like: Field name Old value Error msg
int postagePerKg = 10;
int postage = (int) (weight * postagePerKg); null null "Can't ship 50kg..."
if (discount != null) {
postage = postage * discount.intValue() / 100;
}
IPage resultPage = cycle.getPage("Result");
PropertyUtils.write(resultPage, "postage", new Integer(postage));
return resultPage;
}
}
Run the application and it should work:
Validating Input 79

Note that none of the TextField components are marked as in error.

Validation for a DatePicker component and a TextArea component


Suppose that you'd like to require the user specify the shipping date and a long description of the package:

Both the shipping date and description must be specified. The shipping date must be in the format of yyyy/MM/dd and
must be between December 1, 2006 and May 31, 2007. The description must not exceed 20 characters. To do that,
modify Home.html:
<html jwcid="@Shell" title="Postage">
<body jwcid="@Body">
<ul>
<span jwcid="errors">
<li jwcid="isInError" style="color: red">
<span jwcid="error"/>
</li>
</span>
</ul>
<form jwcid="form">
<table>
<tr>
<td><span jwcid="weightLabel">Weight:</span></td>
<td><input type="text" jwcid="weight"/></td>
</tr>
<tr>
<td>Patron code:</td>
<td><input type="text" jwcid="patronCode"/></td>
</tr>
<tr>
<td>Shipping date:</td>
<td><input type="text" jwcid="shippingDate"/></td>
</tr>
<tr>
80 Chapter 3 Validating Input

<td>Description:</td>
<td><textarea jwcid="desc"></textarea></td>
</tr>
<tr>
<td></td>
<td><input type="submit"/></td>
</tr>
</table>
</form>
</body>
Home.page is:
A minDate validator which will make sure the date is >=
Use a date translator. Set the "pattern" property of the
December 1, 2006. How does Tapestry figure out which
date translator to yyyy/MM/dd, so that the shipping date
number in 12/1/2006 is the year, the month and the day?
value is displayed in this format and it will also expect the
If a number is large (e.g., 2006), it must be the year.
input to be in this format. Internally the date translator
Then for the rest of the numbers, the first number will be
simply uses the Java SimpleDateFormat class to do the
work. So you can lookup its API doc to find other taken as the month and the other as the day. So, you
possible patterns. could have specify the same date as 2006/12/1 without
changing the meaning at all.
<page-specification class="com.ttdev.postage.Home"> A maxDate validator
... which will make sure
<component id="shippingDate" type="DatePicker"> the date is <= May 31,
<binding name="value" value="shippingDate"/> 2005
<binding name="translator" value="translator:date,pattern=yyyy/MM/dd"/>
<binding name="validators" value="validators:required,minDate=12/1/2006,maxDate=5/31/2007"/>
<binding name="displayName" value="literal:Shipping date"/>
</component>
<component id="desc" type="TextArea"> The shipping date is required
<binding name="value" value="desc"/>
<binding name="validators" value="validators:required,maxLength=20"/>
<binding name="displayName" value="literal:Description"/>
</component>
</page-specification>
Provide a display name Limit the length of the
for each so that they description to <= 20
can be used in the error
message (if any) The description is
required
Home.java is:
public abstract class Home extends BasePage {
Define the two
public abstract Date getShippingDate();
properties to store the
public abstract String getDesc();
input
public IPage onSubmit() {
System.out.println(getShippingDate()); Just print them out
System.out.println(getDesc());
...
}
}
Now run the application and it should work:
Validating Input 81

The minDate and maxDate validators will also work:

Other validators
You have seen some validators: required, min, minDate, maxDate, maxLength. In addition, there are other validators
coming with Tapestry. Here is a summary:
Validator Purpose
min Checks if a number is >= the given number.
max Checks if a number is <= the given number.
email Checks if a string is an email.
minLength Checks if the length of a string is >= the given length.
maxLength Checks if the length of a string is <= the given length.
82 Chapter 3 Validating Input

Validator Purpose
pattern Checks if a string matches a "regular expression". For example, to check if the input
string is a name, i.e., consisting of one or more letters (a-z) or digits:
<binding name="validators" value="validators:pattern=\w+"/>
In which \w means a word character (letter or digit) and XXX+ means to expect XXX
one or more times. To accept an empty name:
<binding name="validators" value="validators:pattern=\w*"/>
In which XXX* means to expect XXX zero or more times. If you'd like to check if the
input string is a phone # like 123-4567:
<binding name="validators" value="validators:pattern=\d{3}\-\d{4}"/>
In which \d means a digit and XXX{3} means to expect XXX three times.
minDate Checks if a Date is >= the given Date.
maxDate Checks if a Date is <= the given Date.
For the details, check the API documentation.

Summary
You can present any object in a TextField, as long as you provide a translator. The TextField will use it to translate the
value to a string for display and on form submission, it will use it to translate the string input back into an object to store
into the value. If a translator can't translate the string into a value, it will act like a validator and record an error. Most
translators in Tapestry will treat an empty string as valid and return a null. This is used to allow optional input.
To validate the input in a TextField, just specify a list of validators for the TextField. If any validator considers the value
invalid, it will record a field tracking in the validation delegate. How can the validator find out the validation delegate to
use? It finds it from the surrounding Form component, so you must tell the Form component which validation delegate
to use. Most validators in Tapestry will consider null as valid to allow optional input. If the input is mandatory, you should
use a required validator.
In addition to the TextField component, the DatePicker component and TextArea component also support translation
and validation in exactly the same way.
If the validation is not related to a particular input field, you may do it in your submit listener and record the error into the
validation delegate yourself.
A validation delegate is just a list of field trackings. A field tracking records the name of the HTML input field, the old
value (maybe invalid) and an error renderer. This allows us to re-display the page with the invalid input and error
messages when there are errors. A field tracking is not necessarily an error (i.e., when the error renderer is null).
Usually you will use a bean to hold the validation delegate and maybe some validators that require complicated
initialization. As beans they will be created on demand whenever they're used. Some validators support validation using
Javascript. To use this feature, you need to enable client side validation on the Form component. In addition, you
should have a Shell component and a Body component. In addition to using the validators provided by Tapestry, you
can easily create your own by implementing the Validator interface.
To display the first error in the validation delegate, you can use a Delegator component to delegate the rendering to the
first non-null error renderer. To display all errors, you need to use a For component to loop through all the field
trackings. A For component will loop through its "source" parameter and render its body for each element.
If you'd like to optionally skip a something in the HTML page, you can use an If component. If the condition is true, it will
render its body. Otherwise it will skip it.
You can use a FieldLabel along with a TextField so that it shows itself in red if the TextField is in error. It checks if the
TextField is in error by checking the validation delegate.
You can use informal parameters for many Tapestry components. They will be passed on to the HTML element
generated by that component.
83

Chapter 4
Chapter 4 Creating an e-Shop
84 Chapter 4 Creating an e-Shop

What's in this chapter?


In this chapter you'll learn how to create an e-shop. This involves implementing a global product catalog, a shopping
cart for each user, user login and logout and requiring authenticated access for the checkout page.

Creating an e-shop
Suppose that you'd like to create an e-shop. The front page lists all the products:

As you can see, a product has an id, a name and a price. For example, the first product's id is "p01", its name is
"Pencil" and its price is $1.2. If the user clicks on a product say "Eraser", he will see a detailed description of Eraser:

For simplicity, you will be using strings like "a", "b" and "c" as the detailed descriptions for the products. OK, Let's do it.
First, create a new project named "Shop". Setup the class path, output folder and web.xml as usual. Create a context
descriptor Shop.xml in c:\Tomcat\conf\Catalina\localhost:
<Context
docBase="c:/workspace/Shop/context"
path="/Shop"
reloadable="true"/>
Next, let's display the product listing in the Home page. Where do you get the information of the products? For
simplicity, let's hard code the information. So, create a Catalog class in package com.ttdev.shop:
public class Catalog {
private List products;
private static Catalog globalCatalog;

public Catalog() {
products = new ArrayList();
}
public List getProducts() {
return products;
}
public void add(Product product) {
products.add(product);
}
Creating an e-Shop 85

public static Catalog getGlobalCatalog() {


if (globalCatalog == null) {
globalCatalog = new Catalog();
globalCatalog.add(new Product("p01", "Pencil", "a", 1.20));
globalCatalog.add(new Product("p02", "Eraser", "b", 2.00));
globalCatalog.add(new Product("p03", "Ball pen", "c", 3.50));
}
return globalCatalog;
}
}
Then create a Product class:
public class Product {
private String id;
private String name;
private String desc;
private double price;

public Product(String id, String name, String desc, double price) {


this.id = id;
this.name = name;
this.desc = desc;
this.price = price;
}
}
To list the products, just use a For component to loop through each product. How to display the link to show the details
of a product? For simplicity, let's just show the name of the product and ignore the link at the moment. Home.html
should be like:
<html>
<head>
<title>Shop</title>
</head>
<body> It will be a For component. It will generate a <tr>
<h1>Product listing</h1> element for each product being visited. So, each
<table border="1"> product will appear as a row in the HTML table.
<tr jwcid="products">
<td><span jwcid="id">p01</span></td>
<td><span jwcid="name">Pencil</span></td>
<td><span jwcid="price">1.20</span></td>
</tr>
</table>
</body>
</html>
Home.page should define the components required:
86 Chapter 4 Creating an e-Shop

<page-specification class="com.ttdev.shop.Home">
<property name="currentProduct"/> Store each product into
<component id="products" type="For"> here
<binding name="source" value="products"/>
<binding name="value" value="currentProduct"/>
</component>
<component id="id" type="Insert">
<binding name="value" value="currentProduct.id"/>
</component>
<component id="name" type="Insert">
<binding name="value" value="currentProduct.name"/>
</component>
<component id="price" type="Insert">
<binding name="value" value="currentProduct.price"/>
</component>
</page-specification>
public class Product {
private String id;
Need to provide the private String name;
getters private String desc;
private double price;
...
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
You need to provide a page object in Home.java to provide the list of Product objects:
public abstract class Home extends BasePage {
public List getProducts() {
return Catalog.getGlobalCatalog().getProducts();
}
}
Now, start Tomcat and run the application. You should see:

Showing the product details


Now, let's create the links to show the product details. To create a link, you can use a DirectLink component. First,
modify Home.html to insert the link:
<html>
<head>
<title>Shop</title>
</head>
<body>
<h1>Product listing</h1>
<table border="1">
<tr jwcid="products">
Creating an e-Shop 87

<td><span jwcid="id">p01</span></td>
<td><a href="" jwcid="detailsLink"><span jwcid="name">Pencil</span></a></td>
<td><span jwcid="price">1.20</span></td>
</tr>
</table>
</html>
This <a> element is just for preview only. The "detailsLink" component will generate a completely new <a> element to
replace for it. It means you could use <span> or any other HTML element instead of <a>. Next, define the "detailsLink"
component in Home.page:
<page-specification class="com.ttdev.shop.Home">
<property name="currentProduct"/>
<component id="products" type="For">
<binding name="source" value="products"/>
<binding name="value" value="currentProduct"/>
</component>
<component id="id" type="Insert">
<binding name="value" value="currentProduct.id"/>
</component>
<component id="name" type="Insert">
<binding name="value" value="currentProduct.name"/>
</component>
<component id="price" type="Insert">
<binding name="value" value="currentProduct.price"/>
</component>
<component id="detailsLink" type="DirectLink">
<binding name="listener" value="listener:onClickDetailsLink"/>
</component>
</page-specification>
1: Render 2: Render its
public void onClickDetailsLink() {
itself as an body
...
HTML link ...
}
<a href="...">XXX</a> 3: When the user
clicks the link, the
listener will be called.

As shown above, the DirectLink component will render itself as an HTML link. When the user clicks the link, a request
will be sent to the server to call Tapestry. Tapestry will call the onClickDetailsLink() listener. Therefore, you need to
define such a listener method:
public abstract class Home extends BasePage {
...
public void onClickDetailsLink() {
//show the details page of the product
}
}
In this method you should activate the page that shows the details of the product. For simplicity, let's just print a
message to the console and then do nothing (so the original page is displayed again):
public abstract class Home extends BasePage {
public List getProducts() {
return Catalog.getGlobalCatalog().getProducts();
}
public void onClickDetailsLink() {
System.out.println("DetailsLink was clicked");
}
}
Now, run the program. You should see the links:
88 Chapter 4 Creating an e-Shop

The HTML code should be like:

What does this URL mean?

What does the link "/Shop/app?component=detailsLink&amp;page=Home&amp;service=direct" mean? It calls the


direct service of Tapestry and ask it to get a Home page object, locate the component named "detailsLink" (your
DirectLink component) and call it. It will in turn call your listener.
Clicking on a link will display the page again and a message will be printed in Tomcat's console:
Dec 21, 2004 3:47:32 PM org.apache.catalina.startup.Catalina start
INFO: ServerShop/app?service=direct/0/Home/detailsLink startup in 20470 ms
...
DetailsLink was clicked
Now, let's display the product details. However, all the three links are exactly the same, how can your listener know
which product to display? To solve this problem, you can add the product id as a parameter for each link:
<page-specification class="com.ttdev.shop.Home">
<property name="currentProduct"/>
<component id="products" type="For">
<binding name="source" value="products"/>
<binding name="value" value="currentProduct"/>
</component>
<component id="id" type="Insert">
<binding name="value" value="currentProduct.id"/>
</component>
<component id="name" type="Insert">
<binding name="value" value="currentProduct.name"/>
</component>
<component id="price" type="Insert">
<binding name="value" value="currentProduct.price"/>
</component>
<component id="detailsLink" type="DirectLink">
Creating an e-Shop 89

<binding name="listener" value="listener:onClickDetailsLink"/>


<binding name="parameters" value="currentProduct.id"/>
</component>
</page-specification>
Note that the DirectLink component will render three times inside the For component. When it renders itself, it will
evaluate the expression of "currentProduct.id" and get the id of the product being visited by the For component. This id
(such as "p01") will be encoded as a parameter of the HTML link. If the expression evaluated to an array or a list
instead of a single object, the DirectLink component would encode each element as a separate parameter.
In your case, the generated HTML code will be like:

You don't have to care much about how the parameters are encoded. In your listener, you need to declare each
parameter as an argument in the method signature:
public abstract class Home extends BasePage {
public List getProducts() {
return Catalog.getGlobalCatalog().getProducts();
}
public void onClickDetailsLink(String productId) {
System.out.println("DetailsLink was clicked for product " + productId);
}
}
Tapestry will automatically decode each parameter and pass it to your listener method as an argument. Let's run the
program and click say the "Eraser". You will see the console output:
DetailsLink was clicked
...
INFO: Reloading this Context has started
...
DetailsLink was clicked for product p02
To actually display the details of a product, create a new page named ProductDetails. Modify ProductDetails.html:
<html>
<head>
<title><span jwcid="name">Pencil</span></title>
</head>
<body>
<h1><span jwcid="name2">Pencil</span></h1>
<span jwcid="desc">xxx</span>
</body>
</html>
Note that you'd like to display the name of the product twice: Once as the page title and another as an <h1> heading.
However, you cannot use a single component twice in a template. This is a limitation of Tapestry. You must use two
components. In addition, each component must have a unique name. So, you have named them "name" and "name2"
respectively.
You need to define the component in ProductDetails.page:
90 Chapter 4 Creating an e-Shop

<page-specification class="com.ttdev.shop.ProductDetails">
<component id="name" type="Insert"> 1: Make a copy
<binding name="value" value="name"/>
</component>
<component id="name2" copy-of="name"/> Component "name2" is a copy
<component id="desc" type="Insert"> of "name1".
<binding name="value" value="desc"/> 2: Change the id to
</component> "name2"
</page-specification>
<component id="name2" type="Insert">
<binding name="value" expression="name"/>
</component>
As shown above, component "name2" is just a copy of "name1". You don't need to specify anything else for it, not even
the component type nor the bindings. This trick is handy when you need to use the same component at multiple places
in the same HTML file.
As these components need to get the name and description of the product from the page object, you must use a
custom page object:
public abstract class ProductDetails extends BasePage {
private String productId;
Someone must call setProductId()
public void setProductId(String id) { to tell it the product id before
this.productId = id; getName() and getDesc() are
} called.
public String getName() {
return lookup().getName();
} Lookup the global catalog to find the
public String getDesc() { product object using the product id. Of
return lookup().getDesc(); course, you need to define this
} method.
private Product lookup() {
return Catalog.getGlobalCatalog().lookup(productId);
}
}
Define this lookup() method:
public class Catalog {
private List products;
...
public Product lookup(String productId) {
for (Iterator iter = products.iterator(); iter.hasNext();) {
Product product = (Product) iter.next();
if (product.getId().equals(productId)) {
return product;
}
}
throw new IllegalArgumentException("Unknown product id: " + productId);
}
}
The Product class also needs to have a getDesc() getter:
public class Product {
private String id;
private String name;
private String desc;
private double price;
...
public String getDesc() {
return desc;
}
}
Now, let's display this new page in your listener:
Creating an e-Shop 91

public class Home extends BasePage {


public List getProducts() {
return Catalog.getGlobalCatalog().getProducts();
}
Let Tapestry get a page object
@InjectPage("ProductDetails") for the page named
public abstract ProductDetails getDetailsPage(); "ProductDetails"

public ProductDetails onClickDetailsLink(String productId) {


getDetailsPage().setProductId(productId);
return getDetailsPage();
}
} Tell it the product id
Activate this particular
page object
Now, run the program again and click say "Eraser", you will see:

Setting the packages to look for page classes


In this application, the Home page is associated with the com.ttdev.shop.Home class; the ProductDetails page is
associated with the com.ttdev.shop.ProductDetails class. If later you have another XXX page, it is very likely that it will
be associated with com.ttdev.shop.XXX. Instead of specifying this in each .page file:
<page-specification class="com.ttdev.shop.???">
...
</page-specification>
There is an easier way: Create a file Shop.application in WEB-INF:
<?xml version="1.0"?>
<!DOCTYPE application PUBLIC
"-//Apache Software Foundation//Tapestry Specification 4.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<application>
<meta key="org.apache.tapestry.page-class-packages" value="com.ttdev.shop"/>
</application>

A meta property is
just a configuration If an XXX.page file doesn't specify a
Java class, then look for an XXX class
setting for Tapestry
in the com.ttdev.shop package. You
could specify a list of packages there:

If such an XXX class is not found, use


BasePage.
<... value="com.ttdev.shop,com.ttdev.shop.pages"/>

This file is called the "application specification". It sets some global settings for the whole application. Then you don't
need to set the page class in the .page files. For Home.page:
<page-specification class="com.ttdev.shop.Home">
<property name="currentProduct"/>
<component id="products" type="For">
<binding name="source" value="products"/>
<binding name="value" value="currentProduct"/>
</component>
92 Chapter 4 Creating an e-Shop

<component id="id" type="Insert">


<binding name="value" value="currentProduct.id"/>
</component>
<component id="name" type="Insert">
<binding name="value" value="currentProduct.name"/>
</component>
<component id="price" type="Insert">
<binding name="value" value="currentProduct.price"/>
</component>
<component id="detailsLink" type="DirectLink">
<binding name="listener" value="listener:onClickDetailsLink"/>
<binding name="parameters" value="currentProduct.id"/>
</component>
</page-specification>
For ProductDetails.page:
<page-specification class="com.ttdev.shop.ProductDetails">
<component id="name" type="Insert">
<binding name="value" value="name"/>
</component>
<component id="name2" copy-of="name"/>
<component id="desc" type="Insert">
<binding name="value" value="desc"/>
</component>
</page-specification>
As the application specification is read only once when the application is started, you need to restart it for the change to
take effect. Then run it and it should continue to work.

Implementing a shopping cart


Now, let's allow the user to add products to his shopping cart. Your purpose is that the product details page should be
like:

If the user clicks "Continue shopping", the product listing page will be displayed:

If the user clicks "Add to cart", a single piece of the product is added to his shopping cart, then the contents of his
shopping cart will be displayed:
From there the user should be able to continue shopping or checkout. Now, let's do it. First, add the two buttons to the
ProductDetails.html:
Creating an e-Shop 93

<html>
<head>
<title><span jwcid="name">Pencil</span></title>
</head>
<body>
<h1><span jwcid="name2">Pencil</span></h1>
<span jwcid="desc">xxx</span>
<form jwcid="productActionForm">
<input type="submit" value="Add to cart" jwcid="addToCart"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
</body>
</html>
Note that a button must appear in a form, so you must have a Form component. Then, you may define the components
like this:
<page-specification>
<component id="name" type="Insert">
<binding name="value" value="name"/>
</component>
<component id="name2" copy-of="name"/>
<component id="desc" type="Insert">
<binding name="value" value="desc"/>
</component>
<component id="productActionForm" type="Form">
<binding name="listener" value="listener:actionOnProduct"/>
</component>
<component id="addToCart" type="Submit"/>
<component id="continueShopping" type="Submit"/>
</page-specification>
Of course, for this to work you need to define an actionOnProduct() listener in the page object. However, there is a
problem here: In the listener how can you tell which button was clicked?

Distinguishing which button was clicked


There are three solutions. One is to use one form for each button. Another solution is to add a "tag" to each button:
<page-specification>
...
<component id="productActionForm" type="Form">
<binding name="listener" value="listener:actionOnProduct"/>
</component>
<component id="addToCart" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag" value="0"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag" value="1"/>
</component>
</page-specification>
This way, when the form is submitted, if the "addToCart" button was clicked, the "addToCart" component will call
setButtonClicked(0) on the page object; Similarly, if the "continueShopping" button was clicked, the "continueShopping"
component will call setButtonClicked(1) instead. Therefore, your page object can be written like this:
public abstract class ProductDetails extends BasePage {
private int buttonClicked;
94 Chapter 4 Creating an e-Shop

...
public void setButtonClicked(int buttonClicked) {
this.buttonClicked = buttonClicked;
}
public String actionOnProduct() {
if (buttonClicked==0) {
//add product to cart
...
return null;
}
if (buttonClicked==1) {
return "Home";
}
return null;
}
}
The tag doesn't have to be an int, it can be any object. For example, you could use strings:
<page-specification>
...
<component id="productActionForm" type="Form">
<binding name="listener" value="listener:actionOnProduct"/>
</component>
<component id="addToCart" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag" value="literal:CART"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag" value="literal:SHOP"/>
</component>
</page-specification>
The Java code should change accordingly:
public abstract class ProductDetails extends BasePage {
private String buttonClicked;
...
public void setButtonClicked(String buttonClicked) {
this.buttonClicked = buttonClicked;
}
public String actionOnProduct() {
if (buttonClicked.equals("CART")) {
//add product to cart
//...
return null;
}
if (buttonClicked.equals("SHOP")) {
return "Home";
}
return null;
}
}
You could even use Java static constants:
public abstract class ProductDetails extends BasePage {
static public final String ADD_TO_CART_BUTTON="CART";
static public final String CONTINUE_SHOPPING_BUTTON="SHOP";
private String buttonClicked;
...
public void setButtonClicked(String buttonClicked) {
this.buttonClicked = buttonClicked;
}
public String actionOnProduct() {
if (buttonClicked.equals(ADD_TO_CART_BUTTON)) {
//add product to cart
//...
return null;
}
if (buttonClicked.equals(CONTINUE_SHOPPING_BUTTON)) {
return "Home";
}
return null;
}
}
The tags should be set like this:
So, using a tag to distinguish which button was clicked is the second solution. The third solution is to have a separate
Creating an e-Shop 95

The prefix is not required as it is the default


@ means it is trying to access a static
variable or a static method of a class

<page-specification class="com.ttdev.shop.ProductDetails">
...
<component id="addToCart" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag"
value="ognl:@com.ttdev.shop.ProductDetails@ADD_TO_CART_BUTTON"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag"
value="ognl:@com.ttdev.shop.ProductDetails@CONTINUE_SHOPPING_BUTTON"/>
</component>
</page-specification>

Get the class


com.ttdev.shop.ProductDetails and then
read its ADD_TO_CART_BUTTON static
variable
listener for each button:
No longer need a listener for the
<page-specification>
form's submission
...
<component id="productActionForm" type="Form">
<binding name="listener" value="listener:actionOnProduct"/>
</component>
<component id="addToCart" type="Submit">
<binding name="selected" value="buttonClicked"/> No longer need
<binding name="tag" these
value="ognl:@com.ttdev.shop.ProductDetails@ADD_TO_CART_BUTTON"/>
<binding name="listener" value="listener:addToCart"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="selected" value="buttonClicked"/>
<binding name="tag"
value="ognl:@com.ttdev.shop.ProductDetails@CONTINUE_SHOPPING_BUTTON"/>
<binding name="listener" value="listener:continueShopping"/>
</component>
</page-specification>
Home.java should be:
public abstract class ProductDetails extends BasePage {
static public final String ADD_TO_CART_BUTTON="CART";
static public final String CONTINUE_SHOPPING_BUTTON="SHOP";
private String buttonClicked;
...
public void setButtonClicked(String buttonClicked) {
this.buttonClicked = buttonClicked;
}
public String actionOnProduct() {
if (buttonClicked.equals(ADD_TO_CART_BUTTON)) {
//add product to cart
//...
return null;
}
if (buttonClicked.equals(CONTINUE_SHOPPING_BUTTON)) {
return "Home";
}
return null;
}
public void addToCart() {
//add product to cart
//...
}
public String continueShopping() {
return "Home";
}
}
How does it work? When the form is submitted, the Form component will let each component inside to handle the form
submission in turn. When it lets the "addToCart" component to handle the submission, the "addToCart" component
finds that it was clicked, it will call its listener. Note that if there are other components such as some TextField
96 Chapter 4 Creating an e-Shop

components following the "addToCart", they will not have been rendered yet and the user input will not have been
stored into the page object when the listener is called. If this is a problem, you can modify ProductDetails.page like this:
<page-specification>
...
<component id="addToCart" type="Submit">
<binding name="listener" value="listener:addToCart"/>
<binding name="action" value="listener:addToCart"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="listener" value="listener:continueShopping"/>
<binding name="action" value="listener:continueShopping"/>
</component>
</page-specification>

when the Submit component is rewound, it


finds that it has been clicked. So it will tell
the Form component to call its "action"
listener after the Form component has
finish rewinding.
Up until now you have not shown the code to actually add the product to the shopping cart. Now, let's do it.

Adding a product to the shopping cart


First, in the "addToCart" listener you need to find out which product you are talking about. Can you use the instance
variable of the page object?
public abstract class ProductDetails extends BasePage {
private String productId;
...
public void addToCart() {
//add product to cart. Which product? Can you use the productId variable?
}
}
Let's consider the whole process again (see the diagram below). The product id is set by the last page that is going to
activate the ProductDetails page. When it is activated, it renders itself as an HTML page. When the form is submitted,
Tapestry (the direct service) will get any page object from the pool or create a new page object. That ProductDetails
page object is not necessarily the original ProductDetails page object. So, the "productId" is either undefined (get from
pool) or is null (create new):
1: setProductId("p01")
ProductDetails page
productId: p01

THEY ARE NOT


NECESSARILY
2: Render itself THE SAME
to generate PAGE
HTML page OBJECT!!!

<html>
...
...
...
Tapestry (Direct
</html> 3: The user service) 4: Give me a
submits the form
ProductDetails
page
5: Let the Form or
Submit button call their Page pool
listeners

ProductDetails page ProductDetails page


productId: ???
Creating an e-Shop 97

To solve this problem, you can use a hidden form field to hold the product id:
<html>
...
<form jwcid="productActionForm">
<input type="hidden" jwcid="productId"/>
<input type="submit" value="Add to cart" jwcid="addToCart"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
</body>
</html>
You will use a component of type Hidden to render a hidden form field:
<page-specification>
...
<component id="productId" type="Hidden">
<binding name="value" value="productId"/>
</component>
</page-specification>
How does it work? When the component is rendered (see the diagram below), it will call getProductId() on the page
object to get the product id (suppose that it's p01). Then it will output an HTML hidden form field to store p01 as the
value:
98 Chapter 4 Creating an e-Shop

ProductDetails page 3: call getProductId() and get


productId: p01 "p01" as return

<form ...>
<input
1: Render yourself type="hidden"
name="productId"
4: Output an HTML hidden form value="p01"/>
field ...

Hidden
2: Look, what's my value? Oh,
it's "productId".

...
<page-specification>
...
<component id="productId" type="Hidden">
<binding name="value" value="productId"/>
</component>
</page-specification>
When the form is submitted (see the diagram below), the value in the hidden form field (p01) is included in the HTTP
request. When the Hidden component is asked to handle the form submission, it will get the value p01 from the HTTP
request and then call setProductId() to set the p01 value to the page object. Then the page object will ask your Submit
component to handle the form submission. The latter will call your listener immediately if you're using the "listener"
parameter. At that time you can safely use "productId" in the listener. Of course, this works only if your Hidden
component is placed before your Submit component so that it handles the form submission before the Submit
component does. Otherwise, you may use the "action" parameter of the Submit component.
Creating an e-Shop 99

1: The user submits the form


2: An HTTP request is sent, which
includes the value of "p01".
HTTP request
productId: p01
<html>
<form ...>
<input type="hidden" name="productId" value="p01"/>
...
</form>
</html>
4: What is my value?
ProductDetails page It's "p01"
productId: ???
6: call setProductId("p01")

3: Handle the form


submission

Hidden
7: Handle the form
submission 5: Look, what's my value? Oh,
it's "productId".

8: Call the listener


Submit

<page-specification ...>
...
<component id="productId" type="Hidden">
<binding name="value" value="productId"/>
</component>
</page-specification>
As you already have the "productId" field and the setProductId() method, you only need to add the getter:
public abstract class ProductDetails extends BasePage {
private String productId;
...
public void setProductId(String id) {
this.productId = id;
}
public String getProductId() {
return productId;
}
public void addToCart() {
System.out.println("Trying to add " + productId + " to cart");
}
}
Now, you know which product you're talking about. The next step is to add the product id to the shopping cart. You
could use a Java List to represent the shopping cart (just store the product ids on the List). If you do it this way:
public abstract class ProductDetails extends BasePage {
private String productId;
private List cart;

public ProductDetails() {
cart = new ArrayList();
}
public void addToCart() {
cart.add(productId);
}
}
Then depending on which page object Tapestry picks up from the pool, I may add a product to your shopping cart and
you may add a product to another user's.
You can't use a global shopping cart either:
public class Cart {
public static List cart = new ArrayList();
}
100 Chapter 4 Creating an e-Shop

Because then all users would share a single shopping cart.


The proper way to do it is to store the each user's shopping cart into his own "session". What is a session? A session is
like a Java Map. It can store many objects and each object is indexed with a string key. In addition, for each user
currently using a web application there is a session. For example, if there are currently three users using your e-shop
application, then there are three sessions in Tomcat and they may look like:

Session Key (string) Value (object)


Session for user 1 Some key Some object
Some key Some object
Some key Some object
Session for user 2 Some key Some object
Some key Some object
Session for user 3 Some key Some object
If one of the users just closes the browser and doesn't access the application for a certain period such as 30 minutes
(such a period is called "session timeout"), Tomcat will consider that this user is no longer using the application and will
delete his session.
The idea here is to store the shopping cart (a List) into the session of each user, so that each user will have his
shopping cart. The way to do it in Tapestry is like this: Tapestry contains a table of "application state objects". For your
case, you should add an entry to the table as shown in the diagram for the shopping cart. It says that your shopping
cart object is named "cart", its class is java.util.ArrayList and it should be stored into the current user's session. Later
when your application needs to access the shopping cart for the current user, it can ask Tapestry for an application
state object named "cart". Tapestry looks up the table and finds the entry. So it will create an ArrayList and stores it into
the session of the current user. Finally it returns the cart object (an ArrayList) to your application:

Tapestry Application state objects table


Name Java class Scope
cart java.util.ArrayList session
3: Look it up in the
2: Give me an object named table foo com.ttdev.Foo application
"cart" ... ... ...

Your
application

1: You add an entry to this table


5: Store the ArrayList 4: Create a to let Tapestry know how to
into the session for the java.util.ArrayList create your cart object.
current user under the
named "cart"

Name Object
cart ArrayList
foo
... ...

Session for the current user


If later your application asks for the object named "cart" again, Tapestry will just return the existing one in the session (if
any).
What if the scope were set to "application", then the object would be stored into as a global variable in your application
so all users would share the same object.
To do that, create a folder META-INF in the src folder and then create a file hivemodule.xml in there:
Creating an e-Shop 101

Request to add a row (a "contribution") to the


application objects table (a "configuration"
named tapestry.state.ApplicationObjects)

<?xml version="1.0"?>
<module id="com.ttdev.shop" version="1.0.0">
<contribution configuration-id="tapestry.state.ApplicationObjects">
<state-object name="cart" scope="session">
<create-instance class="java.util.ArrayList"/>
</state-object>
</contribution>
</module>

Name Java class Scope


cart java.util.ArrayList session
foo com.ttdev.Foo application
... ... ...
The whole file is called a "hivemind module descriptor". What is a module? For example, all the classes in your
application (com.ttdev.shop.*) form a module. The classes forming tapestry.jar is also a module (because it also uses
Hivemind). At startup of your application, Hivemind will look for files at the resource path /META-INF/hivemodule.xml
on the classpath. What does that mean? For your application, the classpath includes the folder
c:/workspace/Shop/context/WEB-INF/classes, every single jar file in c:/workspace/Shop/context/WEB-INF/lib (you
should have nothing there) and every single jar file c:/tomcat/share/lib (you have quite many there, including
tapestry.jar). Hivemind will look for the following files inside these folders and/or jar files:
c: c:
workspace tomcat
Shop share
context lib
WEB-INF Resource path tapestry.jar
classes META-INF
META-INF hivemodule.xml
hivemodule.xml
org
com apache
ttdev tapestry
shop
html
Home
BasePage
...
...
In your case, such two files do exist and will be loaded. Each module must have a unique id. Usually you will just use
the Java package of your application as the module id (in your case com.ttdev.shop) but you're free to use something
else as long as it is unique.
Modify the Java code to ask Tapestry for the "cart" application state object:
public abstract class ProductDetails extends BasePage {
private String productId;
Tell Tapestry that you would like to get an application
@InjectState("cart") state object named "cart", through this abstract getter.
public abstract List getCart();

public void addToCart() {


System.out.println("Trying to add " + productId + " to cart");
getCart().add(productId);
}
...
}
You don't have to use annotations. You could achieve the same effect in ProductDetails.page:
102 Chapter 4 Creating an e-Shop

public abstract class ProductDetails extends BasePage {


private String productId;

@InjectState("cart")
public abstract List getCart();

public void addToCart() {


System.out.println("Trying to add " + productId + " to cart");
getCart().add(productId);
The name of the
} The name of the property
... application state object
}
<page-specification class="com.ttdev.shop.ProductDetails">
<inject property="cart" type="state" object="cart"/>
...
</page-specification>
Requesting for an
application state object
Now, reload the application. This is required because the module descriptors are read only once when the application is
started. For your changes to take effect, you must reload it.
If you try to add a product to the shopping cart, it should work but you just won't know because the same product
details page is displayed. Next, let's display the shopping cart. First, create a new page named Cart. Cart.html should
be like:
<html>
<head>
<title>Shopping cart</title>
</head>
<body>
<h1>Shopping cart</h1>
<table border="1">
<tr jwcid="products">
<td><span jwcid="id">p01</span></td>
<td><span jwcid="name">Pencil</span></td>
<td><span jwcid="price">1.20</span></td>
</tr>
</table>
<form jwcid="cartActionForm">
<input type="submit" value="Checkout" jwcid="checkout"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
</body>
</html>
There is nothing special here. It is just like the Home page and is displaying a list of the products. Then modify
Cart.page as:
<page-specification>
<component id="products" type="For">
<binding name="source" value="products"/>
<binding name="value" value="currentProduct"/>
</component>
<component id="id" type="Insert">
<binding name="value" value="currentProduct.id"/>
</component>
<component id="name" type="Insert">
<binding name="value" value="currentProduct.name"/>
</component>
<component id="price" type="Insert">
<binding name="value" value="currentProduct.price"/>
</component>
<component id="cartActionForm" type="Form"/>
<component id="checkout" type="Submit">
<binding name="listener" value="listener:onCheckout"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="listener" value="listener:continueShopping"/>
</component>
</page-specification>
There is nothing special here either. Next, create Cart.java:
Creating an e-Shop 103

public abstract class Cart extends BasePage {


private Product currentProduct;

@InjectState("cart") Retrieve the shopping cart for the current


public abstract List getCart(); user from the session

public List getProducts() {


List cart = getCart();
List products = new ArrayList(); As the shopping cart
for (Iterator iter = cart.iterator(); iter.hasNext();) { contains a list of product
String productId = (String) iter.next(); id's only, you need to
products.add(Catalog.getGlobalCatalog().lookup(productId)); lookup each to find the
} Product object and build
return products; up a list of Product
} objects.
public Product getCurrentProduct() {
return currentProduct;
}
public void setCurrentProduct(Product currentProduct) {
this.currentProduct = currentProduct;
}
public void onCheckout() {
//checkout. Leave it unimplemented for the moment
}
public String continueShopping() {
return "Home";
}
}
Now, change ProductDetails.java to display the Cart page:
public abstract class ProductDetails extends BasePage {
...
public String addToCart() {
getCart().add(productId);
return "Cart";
}
}
Now run the application and add the "Eraser", you may see something like:

Of course, if you have added more products you will see all of them listed here. This is because the session is
accumulating the product id's, and even after the web application is reloaded, the session is not affected at all. In fact,
even if you restart Tomcat, the session is still there because Tomcat will save it to disk and load it back later (for this to
work, the objects you store into the session must implement Serializable. In this case, as the "cart" object is an
ArrayList which does implement Serializable, so it will be fine). To get rid of the old session and get a new one, you may
wait say 30 minutes, but an easier way is to close the browser and open a new one. But how does it work? To
understand it, you need to understand how Tomcat and the browser co-operate to maintain the session.

How Tomcat and the browser maintain the session


Before further explanation, let's add a product to the shopping cart and then check the "cookies" stored in your
browser. For example, for FireFox, choose "Tools | Options":
104 Chapter 4 Creating an e-Shop

Click "Privacy" on the top, click "Show Cookies". Locate the site "localhost" and you'll find a cookie whose name is
"JSESSIONID". This is how Tomcat and the browser maintain the session:

When a user first accesses a web application, Tomcat will generate a random number called "session id" (as shown in
the screen shot above, the id is "57D6E808...") and use it to identify the session. That is, inside Tomcat the sessions
(storage areas) may be like:
Creating an e-Shop 105

Session id Key (string) Value (object)


XXX Some key Some object
Some key Some object
Some key Some object
YYY Some key Some object
Some key Some object
57D6E808... <nothing yet> <nothing yet>
Then it sends this session id back to the browser and tell it to save the session id in a cookie named "JSESSIONID". In
addition, Tomcat tells the browser to associate the cookie with the host "localhost" (see the screen shot below) and with
the path /Shop:

The path /Shop is the context path of the web application you set in c:\Tomcat\conf\Catalina\localhost\Shop.xml:
<Context
docBase="c:/workspace/Shop/context"
path="/Shop"
reloadable="true"/>
Later when the browser accesses any page of the application (e.g., http://localhost/Shop/app), the browser finds that
there is a cookie associated with this host ("localhost") and that the path /Shop/app being accessed is somewhere
under the path associated with the cookie (/Shop), so it will send the content of the cookie (the session id) to the server.
When Tomcat receives the session id, it can find out which session to use with the id.
It means that if you delete this cookie and then access the application again, Tomcat will treat you as a new user. But
why restarting the browser also works? The cookies are stored on disk and so they are persistent. So, usually
restarting the browser will not delete them. However, each cookie has a maximum age (in seconds). For example, see
the "Expires" field of another cookie shown below:
106 Chapter 4 Creating an e-Shop

If that age is set to -1, it means the browser should delete the cookie when the browser is closed. As shown in the
screen shot below, this is exactly the case with your JSESSIONID cookie ("at end of session"):

Now, you understand how a session is maintained. Let's do one more experiment. Restart the browser and try to
access the application:
Creating an e-Shop 107

Then check if the JSESSIONID cookie is there. Surprisingly, you won't find it. Now, try to add a product to the shopping
cart:

Now check if the cookie is there and you will find it. Why is it so? The previous description on how session is created
was not very accurate. In fact, to save memory, Tomcat will not create a session when a user first accesses a web
application. Instead, it waits until the first time when the web application tries to access (read or write) a session, then it
will create the session on the fly. In your case, this occurs when you call getCart() to ask Tapestry for the application
state object named "cart" (when responding to the user's click on "Add to cart"):
public abstract class ProductDetails extends BasePage {
...
public String addToCart() {
getCart().add(productId);
return "Cart";
}
}

Another way to maintain a session


Note that using a cookie to maintain a session is not the only possible way, although it is the most common way.
Another way is called "URL rewriting". To see how URL rewriting works, clear "Accept cookies from sites":
Failing to set the cookies, Tomcat will resort to URL rewriting. Then try to add a product to the shopping cart:
108 Chapter 4 Creating an e-Shop

A session will have been created. But how is it maintained? Let's view the HTML source to the web page. It should be
like:

As you can see above, the session id has been added to the action URL of the form. This way, when the form is
submitted, Tomcat will receives the session id. When the next page is rendered, Tomcat will also automatically add the
same session id to the URLs. It means the session is maintained using "URL rewriting". In this case, to get rid of the
session, closing the browser will also work.

Unified method to let a page remember its data


At the moment your product details page is like this:

The most important point to note here is that a hidden form field is used to store the product id (p02 in this case) so that
Creating an e-Shop 109

when the user clicks on "Add to cart", the listener can find out the product id. What if you'd like to add a link to refresh
the page to make sure the latest data is displayed:

To do that, you can use a direct link and encode the product id as a parameter. However, note that you're using two
methods to transfer the product id back to that page (hidden field and query parameter). All you'd like is that the
ProductDetails page can "remember" the product id, no matter it is activated by a form submission or by a direct link.
Fortunately, Tapestry allows you to do that very easily. First (see the diagram below), in the ProductDetails page you
declare the productId property as a persistent property. Then when the ProductDetails page renders itself, Tapestry will
note that it has such a persistent property, then it will add a hidden form field to each <form> and a query parameter to
each link in the HTML code to store the product id:

Tapestry
4: Generate hidden fields for each <form>
and a query parameter for each link on the
HTML page
3: Look, it has a 1: Render yourself
persistent property.

<html>
...
ProductDetails page <form>
2: Output HTML page <input type="hidden"
Persistent properties name="ProductDetails-productId"
value="p01"/>
productId: p01 ...
</form>
...
<a href="/foo/bar?ProductDetails-
productId=p01">...</a>
</html>
When any form in that HTML page is submitted or when any of the link is clicked, the product id will be included in the
HTTP request. if the request should be handled by a ProductDetails, Tapestry will try to get one from the page pool.
Then it notes that the ProductDetails page has such a persistent property, so it will retrieve the product id from the
HTTP request and store it into the ProductDetails page:
110 Chapter 4 Creating an e-Shop

1: The user submits a


form or clicks a link 2: An HTTP request is sent, which
includes the value of "p01". HTTP request
ProductDetails-
<html> productId: p01
...
<form> 5: Tell me the
<input type="hidden" product id for page
name="ProductDetails-productId" ProductDetails
value="p01"/> 6: Set the product
... id to p01
</form> Tapestry
...
<a href="/foo/bar?ProductDetails-
productId=p01">...</a>
</html>
3: Get a ProductDetails from the pool
4: Look, it has a
persistent property.
Page pool

ProductDetails page ProductDetails page


Persistent properties productId: ???

productId: ???

To implement this idea, modify ProductDetails.java:


public abstract class ProductDetails extends BasePage {
private String productId;

public void setProductId(String id) { Don't need this anymore. Let Tapestry
this.productId = id; manage the property for you.
}
public String getProductId() { This is a persistent property. "client" means
return productId; that the data will be stored in the HTML
} code in the browser (i.e., client).
@Persist("client")
public abstract String getProductId();
public abstract void setProductId(String productId);

@InjectState("cart")
public abstract List getCart();
The setter is not required in general, but the
public String addToCart() { Home page needs to call it to set the
getCart().add(getProductId()); product id.
return "Cart";
}
public String continueShopping() {
return "Home";
}
public String getName() {
return lookup().getName();
}
public String getDesc() {
return lookup().getDesc();
}
private Product lookup() {
return Catalog.getGlobalCatalog().lookup(getProductId());
}
}
Modify ProductDetails.html to add the refresh link and delete the hidden field:
<html>
<head>
<title><span jwcid="name">Pencil</span></title>
</head>
<body>
<h1><span jwcid="name2">Pencil</span></h1>
<span jwcid="desc">xxx</span>
Creating an e-Shop 111

<form jwcid="productActionForm">
<input type="hidden" jwcid="productId"/>
<input type="submit" value="Add to cart" jwcid="addToCart"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
<a href="" jwcid="refresh">Refresh</a>
</body>
</html>
ProductDetails.page is:
<page-specification>
<component id="name" type="Insert">
<binding name="value" value="name"/>
</component>
<component id="name2" copy-of="name"/>
<component id="desc" type="Insert">
<binding name="value" value="desc"/>
</component>
<component id="productActionForm" type="Form">
</component>
<component id="addToCart" type="Submit">
<binding name="listener" value="listener:addToCart"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="listener" value="listener:continueShopping"/>
</component>
<component id="productId" type="Hidden"> It will render itself as an HTML
<binding name="value" value="productId"/> link that will activate a page. The
</component> name of the page is specified
<component id="refresh" type="PageLink"> using its "page" parameter.
<binding name="page" value="literal:ProductDetails"/>
</component>
</page-specification>
Activate the ProductDetails page. The literal
prefix is required otherwise it will try to call
getProductDetails() to get the page name.
Now run the application and both the add to cart function and the refresh function should work. If you check the HTML
code generated:

You can see that all the persistent properties are packed and encoded together and stored under the name
state:<PAGE NAME> (state:ProductDetails in this case).
112 Chapter 4 Creating an e-Shop

Storing persistent property into the session


At the moment you're storing the persistent property into the client (browser). If you'd like, it is possible to store it into
the session:
public abstract class ProductDetails extends BasePage {
@Persist("session")
public abstract String getProductId();
public abstract void setProductId(String productId);
...
}
The way it works is very similar. However, you must be very careful when you do that. For example, if you store the
product id into the session. Then, something like this may happen: First, the user views the details of p01. So, p01 is
stored into the session. Then he chooses to continue shopping and lists all the products. Then he views the details of
p02. So, p02 is stored into the session, overwriting p01 originally stored there. Now, he clicks the "Back" button in the
browser twice to go back to the details page for p01. Then he clicks "Add to cart" button. He thinks that he is adding
p01 to the shopping cart, but actually it is p02 that is in the session, so p02 is added instead, creating a huge surprise.
The problem here is that if you store something into the session, it should be something per-user instead of something
per-page. Clearly the current product id is a per-page thing, not a per-user thing. So using a client persistent property is
a much better choice.

Implementing checkout
You have implemented the shopping cart. Now, let's add the checkout function. Suppose that for a user to checkout, he
must first create an account with you (to enter his credit card # and etc.) and login. For simplicity, let's assume that
there are already some existing user accounts:
User id Email Password Credit card #
u001 paul@yahoo.com aaa 1111 2222 3333 4444
u002 john@hotmail.com bbb 2222 3333 4444 5555
u003 mary@gmail.com ccc 3333 4444 5555 6666
Suppose that a user can click a login link on the Home page:

Then the Home page is displayed again. When a user tries to checkout, if he already logged in, he only needs to
confirm the checkout:
Creating an e-Shop 113

But if he hasn't logged in when trying to checkout, he will be asked to login first, then he will see confirm page:

Now, let's add the login link on the Home page. Modify Home.html:
<html>
<head>
<title>Shop</title>
114 Chapter 4 Creating an e-Shop

</head>
<body>
<h1>Product listing</h1>
<table border="1">
<tr jwcid="products">
<td><span jwcid="id">p01</span></td>
<td><a href="" jwcid="detailsLink"><span jwcid="name">Pencil</span></a></td>
<td><span jwcid="price">1.20</span></td>
</tr>
</table>
<p>
<a href="" jwcid="loginLink">Login</a>
</body>
</html>
Define the "loginLink" component in Home.page:
<page-specification>
...
<component id="loginLink" type="PageLink">
<binding name="page" value="literal:Login"/>
</component>
</page-specification>
For this to work, create a new page named Login. Login.html is like:
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form jwcid="loginForm">
<table border="0">
<tr><td>Email:</td><td><input type="text" jwcid="email"/></td></tr>
<tr><td>Password:</td><td><input type="password" jwcid="password"/></td></tr>
<tr><td></td><td><input type="submit" value="Login"/></td></tr>
</table>
</form>
</body>
</html>
Define the components in Login.page:
<page-specification>
<component id="loginForm" type="Form">
<binding name="listener" value="listener:onLogin"/>
</component> Handle the submission in the
<component id="email" type="TextField"> listener of the Form. So, no
<binding name="value" value="email"/> need to make the submit
</component> button a component at all.
<component id="password" type="TextField">
Set "hidden" to true so that it will
<binding name="value" value="password"/>
render itself as a password input
<binding name="hidden" value="true"/>
field instead of a regular text input
</component>
field.
</page-specification>

Login.java is like:
public abstract class Login extends BasePage {
private String email;
private String password;

public String onLogin() {


try {
User user = Users.getKnownUsers().getUser(email, password);
//remember that this user has logged in.
return "Home";
} catch (AuthenticationException e) {
//display an error and show the Login page again.
return null;
}
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
Creating an e-Shop 115

this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
You need to define the classes required:
public class User {
private String id;
private String email;
private String password;
private String creditCardNo;

public User(String id, String email, String password, String creditCardNo) {


this.id = id;
this.email = email;
this.password = password;
this.creditCardNo = creditCardNo;
}
public boolean authenticate(String email, String password) {
return this.email.equals(email) && this.password.equals(password);
}
}

public class Users {


private List users;
private static Users knownUsers;

public Users() {
users = new ArrayList();
}
public void add(User user) {
users.add(user);
}
public User getUser(String email, String password) {
for (Iterator iter = users.iterator(); iter.hasNext();) {
User user = (User) iter.next();
if (user.authenticate(email, password)) {
return user;
}
}
throw new AuthenticationException();
}
public static Users getKnownUsers() {
if (knownUsers == null) {
knownUsers = new Users();
knownUsers.add(new User("u001", "paul@yahoo.com", "aaa", "1111 2222 3333 4444"));
knownUsers.add(new User("u002", "john@hotmail.com", "bbb", "2222 3333 4444 5555"));
knownUsers.add(new User("u003", "mary@gmail.com", "aaa", "3333 4444 5555 6666"));
}
return knownUsers;
}
}

public class AuthenticationException extends RuntimeException {


private static final long serialVersionUID = -1670128825240124508L;
}
Now, the question is, in the onLogin() listener, what to do if the user is logged in successfully? You should remember
that he has logged in so that when he checks out, you will not ask him to login again. The best solution is to save his
user id or even his User object into the session. This is easy. Just make it another application state object. Modify
hivemodule.xml:
<?xml version="1.0"?>
<module id="com.ttdev.shop" version="1.0.0">
<contribution configuration-id="tapestry.state.ApplicationObjects">
<state-object name="cart" scope="session">
<create-instance class="java.util.ArrayList"/>
</state-object>
</contribution>
<contribution configuration-id="tapestry.state.ApplicationObjects">
116 Chapter 4 Creating an e-Shop

<state-object name="user" scope="session">


<create-instance class="com.ttdev.shop.User"/>
</state-object>
</contribution>
</module>
This way, the table of application state objects will become:
Name Java class Scope
cart java.util.ArrayList session
user com.ttdev.shop.User session
... ... ...
For Tapestry to create the User object, it must have a constructor with no arguments. It also needs to implement
Serializable so that it can saved along with the session:
public class User implements Serializable {
private static final long serialVersionUID = 1858318434177120215L;
private String id;
private String email;
private String password;
private String creditCardNo;

public User() {
}
public User(String id, String email, String password, String creditCardNo) {
this.id = id;
this.email = email;
this.password = password;
this.creditCardNo = creditCardNo;
}
public boolean authenticate(String email, String password) {
return this.email.equals(email) && this.password.equals(password);
}
}
When implementing Serializable, you're strongly recommend to have a static serial version UID. The value can be any
long value. Whenever you change the class that will change its serialized format such as adding a new field, you should
change the version UID. This way if someone sends you a serialized version that is of a different UID, Java will note
that and throw an exception.
Now, after the user logging in, you can save the User object into the session:
public abstract class Login extends BasePage {
private String email;
private String password;

@InjectState("user")
public abstract User getUser();

public String onLogin() {


try {
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return "Home";
} catch (AuthenticationException e) {
//display an error and show the Login page again.
return null;
}
}
...
}
You need to implement the copyFrom() method in User.java:
public class User implements Serializable {
private static final long serialVersionUID = 1858318434177120215L;
private String id;
private String email;
private String password;
private String creditCardNo;

public User() {
}
public User(String id, String email, String password, String creditCardNo) {
this.id = id;
this.email = email;
this.password = password;
this.creditCardNo = creditCardNo;
Creating an e-Shop 117

}
public boolean authenticate(String email, String password) {
return this.email.equals(email) && this.password.equals(password);
}
public void copyFrom(User user) {
this.id = user.id;
this.email = user.email;
this.password = user.password;
this.creditCardNo = user.creditCardNo;
}
}
Now, restart the application so that the hivemodule.xml is read again. Then test run and try to login using a valid
account. It should work. But what if you use an invalid email or password? You can make use a validation delegate to
display the error. Modify Login.html:
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<span style="color: red"><span jwcid="errorMsg"/></span>
<form jwcid="loginForm">
<table border="0">
<tr><td>Email:</td><td><input type="text" jwcid="email"/></td></tr>
<tr><td>Password:</td><td><input type="password" jwcid="password"/></td></tr>
<tr><td></td><td><input type="submit" value="Login" jwcid="login"/></td></tr>
</table>
</form>
</body>
</html>
Define the "errorMsg" component and the ValidationDelegate bean in Login.page:
<page-specification>
<component id="loginForm" type="Form">
<binding name="listener" value="listener:onLogin"/>
</component>
<component id="email" type="TextField">
<binding name="value" value="email"/>
</component>
<component id="password" type="TextField">
<binding name="value" value="password"/>
<binding name="hidden" value="true"/>
</component>
<component id="errorMsg" type="Delegator">
<binding name="delegate" value="beans.delegate.firstError"/>
</component>
</page-specification>
Provide the validation delegate bean and record any error message in Login.java:
public abstract class Login extends BasePage {
private String email;
private String password;

@InjectState("user")
public abstract User getUser();

@Bean
public abstract ValidationDelegate getDelegate();

public String onLogin() {


try {
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return "Home";
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
return null;
}
}
...
}
Now, let's work on the checkout function on the Cart page. At the moment the onCheckout() listener in Cart.java is
empty. What it should really do is to check if the user has logged in. If yes, send him to a Confirm page showing the
118 Chapter 4 Creating an e-Shop

total amount and letting him to confirm the purchase. If not, send him to the Login page. That is:
public abstract class Cart extends BasePage {
...
@InjectStateFlag("user")
public abstract boolean getUserExists();

public String onCheckout() { Check if there is an application state object


if (getUserExists()) { named "user" exists (in any scope). The
return "Confirm"; method must return a boolean and must be
} else { named "getXXX" or "isXXX".
return "Login";
} Need to create this
} Confirm page later
}

Of course, you don't have to use annotations to inject a state flag. You can achieve the same effect in Cart.page:
<page-specification>
<inject type="state-flag" property="userExists" object="user"/>
...
</page-specification>
Next, you need to create a Confirm page. Confirm.html should be like:
<html>
<head>
<title>Confirmation</title>
</head>
<body>
<h1>Confirm your order</h1>
You're going to pay <span jwcid="total">100</span> with your
credit card <span jwcid="creditCardNo">xxxx yyyy zzzz</span>.
<p>
<form jwcid="confirmForm">
<input type="submit" value="Confirm" jwcid="confirm"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
</body>
</html>
Confirm.page is like:
<page-specification>
<component id="total" type="Insert">
<binding name="value" value="total"/>
</component>
<component id="creditCardNo" type="Insert">
<binding name="value" value="creditCardNo"/>
</component>
<component id="confirmForm" type="Form"/>
<component id="confirm" type="Submit">
<binding name="listener" value="listener:onConfirm"/>
</component>
<component id="continueShopping" type="Submit">
<binding name="listener" value="listener:onContinueShopping"/>
</component>
</page-specification>
Confirm.java is like:
public abstract class Confirm extends BasePage {
@InjectState("cart")
public abstract List getCart();

@InjectState("user")
public abstract User getUser();

public double getTotal() {


double total = 0;
for (Iterator iter = getCart().iterator(); iter.hasNext();) {
String productId = (String) iter.next();
total += Catalog.getGlobalCatalog().lookup(productId).getPrice();
}
return total;
}
Creating an e-Shop 119

public String getCreditCardNo() {


return getUser().getCreditCardNo();
}
public String onConfirm() {
//place the order.
System.out.println("Placing the order...");
return "Home";
}
public String onContinueShopping() {
return "Home";
}
}
Define the getCreditCardNo() method in the User class:
public class User implements Serializable {
private static final long serialVersionUID = 1858318434177120215L;
private String id;
private String email;
private String password;
private String creditCardNo;

public User() {
}
public User(String id, String email, String password, String creditCardNo) {
this.id = id;
this.email = email;
this.password = password;
this.creditCardNo = creditCardNo;
}
public boolean authenticate(String email, String password) {
return this.email.equals(email) && this.password.equals(password);
}
public void copyFrom(User user) {
this.id = user.id;
this.email = user.email;
this.password = user.password;
this.creditCardNo = user.creditCardNo;
}
public String getCreditCardNo() {
return creditCardNo;
}
}
Now test run it. Login, add some products, checkout and confirm. It should work fine:
120 Chapter 4 Creating an e-Shop

If the user tries to checkout but he is not logged in yet, he will be sent the Login page:
public abstract class Cart extends BasePage {
...
public String onCheckout() {
if (getUserExists()) {
return "Confirm";
} else {
return "Login";
}
}
}
After logging in, he should be sent to the Confirm page to continue the checkout process:

Login

Not logged in yet


Continue the process

Cart Confirm
Checkout Already logged in

But this is not what is done at the moment:


public abstract class Login extends BasePage {
private String email;
private String password;
...
public String onLogin() {
try {
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return "Home";
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
return null;
}
}
}
That is, after the user logs in successfully, he will be sent to the Home page instead of the Confirm page. To solve this
problem, modify Login.java:
public abstract class Login extends BasePage {
private String email;
private String password;
private String nextPage = "Home";
Creating an e-Shop 121

public void setNextPage(String nextPage) {


this.nextPage = nextPage;
}
@InjectState("user")
public abstract User getUser();

@Bean
public abstract ValidationDelegate getDelegate();

public String onLogin() {


try {
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return nextPage;
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
return null;
}
}
...
}
Set the next page before activating it:
public abstract class Cart extends BasePage {
private Product currentProduct;

@InjectState("cart")
public abstract List getCart();

public List getProducts() {


List cart = getCart();
List products = new ArrayList();
for (Iterator iter = cart.iterator(); iter.hasNext();) {
String productId = (String) iter.next();
products.add(Catalog.getGlobalCatalog().lookup(productId));
}
return products;
}
public Product getCurrentProduct() {
return currentProduct;
}
public void setCurrentProduct(Product currentProduct) {
this.currentProduct = currentProduct;
}
@InjectStateFlag("user")
public abstract boolean getUserExists();

@InjectPage("Login")
public abstract Login getLoginPage();

@InjectPage("Confirm")
public abstract Confirm getConfirmPage();

public IPage onCheckout() {


if (getUserExists()) {
return "Confirm";
return getConfirmPage();
} else {
Login login = getLoginPage();
login.setNextPage("Confirm");
return login;
}
}
public String continueShopping() {
return "Home";
}
}
Note that as the onCheckout() method must return a string or a page object. So you need to standardize on returning a
page.
122 Chapter 4 Creating an e-Shop

However, this will not work. The problem is, the "nextPage" variable is indeed properly set when the Login page renders
itself. But when the login form is submitted, Tapestry may get a different Login page from the pool or even create a new
page object. So the value of "nextPage" is lost. To solve this problem, you can either use a hidden field to store the
value of "nextPage" in a hidden form field or make it a client persistent property. Let's take the latter approach as it is
easier:
public abstract class Login extends BasePage {
private String email;
private String password;
private String nextPage = "Home";

public void setNextPage(String nextPage) {


this.nextPage = nextPage;
}
@Persist("client")
public abstract String getNextPage();
public abstract void setNextPage(String nextPage);

@InjectState("user")
public abstract User getUser();

@Bean
public abstract ValidationDelegate getDelegate();

public String onLogin() {


try {
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return getNextPage() != null ? getNextPage() : "Home";
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
return null;
}
}
...
}
Note that using a persistent property you can't set the initial value easily. So, you simply check if the next page is null. If
so, return the string "Home". Now restart the browser and run the application. Try to checkout without logging in. It
should ask you to login and then return you to the Confirm page:
Creating an e-Shop 123

Letting the Confirm page protect itself


At the moment the Confirm page is quite insecure. It should be available only after the user has logged in. But it is not
protecting itself. It is depending on the Cart page to do the checking. It means a malicious or careless user could
bypass the Cart page and try to activate the Confirm page directly using
http://localhost:8080/Shop/app?service=page&page=Confirm:

Login

Not logged in yet


Continue the process

Cart Confirm
Checkout Already logged in

Activate
Malicious user

To solve this problem, the Confirm page should really do the checking to protect itself:
124 Chapter 4 Creating an e-Shop

Login

If not logged in yet Continue the process

Cart Confirm
Checkout Activate

Activate

Malicious user

To do that, modify Confirm.java:


public abstract class Confirm extends BasePage implements PageValidateListener {
...
@InjectStateFlag("user")
public abstract boolean getUserExists(); If a page implements this interface, it
needs to implement the
@InjectPage("Login") pageValidate() method which will be
public abstract Login getLoginPage(); called just after the page is activated.
You can perform various checking to
public void pageValidate(PageEvent event) { see if you are willing to be activated.
if (!getUserExists()) {
Login login = getLoginPage();
login.setNextPage("Confirm");
throw new PageRedirectException(login);
}
}
}
Check if there is a User object in the This will activate the Login page. Why The URL displayed in the
session. If yes, fine. If not, throw a not just call activate()? As the rendering browser will not be changed
PageRedirectException to activate the has begun, calling activate() will have no because the redirection
Login page. Before that, tell the Login effect. To interrupt the rendering of the happens inside Tapestry
page to return to this Confirm page current page and render another page, only.
after login. you need to throw a
PageRedirectException.

As the Confirm page is now protecting itself, your Cart page no longer needs to protect the Confirm page:
public abstract class Cart extends BasePage {
...
@InjectStateFlag("user")
public abstract boolean getUserExists();

@InjectPage("Login")
public abstract Login getLoginPage();

@InjectPage("Confirm")
public abstract Confirm getConfirmPage();

public String IPage onCheckout() {


return "Confirm";
if (getUserExists()) {
return getConfirmPage();
} else {
Login login = getLoginPage();
login.setNextPage("Confirm");
return login;
}
}
}
Creating an e-Shop 125

Now run the program and it should work equally well.

Calling back a page that takes parameters


Suppose that you'd like to add a "Login" link to the ProductDetails page:

After logging in, you'd like to return the user to the original ProductDetails page:

To do that, modify ProductDetails.html:


<html>
<head>
<title><span jwcid="name">Pencil</span></title>
</head>
<body>
<h1><span jwcid="name2">Pencil</span></h1>
<span jwcid="desc">xxx</span>
<form jwcid="productActionForm">
<input type="submit" value="Add to cart" jwcid="addToCart"/>
<input type="submit" value="Continue shopping" jwcid="continueShopping"/>
</form>
<a href="" jwcid="refresh">Refresh</a>
<p>
<a href="" jwcid="loginLink">Login</a>
</body>
</html>
ProductDetails.page is:
<page-specification>
...
<component id="loginLink" type="DirectLink">
<binding name="listener" value="listener:login"/>
</component>
126 Chapter 4 Creating an e-Shop

</page-specification>
Define the login() listener:
public abstract class ProductDetails extends BasePage {
...
@InjectPage("Login")
public abstract Login getLoginPage();

public Login login() {


getLoginPage().setNextPage("ProductDetails");
return getLoginPage();
}
}
However, this won't work. After logging in, the Login page will try to activate the ProductDetails page. The problem is,
before activating it, one must call setProductId() on it to tell it which product to show. It means remembering the page
name as the next page is not enough. You must also remember the parameters it needs. To solve this problem,
Tapestry provides some classes for us:

It is an interface
ICallback representing the next
page

Implements Implements

PageCallback ExternalPageCallback
Page name: XXX Page name: XXX
Parameters: {a, b, c}

A next page that can be A next page that can be


represented by a page name represented by a page name
without any parameter and an array of parameter
To make use of them, modify the ProductDetails class:
This states that this page
is an "external page", i.e.,
it can accept parameters.
public abstract class ProductDetails extends BasePage implements IExternalPage {
@Persist("client")
public abstract String getProductId(); When someone activates the page,
public abstract void setProductId(String productId); this method will be called before the
... page starts to render itself.
public void activateExternalPage(Object[] parameters, IRequestCycle cycle) {
setProductId((String) parameters[0]);
} The parameters provided
@InjectPage("Login") by the caller are store
public abstract Login getLoginPage(); here
public Login login() {
getLoginPage().setNextPage(new ExternalCallback(this, new String[] { getProductId() }));
return getLoginPage();
}
} Create an external call back. It contains two When the user clicks the Login
Assume the first parameter is pieces of information: the page name (must link, the product id will be
the product id. So, store it into be an external page) and an array of restored automatically from the
the property for use during the parameters. HTTP request (as it is marked
rendering. as a client persistent property).
Now the "nextPage" property of the Login page should no longer be a page name (string). Instead, it should be an
ICallback object:
Creating an e-Shop 127

public abstract class Login extends BasePage {


private String email; The next page is no longer a simple
private String password; page name, but a call back object.

public abstract void setNextPage(String ICallback callback);


public abstract String ICallback getNextPage();
You can add the
public String void onLogin(IRequestCycle cycle) { request cycle as an
try { argument
User user = Users.getKnownUsers().getUser(email, password);
getUser().copyFrom(user);
return getNextPage() != null ? getNextPage() : "Home";
ICallback callback = getNextPage();
if (callback != null) {
callback.performCallback(cycle); Activate the callback (either a page
} else { without parameters or an external
cycle.activate("Home"); page with parameters)
}
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
return null;
}
} To display the Login page again, just
} don't activate any page.
Confirm.java needs to be modified accordingly:
public abstract class Confirm extends BasePage implements PageValidateListener {
public void pageValidate(PageEvent event) {
if (!getUserExists()) {
Login login = getLoginPage();
login.setNextPage("Confirm");
login.setNextPage(new PageCallback("Confirm"));
throw new PageRedirectException(login);
}
}
...
}
Now run the application. The callbacks should be working.

Passwords are exposed


However, there is a serious problem in the program: the passwords of the users are exposed! Consider Login.java:
public abstract class Login extends BasePage {
private String email;
private String password;
...
}
Whenever you have instance variables in a page object, you must be extremely careful, because the page objects are
pooled and reused by different users. These variables must not contain confidential information or per-user information.
In this case, one of them is the user's password! To fix it, just make them into properties:
public abstract class Login extends BasePage {
private String email;
private String password;
abstract public String getEmail();
abstract public String getPassword();
...
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
128 Chapter 4 Creating an e-Shop

public void onLogin(IRequestCycle cycle) {


try {
User user = Users.getKnownUsers().getUser(getEmail(), getPassword());
getUser().copyFrom(user);
if (getNextPage() != null) {
getNextPage().performCallback(cycle);
} else {
cycle.activate("Home");
}
} catch (AuthenticationException e) {
ValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("Login failed", null);
}
}
}

Implementing logout
Suppose that you'd like to allow the user to logout:

The minimum that you need to do is to remove the User object from the session. However, a better way is to delete the
session altogether. Tapestry provides a service called "Restart service" that does exactly this: Delete the session and
then display the Home page. To call such the Restart service, modify Home.html:
<html>
<head>
<title>Shop</title>
</head>
<body>
<h1>Product listing</h1>
<table border="1">
<tr jwcid="products">
<td><span jwcid="id">p01</span></td>
<td><a href="" jwcid="detailsLink"><span jwcid="name">Pencil</span></a></td>
<td><span jwcid="price">1.20</span></td>
</tr>
</table>
<p>
<a href="" jwcid="loginLink">Login</a>
<a href="" jwcid="logoutLink">Logout</a>
</body>
</html>
How to call a service using a link? To call a page, you can use a PageLink. To call a listener, you can use a DirectLink.
To call service, you can use a ServiceLink component in Home.page:
Creating an e-Shop 129

<page-specification>
...
<component id="logoutLink" type="ServiceLink">
<binding name="service" value="literal:restart"/>
</component>
</page-specification>
The name of the service to call. The default
prefix is ognl, so you need to tell it this is a
literal here. Or you could quote it with single
quotes.

<page-specification>
...
<component id="logoutLink" type="ServiceLink">
<binding name="service" value="'restart'"/>
</component>
</page-specification>

Summary
To create a link to show a page, use a PageLink component. To create a link to show a page that takes parameters,
either use a DirectLink to pass the parameters to the listener which will activate the page using the bucket brigade
pattern.
If a page takes parameters, it is dangerously easy to come to believe that the parameters are still set when you click a
link or submit the form on that page. This is NOT the case because a new page may be taken from the pool. To
maintain the parameters, either put them into a DirectLink or Hidden fields or store them as the client persistent
properties. If you use client persistent properties, when the page is rendered, the properties will be stored as query
parameters in links and hidden fields in forms. When the user clicks a link or submits a form, if during the handling of
the HTTP request that page is loaded, the properties will be retrieved from the HTTP query and store into the page
object.
You may also store a persistent property into the session. However, you must make sure this data is per-user instead
of per-page, otherwise the "Back" button may cause surprises.
A session is a Map of key-value pairs stored in the memory of the server for each user. The server puts the session id
into a cookie in the browser so that it can lookup the session on the subsequent requests. Or it could can use URL
rewriting to pass the session id along.
Every Tapestry application has a list of application state objects. Each row in the list specifies the name of the object,
its Java class and its scope. The scope can be "session" or "application" and it will determine this object will be put into
the session or into a global area. To add a row to that list, edit the Hivemind module descriptor (at the resource path /
META-INF/hivemodule.xml) to add a contribution to the configuration named tapestry.state.ApplicationObjects. To
access the object, use @InjectState or <inject type="state">. The object will be created automatically if it doesn't exist
yet. Whenever you modify the hivemodule.xml file, don't forget to reload the application because it is only read at start
up. If you're putting an object into the session, your Java class should implement Serializable because the server may
save the data in the session to disk. To check if an application state object exists, use @InjectStateFlag or <inject
type="state-flag">.
If your Form contains multiple Submit components, you need to distinguish which one was clicked. You can have a
different listener for each of them. The listener of the Submit component that is clicked will be called just before the
Form calls its own listener (if any). Another solution is to let the Submit component set its "selected" parameter to its
"tag" value and then in the Form's listener check which one was clicked.
To let a page protect itself, let it implement PageValidateListener and implement the pageValidate() method. It will be
called when the page is activated. If something is wrong, it can redirect to another page by throwing a
PageRedirectException. This is most useful when the page requires that the user has logged in.
Commonly a login page needs to remember the next page to show after the login. The next page can be represented
using an ICallback object. A PageCallback stores just the page name. An ExternalPageCallback stores the page name
and an array of parameters. That page must implement IExternalPage for this to work. The added benefit is that this
page can now be invoked with parameters using a URL.
To implement logout, use the Restart service provided by Tapestry so that the session is deleted. To call it, use a
ServiceLink.
If you need to use a component at two difference locations in the same page with the same parameter bindings, you
should use the copy-of feature.
130 Chapter 4 Creating an e-Shop

Instead of setting the Java class in each .page file, you can list some Java packages in the application specification and
Tapestry will look for a class with the name of the page in those packages.

You might also like