Sie sind auf Seite 1von 497

Enjoying Web Development with Tapestry

Copyright 2005-2006 Ka Iok 'Kent' Tong

Publisher: Book website: Notice:

TipTec Development http://www.agileskills2.org 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. Third edition 2006

Author's email: freemant2000@yahoo.com

Edition:

Enjoying Web Development with Tapestry

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.

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

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

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

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

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

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 is telling Tomcat that users should access this application using 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 descriptor to determine the path:

Tell Tomcat that the application's files can be found in c:\workspace\HelloWorld\context.

HelloWorld.xml Foo.xml Bar.xml

/HelloWorld /Foo /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 3: Look, we have a component here

Tapestry
7: Regular HTML code again

2: Output it 8: Output it

<html> Hello John ! </html>

<html> Hello <span jwcid="subject">world</span> ! </html>

4: Create the component. It is just a Java object.

5: Generate HTML for yourself

Component "subject"

6: Output HTML code for 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 component named "subject" here

It is an Insert component. It will output some plain text as the HTML.

<page-specification> <component id="subject" type="Insert"> <binding name="value" value="ognl:greetingSubject"/> </component> </page-specification> Each Insert component has a few "parameters". In particular, it will evaluate its parameter named "value" and output the result as the plain text to output. This is the "prefix" of the expression. It is saying that what is following is an "OGNL" expression.

The whole thing will be evaluated as the value of the "value" parameter and output

This is an OGNL expression. But what does it mean?

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 object)


1: Generate HTML for yourself

3: Result is "John"

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 <form action="..."> stockQuoteForm <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 4: Render yourself <html> <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>

Tapestry Page object


void onOk() { ... }

6: Look, what expression to evaluate? Oh, it is a listener expression.

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 page object. 2: Handle the form submission 3: What is the value of the HTML text input?

HTTP request

stockId

4: It's "SUN"

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 page object

Result.java 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 http://localhost:8080/StockQuote/app?service=page&page=Result. You should see: browser to display

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 cycle". It represents the request from 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() { Tapestry is very smart. It will check if the return "MSFT"; method has a request cycle formal } argument, if yes, it will provide one to it. public void setStockId(String stockId) { 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 void actionTriggered() { page.onOk(cycle); }

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 void onOk(IRequestCycle cycle) { //don't call activate() at all } } public abstract class Home extends BasePage { ... public void onOk(IRequestCycle cycle) { 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
4: Create a subclass of Home to provide a getter for the property

3: It is requested that a property named "resultPage" be added to the class.

2: Basically just create an instance of this Home class, but...

Extends

Home.page <page-specification class="com.ttdev.stockquote.Home"> <inject property="resultPage" type="page" object="Result"/> <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>

HomeEnhanced
Result getResultPage() { return cycle.getPage("Result"); }

Home.java public abstract class Home extends BasePage { private String stockId; abstract public Result getResultPage(); public String getStockId() { return "MSFT"; } public void setStockId(String stockId) { this.stockId = stockId; } public IPage onOk(IRequestCycle cycle) { int stockValue = stockId.hashCode() % 100; Result resultPage = getResultPage(); resultPage.setStockValue(stockValue); return resultPage; } }

As the type of the property is "page", the getter will just load the page from the request cycle. The name of the page is given as the "object". If the type were not "page", then how to interpret the "object" would be different.

Tapestry will make sure the return type of the getter will match of the getter is declared as abstract in the parent.

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 http://localhost:8080/StockQuote/app?service=page&page=Result. You'll see:

another

user.

Enter

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
1: Render yourself 7: Render yourself 2: Done 3: Moved into the pool 5: Give me a Result page object

Page pool Result page object

Result page 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, 4: Create a subclass of but... Result to provide a getter, Result.page setter and an initialize() <page-specification class="com.ttdev.stockquote.Result"> method for the property <property name="stockValue"/> <component id="stockValue" type="Insert"> <binding name="value" value="stockValue"/> </component> </page-specification> Extends

ResultEnhanced
int stockValue; protected void initialize() { stockValue = 0; } public int getStockValue() { return stockValue; } public void setStockValue(int v) { this.stockValue = v; }

How does Tapestry know that it is an int? It knows that from this method signature. If there weren't such a method, it would just declare it as an Object. Result.java public abstract class Result extends BasePage { 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"> <binding name="value" value="stockId"/> </component> </page-specification> InjectPage is a Java interface is defined in the org.apache.tapestry.annoations package. So you must import the package.

The <inject> element there has exactly the same effect as the @InjectPage annotation here.

Home.java import org.apache.tapestry.annotations.*; 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; } }

Simply delete the <property> element. When Tapestry sees an unimplemented getter (either the method is declared abstract or the class is implementing an interface but doesn't provide an implementation for a method) like getStockId() here, it will create a property for 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 { @InjectPage("Result") abstract public Result getResultPage(); @InitialValue("literal:MSFT") abstract public String getStockId();

After creating a new page object, Tapestry will evaluate this expression to get the value and store it into the property. In this case you're using the literal prefix. If required, you can use the ognl prefix and specify an OGNL 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 component is Form Write the binding here directly

<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> ... The path to the dojo.js file. This file is </html> 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" ...> ... </form> </html> You must specify the title Bring in the dojo library

<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(); For the patron whose code is patronCodeToDiscount.put("p1", new Integer(90)); "p1", the discount is 90% (i.e., patronCodeToDiscount.put("p2", new Integer(95)); 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"

Component "weight"

3: Convert the object Integer(5) to a string 4: The string is "5"

translator

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 submission

2: What is the value of the HTML text input?

3: It's a string "5"

Component "weight"
To implement this idea, modify Home.page:

4: Convert string "5" to an object 5: The object is Integer(5)

translator

<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; Field name Old value Error msg Create a new one for each @InjectPage("Result") weight "-20" "Weight must be >= 0" request (form public abstract IPage getResult(); submission) public abstract double getWeight(); public abstract String getPatronCode(); public Home() { Record the patronCodeToDiscount = new HashMap(); (invalid) patronCodeToDiscount.put("p1", new Integer(90)); patronCodeToDiscount.put("p2", new Integer(95)); value input Set the "weight" } by the user Record the TextField as the public IPage onSubmit() { error current component delegate = new ValidationDelegate(); message (input field) 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); Add another error } Integer discount = (Integer) patronCodeToDiscount.get(getPatronCode()); to the validation delegate Patron code if (discount == null && getPatronCode() !=null) { delegate.setFormComponent((IFormComponent) getComponent("patronCode")); not found but delegate.recordFieldInputValue(getPatronCode()); was indeed delegate.record("Patron not found", null); input by the } user The type of the error. There are if (delegate.getHasErrors()) { Returning null means other types such as return null; It contains at using this page (Home) REQUIRED (a required input is } least one as the response page not provided), TOO_LARGE int postagePerKg = 10; error? (value is too large) and etc. int postage = (int) (weight * postagePerKg); This is not used at all and you if (discount != null) { may just pass a null to postage = postage * discount.intValue() / 100; represent an unknown type of } error. IPage resultPage = getResult(); 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 weight ... ... Old value "-20" ... Error renderer 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> Lookup this bean by name. If <component id="patronCode" type="TextField"> it's not there yet, it will be <binding name="value" value="patronCode"/> created automatically. </component> <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 submission

2: What is the value of the HTML text input? 3: It's a string "5"

validator

validator

...

6: Check if Integer(5) 7: Check if Integer(5) is valid. Assume it's is valid. Assume it's considered valid. considered invalid.

Component "weight"

4: Convert string "5" to an object 5: The object is Integer(5)

translator

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> What if the object is found <component id="patronCode" type="TextField"> to be < 0? It will record an <binding name="value" value="patronCode"/> error into the validation </component> delegate. But how can it <component id="errors" type="Delegator"> <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

Ye s

1: Render yourself

m :A

Ii

er

ro

r?

s It' 5:

" bc "a

3:

8: Output two red stars 7: Output any error indication

Component "weight"

t' s ha W ? 4: alue v

d ol

6: Output an input field with "abc"

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
5: It's int 5 1: Render yourself
r? ro er 4: Call n Ii getWeight() m A 2:
3: N

Field name Old value Error msg

Component "weight"

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

translator

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> Add a new validator here. <component id="patronCode" type="TextField"> It will check to ensure the <binding name="value" value="patronCode"/> object is not null. </component> <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> This is the error <component id="patronCode" type="TextField"> message <binding name="value" value="patronCode"/> 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> When it renders itself, it will check with <component id="weightLabel" type="FieldLabel"> the validation delegation to see if the <binding name="field" value="component:weight"/> "weight" component is in error. If so, it </component> will render itself in red. </page-specification> 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() { The most important code here. Check if the patron return false; code is unknown. If so, throw a ValidatorException. } The TextField will catch it and record it as an error in public void renderContribution( IMarkupWriter writer, IRequestCycle cycle, the validation delegate. FormComponentContributorContext context, IFormComponent field) { } If the validator could use Javascript to perform public boolean isRequired() { validation in the browser, it should output the return false; 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"/> The bean prefix says what is </component> following is the name of a bean. <component id="patronCode" type="TextField"> The value of the whole expression <binding name="value" value="patronCode"/> <binding name="validators" value="bean:patronCodeValidator"/> is that bean object. If you'd like, you could write </component> "ognl:beans.patronCodeValidator" <component id="errors" type="Delegator"> <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

The "source" is a list, an array or any collection. The For component will loop for each element.

A For component will loop and render its body multiple times. How many times? This depends on its "source" 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 from the field tracking and render it For each element, the For component will set its "value" 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 weight "5" Old value null Error renderer

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


<html> <ul> <span jwcid="errors"> <span jwcid="isInError"> <li><span jwcid="error"/></li> </span> </span> </ul> ... </html>

After
<html> <ul> <span jwcid="errors"> <li jwcid="isInError"> <span jwcid="error"/> </li> </span> </ul> ... </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:
public abstract class Home extends BasePage { ... 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; } } This combination is invalid. Must check if the patron code is not null. It will be null if it is not a known patron because it will not be set and remain at its initial value.

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:
Use a date translator. Set the "pattern" property of the date translator to yyyy/MM/dd, so that the shipping date value is displayed in this format and it will also expect the input to be in this format. Internally the date translator simply uses the Java SimpleDateFormat class to do the work. So you can lookup its API doc to find other possible patterns. A minDate validator which will make sure the date is >= December 1, 2006. How does Tapestry figure out which number in 12/1/2006 is the year, the month and the day? If a number is large (e.g., 2006), it must be the year. Then for the rest of the numbers, the first number will be taken as the month and the other as the day. So, you 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 { public abstract Date getShippingDate(); public abstract String getDesc(); public IPage onSubmit() { System.out.println(getShippingDate()); System.out.println(getDesc()); ... } } Define the two properties to store the input Just print them out

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 min max email minLength maxLength Purpose Checks if a number is >= the given number. Checks if a number is <= the given number. Checks if a string is an email. Checks if the length of a string is >= the given length. Checks if the length of a string is <= the given length.

82

Chapter 3 Validating Input

Validator pattern

Purpose 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 maxDate Checks if a Date is >= the given Date. 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> It will be a For component. It will generate a <tr> <body> element for each product being visited. So, each <h1>Product listing</h1> product will appear as a row in the HTML table. <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> </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; private String name; Need to provide the private String desc; getters 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 itself as an HTML link 2: Render its body public void onClickDetailsLink() { ... ... }

<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"> 1: Make a copy <component id="name" type="Insert"> <binding name="value" value="name"/> </component> Component "name2" is a copy <component id="name2" copy-of="name"/> of "name1". <component id="desc" type="Insert"> <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 } product object using the product id. Of public String getDesc() { course, you need to define this return lookup().getDesc(); 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(); } @InjectPage("ProductDetails") public abstract ProductDetails getDetailsPage(); Let Tapestry get a page object for the page named "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 setting for Tapestry

If an XXX.page file doesn't specify a Java class, then look for an XXX class 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"> No longer need <binding name="selected" value="buttonClicked"/> these <binding name="tag" 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

2: Render itself to generate HTML page

THEY ARE NOT NECESSARILY THE SAME PAGE OBJECT!!!

<html> ... ... ... </html>

3: The user submits the form

Tapestry (Direct service)


5: Let the Form or Submit button call their listeners

4: Give me a ProductDetails page

Page pool

ProductDetails page
productId: ???

ProductDetails page

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
productId: p01

3: call getProductId() and get "p01" as return <form ...> <input type="hidden" name="productId" value="p01"/> ...

1: Render yourself 4: Output an HTML hidden form 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
<html> <form ...> <input type="hidden" name="productId" value="p01"/> ... </form> </html> productId: p01

ProductDetails page
productId: ???

4: What is my value? It's "p01" 6: call setProductId("p01")

3: Handle the form submission

7: Handle the form submission

Hidden
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 Session for user 1 Key (string) Some key Some key Some key Session for user 2 Some key Some key Some object Some object Some object Some object Some object Value (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 java.util.ArrayList com.ttdev.Foo ... Scope session application ...

2: Give me an object named "cart"

cart 3: Look it up in the foo table ...

Your application
5: Store the ArrayList into the session for the current user under the named "cart" 1: You add an entry to this table to let Tapestry know how to create your cart object.

4: Create a java.util.ArrayList

Name cart foo ...

Object

ArrayList
...

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 cart foo ... Java class java.util.ArrayList com.ttdev.Foo ... Scope session 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: workspace Shop context WEB-INF classes META-INF hivemodule.xml org com ttdev shop Home ... apache tapestry html BasePage ... Resource path c: tomcat share lib tapestry.jar META-INF hivemodule.xml

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") public abstract List getCart(); Retrieve the shopping cart for the current user from the session

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; } public void onCheckout() { //checkout. Leave it unimplemented for the moment } public String continueShopping() { return "Home"; } }

As the shopping cart contains a list of product id's only, you need to lookup each to find the Product object and build up a list of Product objects.

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 XXX

Key (string) Some key Some key Some key Some object Some object Some object Some object Some object

Value (object)

YYY

Some key Some key

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

ProductDetails page
Persistent properties productId: p01

<html> ... <form> <input type="hidden" 2: Output HTML page name="ProductDetails-productId" value="p01"/> ... </form> ... <a href="/foo/bar?ProductDetailsproductId=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
ProductDetailsproductId: p01 5: Tell me the product id for page ProductDetails

<html> ... <form> <input type="hidden" name="ProductDetails-productId" value="p01"/> ... </form> ... <a href="/foo/bar?ProductDetailsproductId=p01">...</a> </html> 4: Look, it has a persistent property.

6: Set the product id to p01

Tapestry

3: Get a ProductDetails from the pool

Page pool ProductDetails page


Persistent properties productId: ???

ProductDetails page
productId: ???

To implement this idea, modify ProductDetails.java:


public abstract class ProductDetails extends BasePage { private String productId; Don't need this anymore. Let Tapestry public void setProductId(String id) { manage the property for you. this.productId = id; } 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();

public String addToCart() { getCart().add(getProductId()); 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()); } }

The setter is not required in general, but the Home page needs to call it to set the product id.

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> It will render itself as an HTML <component id="productId" type="Hidden"> link that will activate a page. The <binding name="value" value="productId"/> name of the page is specified </component> using its "page" parameter. <component id="refresh" type="PageLink"> <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 u001 u002 Email paul@yahoo.com john@hotmail.com aaa bbb Password Credit card # 1111 2222 3333 4444 2222 3333 4444 5555 3333 4444 5555 6666

u003 mary@gmail.com ccc 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"/> Handle the submission in the </component> listener of the Form. So, no <component id="email" type="TextField"> need to make the submit <binding name="value" value="email"/> button a component at all. </component> <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 private String private String private String { id; email; password; 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 cart user ... Java class java.util.ArrayList com.ttdev.shop.User ... Scope session 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 private static private String private String private String private String implements Serializable { final long serialVersionUID = 1858318434177120215L; id; email; password; 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 private static private String private String private String private String implements Serializable { final long serialVersionUID = 1858318434177120215L; id; email; password; 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 private static private String private String private String private String implements Serializable { final long serialVersionUID = 1858318434177120215L; id; email; password; 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

Checkout

Cart

Already logged in

Confirm

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

Checkout

Cart

Already logged in

Confirm

Malicious user

Activate

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

Checkout

Cart

Activate

Confirm

Activate

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

ICallback
Implements

It is an interface representing the next page Implements

PageCallback
Page name: XXX

ExternalPageCallback
Page name: XXX Parameters: {a, b, c} A next page that can be represented by a page name and an array of parameter

A next page that can be represented by a page name without any 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") When someone activates the page, public abstract String getProductId(); this method will be called before the public abstract void setProductId(String productId); 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 restored automatically from the be an external page) and an array of the property for use during the HTTP request (as it is marked parameters. 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 { The next page is no longer a simple private String email; page name, but a call back object. private String password; public abstract void setNextPage(String ICallback callback); public abstract String ICallback getNextPage(); You can add the request cycle as an public String void onLogin(IRequestCycle cycle) { argument try { 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.

131

Chapter 5
Chapter 5

Creating Custom Components

132

Chapter 5 Creating Custom Components

What's in this chapter


In this chapter you'll learn how to create your own components that can be reused just like the Tapestry components.

Displaying a copyright notice on all pages


Suppose that you'd like to an application that consists of quite some pages. What the application does is unimportant. What is important is that all the pages must have a copyright notice at their bottom such as:

This is easy. For example, you could create Home.html like:


<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <hr> Copyright 2005. Foo Inc. All rights reserved. </body> </html>

However, the HTML code for the copyright notice will have to be duplicated in all the pages in the application. This is no good. If you had a Copyright component that can generate the copyright notice, then you could solve the problem like this:
<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="copyright">Copyright notice</span> </body> </html>

Even though there is no such a Copyright component, you can create it yourself. To do that, create a new Tapestry project named "Components". Setup the class path, output folder and web.xml and create a context descriptor as usual. Then, modify Home.html:
<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="copyright">Copyright notice</span> </body> </html>

Home.page is like:
<page-specification> <component id="copyright" type="Copyright"/> </page-specification>

Of course, this will not work yet, because unlike Insert, Form, For and etc., Tapestry doesn't know of any component type named "Copyright". This is fine. You can define it yourself. To do that, create a file Copyright.jwc in the context/WEB-INF folder. It should be like: How does a BaseComponent render itself? By default, it will find out the filename of its .jwc file (Copyright.jwc) and then look for a corresponding HTML file (Copyright.html) in the same folder and output it. So you need to create such a

Creating Custom Components

133

The Java class is org.apache.tapestry.BaseComponent This file defines a component type The root element of this coming with Tapestry. In fact, this is the default so even if Copyright. A component is just a xml file is <componentyou didn't specify the class attribute, the effect would still Java object. But what is the Java specification> be the same. class? Copyright.jwc <?xml version="1.0"?> <!DOCTYPE component-specification PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN" "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd"> <component-specification class="org.apache.tapestry.BaseComponent"> </component-specification>

<?xml version="1.0"?> <!DOCTYPE component-specification PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN" "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd"> <component-specification class="org.apache.tapestry.BaseComponent"> </component-specification>

Copyright.html. It should be like:


<hr> Copyright 2005. Foo Inc. All rights reserved.

Now, run the application and you should see that it's working:

How does it work? When the Home page is rendering itself (see the diagram below), it finds a component named "copyright". In order to create this component, it checks its page specification (Home.page) and finds that its type is Copyright. In order to create such a Copyright component, it tries to read the file Copyright.jwc in WEB-INF. The Java class of the component is specified there. In this case it is BaseComponent provided by Tapestry. So, the Home page object creates such a BaseComponent and asks it to render itself. This component finds that its corresponding .jwc file is Copyright.jwc, so it will render Copyright.html:

134

Chapter 5 Creating Custom Components

<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="copyright">Copyright notice</span> </body> </html> 2: Oh, it's type is Copyright. How to create such a Copyright component? Let's lookup Copyright.jwc in WEB-INF. Home.page <page-specification> <component id="copyright" type="Copyright"/> </page-specification>

1: Look, I have a component named "copyright" here. I need to create it and ask it to render itself. But what kind of component is it? Let's look up my . page file.

Home page
5: Render yourself

4: Create

BaseComponent "copyright"

...

Copyright.jwc <component-specification class="org.apache.tapestry.BaseComponent"> </component-specification>

3: Its Java class is BaseComponent. Now, I can create it.

6: As my .jwc file is Copyright.jwc, render everything in Copyright.html

Copyright.html <hr> Copyright 2005. Foo Inc. All rights reserved.

The Copyright.jwc file is called the "component specification" for the Copyright component (just like a page specification). The Copyright.html file is called the "template" for it. Copyright.html is not limited to plain HTML. It can contain other components too. For example, instead of always displaying "2005" as the year, you can display the current year:

"copyright"
Copyright.html <hr> Copyright <span jwcid="year">2005</span>. Foo Inc. All rights reserved.

2: call getCurrentYear() on it

1: Create and tell it to render itself

Copyright.jwc <component-specification class="com.ttdev.components.Copyright"> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> </component-specification> This will call getCurrentYear() on the Copyright component Just like a page class, always mark it as abstract. In order to provide the getCurrentYear() method, you must create a subclass of BaseComponent. Copyright.java package com.ttdev.components; public abstract class Copyright extends BaseComponent { public int getCurrentYear() { return new GregorianCalendar().get(GregorianCalendar.YEAR); } }

"year"

...

As shown in the diagram above, when the Copyright component renders Copyright.html, it will in turn create and render the Insert component named "year". This component will call getCurrentYear() on the Copyright component object to find the value to insert. Of course, the BaseComponent class coming with Tapestry doesn't have such a getCurrentYear() method, so you need to define your own component class by subclassing BaseComponent. Now run the application again and it should continue to work.

Creating Custom Components

135

Should Copyright.html be a complete page?


At the moment, Copyright.html is:
<hr> Copyright <span jwcid="year">2005</span>. Foo Inc. All rights reserved.

If you use a web page editor, when you save the file, it may find that the file is not yet a complete HTML page and so it may try to complete it:
<html> <body> <hr> Copyright <span jwcid="year">2005</span>. Foo Inc. All rights reserved. </body> </html>

Then, when the Home page is rendered, the result may be like:
<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <html> <body> <hr> Copyright 2005. Foo Inc. All rights reserved. </body> </html> </body> </html>

The browser may still render this HTML code fine, but the code is really invalid. A solution is to change Copyright.html to:
Only this part is kept, everything <html> outside is removed. <body jwcid="$content$"> <hr> Copyright <span jwcid="year">2005</span>. Foo Inc. All rights reserved. </body> </html>

This way Tapestry loads the Copyright.html file, it notes that the <body> element has a "jwcid" attribute so it should be a Tapestry component. But it further finds that its id is "$content$". This id has a special meaning to Tapestry. It is not really an id for a component. Instead, it is telling Tapestry to discard what it has seen so far and use the body of this element (starting from the <hr> tag) as the real template content.

Stating that the body will be discarded


At the moment, someone could use the Copyright component like:
<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="copyright"><span jwcid="@Insert" value="abc">Copyright notice</span></span> </body> </html>

It will run but the Insert component won't be used at all because your Copyright component will simply output its template and won't use its body for rendering at all. So, putting components in its body just doesn't make sense. Therefore, you should state that the Copyright component doesn't allow components in its body:

136

Chapter 5 Creating Custom Components

Doesn't allow components in its body. Note that it can still have static HTML code in its body. Just no components. <component-specification class="com.ttdev.components.Copyright" allow-body="no"> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> </component-specification> Not allowed! OK!

<span jwcid="copyright"> <span jwcid="@Insert" value="abc">Copyright notice</span> </span>

<span jwcid="copyright"> <b>Hello</b> </span>

If you run the application while having components in its body, Tapestry will throw an exception when reading Home.html:

It is saying that you can't have dynamic components inside an ignored block. This is a good thing because it is telling you that you're trying to do something unreasonable (embedding components inside a component that will ignore its body).

Creating a Box component


To see a component that allows a body, let's create a Box component that will render its body inside a table. For example, you could embed a Copyright component inside a Box component:

Creating Custom Components

137

To do that, modify Home.html to use such a Box component:


<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="box"> <span jwcid="copyright">Copyright notice</span> </span> </body> </html>

Define the component in Home.page:


<page-specification> <component id="copyright" type="Copyright"/> <component id="box" type="Box"/> </page-specification>

Of course, you need to create a new component type named Box. To do that, create Box.jwc:
Allow components in its body. You could set it to "yes", "true" or even "1". <component-specification class="com.ttdev.components.Box" allow-body="yes"> </component-specification>

You don't need to create Box.html because the Box component will output a table and then its body: It doesn't use a template at all. Create Box.java:
public abstract class Box extends AbstractComponent { protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) { ... } }

Note that it is extending AbstractComponent instead of BaseComponent. What is the difference between a BaseComponent and an AbstractComponent? An AbstractComponent has an abstract renderComponent() method to render itself. A subclass needs to implement this method to do the rendering. You can do the rendering any way you like. It has no concept of a component template. BaseComponent is a subclass of AbstractComponent. It has a concept of a component template. Its renderComponent() will render its template:

138

Chapter 5 Creating Custom Components

AbstractComponent
abstract void renderComponent();

It has no concept of component template. Its rendering can be as flexible as you like.

Extends

BaseComponent
void renderComponent() { //render its template }

It has a concept of component template. To render itself, it just renders its template.

As your Box component will not use a template, it should extend the AbstractComponent instead of the BaseComponent. In contrast, as the Copyright component uses a template, it should extend the BaseComponent:

AbstractComponent

Extends

Extends

BaseComponent

Box

Extends

Copyright
Now, implement the renderComponent() method in Box.java:
public class Box extends AbstractComponent { protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) { writer.begin("table"); writer.attribute("border", 1); writer.begin("tr"); writer.begin("td"); renderBody(writer, cycle); writer.end(); writer.end(); writer.end(); } } <table border="1"><tr><td> <hr> Copyright 2005. Foo Inc. All rights reserved. Render its body </td></tr></table>

<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="box"> <span jwcid="copyright">Copyright notice</span> </span> </body> </html>

The render process is simple. You output a <table>, a <tr> and a <td> and then render the body of the component, which is a Copyright component. Finally you output the end tags for <td>, <tr> and <table> respectively.

Creating Custom Components

139

Now run the application and it should be working.

Customizing the Box component using informal parameters


What if you'd like to customize the table like setting its width? You would like to use informal parameters like:
<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <span jwcid="box" width="50%"> <span jwcid="copyright">Copyright notice</span> </span> </body> </html>

For it to work, the Box component must declare that it supports informal parameters:
<component-specification class="com.ttdev.components.Box" allow-body="yes" allow-informal-parameters="yes"> </component-specification>

Then, in its renderComponent() method you need to output the informal parameters:
public abstract class Box extends AbstractComponent { protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) { writer.begin("table"); writer.attribute("border", 1); renderInformalParameters(writer, cycle); writer.begin("tr"); writer.begin("td"); renderBody(writer, cycle); writer.end(); writer.end(); writer.end(); } }

Now, run the application again and the table will be 50% of the width of the browser window:

The HTML code will be:


<html> <head></head> <body> <h1>Page 1</h1> This is page 1. <table border="1" width="50%"><tr><td> <hr> Copyright 2005. Foo Inc. All rights reserved. </td></tr></table> </body> </html>

140

Chapter 5 Creating Custom Components

Customizing the Copyright component using formal parameters


Suppose that in your application on some pages you'd like to display "Foo Inc." as the copyright holder, but for some other pages you'd like to display "Bar Inc.". How to do that? You can let your Copyright component accept a "holder" parameter. First, modify Copyright.jwc:
<component-specification class="com.ttdev.components.Copyright" allow-body="no"> <parameter name="holder" required="yes"/> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> </component-specification>

The parameter is named "holder"

It must be bound when you use a Copyright component

Then, if you'd like to show "Bar Inc." on the Home page, modify Home.page to provide a binding for this parameter:
<page-specification> <component id="copyright" type="Copyright"> <binding name="holder" value="literal:Bar Inc."/> </component> <component id="box" type="Box"/> </page-specification>

Now, you can access the value of the "holder" parameter in Copyright.java just as a property:
public abstract class Copyright extends BaseComponent { public abstract String getHolder(); public int getCurrentYear() { return new GregorianCalendar().get(GregorianCalendar.YEAR); } }

Again, this property is provided by the subclass created by Tapestry. As usual, if you don't need to read the holder in the Java class, you don't need to declare the getter. In order to output the value of the holder, you need an Insert component. Modify Copyright.html:
<html> <body jwcid="$content$"> <hr> Copyright <span jwcid="year">2005</span>. <span jwcid="holder">Foo Inc.</span> All rights reserved. </body> </html>

Define this component in Copyright.jwc:


<component-specification class="com.ttdev.components.Copyright" allow-body="no"> <parameter name="holder" required="yes"/> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> <component id="holder" type="Insert"> <binding name="value" value="holder"/> </component> </component-specification>

Now run the application and it should display "Bar Inc.":

Creating Custom Components

141

Making a parameter optional


At the moment the "holder" parameter is required. That is, whoever using the Copyright component must bind the "holder" parameter to some value. What if you'd like to make it optional and treat it as "Foo Inc." if it is not bound? To do that, modify Copyright.jwc:
It is no longer required The default value. By default it is an OGNL expression. But as here you'd like to use a literal, so you need to use the literal prefix here.

<component-specification class="com.ttdev.components.Copyright" allow-body="no"> <parameter name="holder" required="no" default-value="literal:Foo Inc."/> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> <component id="holder" type="Insert"> <binding name="value" value="holder"/> </component> </component-specification>

Next, if you'd like to display "Foo Inc." on the Home page, you can omit the parameter in Home.page:
<page-specification> <component id="copyright" type="Copyright"> <binding name="holder" value="Bar Inc."/> </component> <component id="box" type="Box"/> </page-specification>

Using annotation to declare a parameter


If you'd like, you can use annotation to declare a parameter. For example, for the Copyright component, you can do it this way:
The name of the parameter. Actually you don't need to specify it here as Tapestry can figure it out from the getter. The parameter is not required (optional). Actually you don't need to specify it here because by default a parameter is not required. Specify the default value. If you don't specify it, the default value for the Java type is used.

public abstract class Copyright extends BaseComponent { @Parameter(name="holder", required=false, defaultValue="literal:Foo Inc.") public abstract String getHolder(); public int getCurrentYear() { return new GregorianCalendar().get(GregorianCalendar.YEAR); } }

Then you don't need to specify the parameter in Copyright.jwc:


<component-specification class="com.ttdev.components.Copyright" allow-body="no"> <parameter name="holder" required="no" default-value="literal:Foo Inc."/> <component id="year" type="Insert">

142

Chapter 5 Creating Custom Components

<binding name="value" value="currentYear"/> </component> <component id="holder" type="Insert"> <binding name="value" value="holder"/> </component> </component-specification>

Run the application and it should continue to work.

Looking for the component class in the specified packages


You don't need to specify the Java class in each .jwc file. You can list some Java packages and let Tapestry to look for the component class in those packages. For example, in this case, do it in the application specification (Components.application):
<?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 name="Components"> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> </application>

If the class is not found, then it will use BaseComponent. Now, Copyright.jwc can be simplified as:
<component-specification class="com.ttdev.components.Copyright" allow-body="no"> <component id="year" type="Insert"> <binding name="value" value="currentYear"/> </component> <component id="holder" type="Insert"> <binding name="value" value="holder"/> </component> </component-specification>

Box.jwc can be simplified as:


<component-specification class="com.ttdev.components.Box" allow-body="yes" allow-informal-parameters="yes"> </component-specification>

Restart the application so that the application specification is read again. Observe that it still works.

Creating a component that takes input


Suppose that you'd like to create a component that renders a color as three integers (red, green and blue):

For simplicity, let's ignore the color sample first and focus on the three text fields. First, create a RGB.jwc file and a RGB.html file. RGB.html should be:
<html> <body jwcid="$content$"> R: <input type="text" size="3" maxlength="3" jwcid="red"/> G: <input type="text" size="3" maxlength="3" jwcid="green"/> B: <input type="text" size="3" maxlength="3" jwcid="blue"/>

Creating Custom Components

143

</body> </html>

RGB.jwc is like:
You will render it yourself using the template (RGB.html) only and will not use the body of the component at all, so it shouldn't contain components in its body. It will render itself as several text fields and a span (as the color sample), you don't see any need for informal parameters.

<component-specification allow-body="no" allow-informal-parameters="no"> <component id="red" type="TextField"> <binding name="value" value="red"/> </component> <component id="green" type="TextField"> <binding name="value" value="green"/> </component> <component id="blue" type="TextField"> <binding name="value" value="blue"/> </component> </component-specification>

Create RGB.java:

144

Chapter 5 Creating Custom Components

It will render its template, so it should extend BaseComponent. public abstract class RGB extends BaseComponent { 5: These properties will be read by those three public abstract int getRed(); TextField components on render. On form submission public abstract void setRed(int red); (called "rewind"), those TextField components will store public abstract int getGreen(); the input values into these properties. public abstract void setGreen(int green); public abstract int getBlue(); public abstract void setBlue(int blue); The color to show to the user for editing. The caller must provide a color so it is @Parameter(required=true) marked as required. When the form is public abstract int getColor(); submitted, store the new value into the public abstract void setColor(int color); parameter. protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) { if (!cycle.isRewinding()) { 2: Check if it is not rewinding (i.e., setRed(getColor() >> 16); rendering). The request cycle has a flag setGreen((getColor() >> 8) & 0xff); to tell this. setBlue(getColor() & 0xff); } super.renderComponent(writer, cycle); if (cycle.isRewinding()) { setColor((getRed() << 16) | (getGreen() << 8) | getBlue()); } } } 1: This method is called for both render and form submission (called "rewind"). If it is rendering, it should render itself. If it is rewinding, it should store the user input back to the "color" parameter. 3: Just rendering. So, get the "color" parameter and set the red, green, blue properties. 4: Call BaseComponent's renderComponent(). If it is a render, this method will render the body (including the three TextField components). In the process the red, green and blue properties will be read. If it is a rewind, it will ask the components in its body to rewind. In the process the red, green and blue properties will be set. <component-specification allow-body="no" allow-informal-parameters="no"> <component id="red" type="TextField"> <binding name="value" value="red"/> </component> <component id="green" type="TextField"> <binding name="value" value="green"/> </component> <component id="blue" type="TextField"> <binding name="value" value="blue"/> </component> </component-specification>

6: If rewinding, the red, green and blue properties have been set. So, set the "color" parameter.

As shown above, renderComponent() will be called for both render and rewind. If it is rendering, your renderComponent () will check the rewind flag in the request cycle and find that it is a render, so it will retrieve the "color" parameter and use it to set the red, green and blue properties. Then it will call the BaseComponent's renderComponent(), which will render the body. As there are three TextField components in the body, they will be asked to render themselves. In the process they will read the three properties and render the three text input fields. Then, returning to the renderComponent() of RGB, from the request cycle it notes that it is not rewinding, so it does nothing for the moment. When the form is submitted (a rewind), Tapestry will set a rewind flag in the request cycle telling that it is in the rewind phase. Then it will call the renderComponent() of the Form component. The Form component will in turn call the renderComponent() on the components in its body. It means the renderComponent() of your RGB component will be called. First, it finds that it is not rewinding, so it doesn't need to set the three properties again. Then it calls BaseComponent's renderComponent(), which will rewind the components in the body. As there are three TextField components in the body, they will be asked to rewind themselves. In the process they will store the user input in the text input fields into the three properties. Then, returning to the renderComponent() of RGB, from the request cycle it notes that it is rewinding, so it uses the three properties to set the "color" parameter. Let's modify Home.html to use such a RGB component see if it is working:
<html> <head></head> <body>

Creating Custom Components

145

<h1>Page 1</h1> This is page 1. <form jwcid="form"> <span jwcid="rgb">edit color here</span> <input type="submit" value="OK"/> </form> <span jwcid="box" width="50%"> <span jwcid="copyright">Copyright notice</span> </span> </body> </html>

Modify Home.page:
<page-specification class="com.ttdev.components.Home"> <component id="copyright" type="Copyright"> </component> <component id="box" type="Box"/> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="rgb" type="RGB"> <binding name="color" value="color"/> </component> </page-specification>

Provide the onOk() listener and a "color" property in Home.java:


public abstract class Home extends BasePage { public abstract int getColor(); public void onOk() { System.out.println(Integer.toHexString(getColor())); } }

Run the application and it should work:

After clicking OK, you'll see the output in the console:


Jul 24, 2005 9:42:04 AM org.apache.catalina.core.StandardContext reload INFO: Reloading this Context has started ff0080

Now let's show the color sample. Modify RGB.html:


<html> <body jwcid="$content$"> R: <input type="text" size="3" maxlength="3" jwcid="red"/> G: <input type="text" size="3" maxlength="3" jwcid="green"/> B: <input type="text" size="3" maxlength="3" jwcid="blue"/> Sample: <span jwcid="sample" style="background-color: rgb(255, 0, 0)"> &nbsp;&nbsp;&nbsp;&nbsp;</span> </body> </html>

This HTML code almost works, except that the background color should be determined at runtime. Whenever you'd like to set the attribute of an HTML element at runtime, you can use the Any component. Modify RGB.jwc: Of course, you don't want the getBackgroundColorAssignment() method to return "abc". You'd like it to return something like "background-color: rgb(255, 0, 128)", where the actual color value is depending the red, green and blue

146

Chapter 5 Creating Custom Components

<html> <body jwcid="$content$"> R: <input type="text" size="3" maxlength="3" jwcid="red"/> G: <input type="text" size="3" maxlength="3" jwcid="green"/> B: <input type="text" size="3" maxlength="3" jwcid="blue"/> Sample: <span jwcid="sample" style="background-color: rgb(255, 0, 0)"> &nbsp;&nbsp;&nbsp;&nbsp;</span> 2: Output informal parameters </body> 1: Output element </html> 3: Render body <span style="abc"> &nbsp;&nbsp;&nbsp;&nbsp; </span> What element should I <component-specification become? It's <span>. allow-body="no" Actually, as it appears in allow-informal-parameters="no"> a <span> in the HTML ... template, it will output a <component id="sample" type="Any"> <span> by default and <binding name="element" value="literal:span"/> <binding name="style" value="backgroundColorAssignment"/> you don't really need to bind this parameter. </component> </component-specification> Call getBackgroundColorAssignment() to get the value for the style It is interesting that the Any component doesn't know of a parameter named "style". That is, this parameter is an informal parameter. Just like many other components, the Any component will output the informal parameters as HTML attributes. So, if the getBackgroundColorAssignment() method returns "abc", the output will be:

values. Therefore, modify RGB.java:


public abstract class RGB extends BaseComponent { ... public String getBackgroundColorAssignment() { return "background-color: rgb(" + getRed() + "," + getGreen() + "," + getBlue() + ")"; } }

Now, run the application and you should see the color sample:

Documenting a component
When you're writing a component, you may want to document its purpose and document each of its parameters. Take the Copyright component as an example, you may document it this way:
<component-specification allow-body="no"> <description>It renders a copyright notice.</description> <parameter name="holder" required="no" default-value="literal:Foo Inc."> <description>The copyright holder.</description> </parameter> <component id="year" type="Insert">

Creating Custom Components

147

<binding name="value" value="currentYear"/> </component> <component id="holder" type="Insert"> <binding name="value" value="holder"/> </component> </component-specification>

If the parameter is declared using the @Parameter annotation, then currently there is no way to specify a description for it. How can others view the description? There are tools that can use this information and generate a component reference page for it.

Reusing components in another project


Suppose that you'd like to reuse the components (Copyright, Box, RGB) in another project. How to do that? Let's first create another Tapestry project. Let's name it ComponentUser. Let its Home page use say the Copyright component. Modify Home.html as:
<html> <body> <span jwcid="copyright">copyright notice</span> </body> </html>

Define the component in Home.page:


<page-specification> <component id="copyright" type="Copyright"> <binding name="holder" value="literal:ABC Inc."/> </component> </page-specification>

Of course, at the moment it can't find such a Copyright component type yet. Next, you are about to package all your three components in your Components project into a jar file. But before that, let's re-organize your files a little bit. For example, consider the Copyright component type, it needs three files: Copyright.html, Copyright.jwc and Copyright.java. Let's move them into the same folder of the Copyright.java file. Do the same for the Box component type (which doesn't have a template) and the RGB component type:

This step is not absolutely necessary, but it is a good way because all the files for a component type can be found at one place. After moving the .jwc files out of the WEB-INF folder, Tapestry can no longer find them so it won't recognize your component types in your existing project (Components project) anymore. To fix this problem, you need to tell Tapestry where it can find the .jwc files for these component types. You can do that in the application specification:

148

Chapter 5 Creating Custom Components

Where can Tapestry find the .jwc file for the Copyright component? It can find it at this path. This path is relative to the application specification. So, it will try to read the file c:\workspace\Components\context\WEBINF\classes\com\ttdev\components\Copyright.jwc. <application name="Components"> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> <component-type type="Copyright" specification-path="classes/com/ttdev/components/Copyright.jwc"/> <component-type type="Box" specification-path="classes/com/ttdev/components/Box.jwc"/> <component-type type="RGB" specification-path="classes/com/ttdev/components/RGB.jwc"/> </application>

How does it work? For example, when Tapestry sees a component with the id "copyright" used as such in Home.html:
Home.html <html> <head></head> <body> <h1>Page 1</h1> This is page 1. <form jwcid="form"> <span jwcid="color">edit color here</span> <input type="submit" value="OK"/> </form> <span jwcid="box" width="50%"> <span jwcid="copyright">Copyright notice</span> </span> </body> </html> 1: There is a component here. What type is it?

It needs to find out its component type from Home.page:


2: It's type is Copyright. But where is the .jwc file for Copyright? Lookup the application specification. Home.page <page-specification class="com.ttdev.components.Home"> <component id="copyright" type="Copyright"/> <component id="box" type="Box"/> <component id="form" type="Form"> <binding name="listener" value="onOk"/> </component> <component id="rgb" type="RGB"> <binding name="color" value="color"/> </component> </page-specification>

Then it needs to find the .jwc file for the Copyright component type. It does that by looking up the application specification:

Creating Custom Components

149

3: The .jwc file should be in classes/com/ttdev/components/Copyright.j wc. This is a relative path. It is relative to the folder containing the application specification: Components.application <application name="Components"> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> <component-type type="Copyright" specification-path="classes/com/ttdev/components/Copyright.jwc"/> <component-type type="Box" specification-path="classes/com/ttdev/components/Box.jwc"/> <component-type type="RGB" specification-path="classes/com/ttdev/components/RGB.jwc"/> </application>

c: workspace Components context WEB-INF Components.application classes com ttdev components Copyright.jwc Look into the folder containing the application specification, then follow the relative path.

But who puts Copyright.jwc into there? It's Eclipse:

Eclipse will compile these files and put the resulting files into the WEBINF/classes folder while maintaining the folder structure.

However, some files can't be compiled. For example, the HTML files and .jwc files can't be compiled, so they are simply copied into there.

By the way, if Tapestry can't find the Copyright component type listed in the application specification, it will try to find the component specification in the WEB-INF folder. That's why it has been working before without editing the application specification.

150

Chapter 5 Creating Custom Components

Now, let's package your components into a jar file. Create a new text file named "Components.library" in the com.ttdev.components package:

The file should be like:


When the components are used by another application, you can no longer expect that the specification of that application to list the Java packages for the components. So you need to do that in the library specification.

<?xml version="1.0"?> <!DOCTYPE library-specification PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN" "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd"> <library-specification> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> <component-type type="Copyright" specification-path="Copyright.jwc"/> <component-type type="Box" specification-path="Box.jwc"/> <component-type type="RGB" specification-path="RGB.jwc"/> </library-specification> Three component types (Copyright, Box and RGB) are provided by this library For the Copyright component type, where is its .jwc file? It is in "Copyright.jwc". This is a path relative to the folder containing the .library file. Because they are in the same folder, just writing the filename is enough.

Right click the "Components" project and choose "Export". Choose "Jar file":

Click "Next". Exclude all files first:

Creating Custom Components

151

Then include the Components.library file and all the files comprising the three component types. No need to include say Home.java:

In the same dialog box, tell it to save the jar file as c:\workspace\Components\CompLib.jar:

152

Chapter 5 Creating Custom Components

Click "Finish" to generate the CompLib.jar file. This is your component library. To use this library in the "ComponentUser" project, copy CompLib.jar into c:\workspace\ComponentUser\context\WEB-INF\lib folder (create the folder if required). This way the application will be able to use this jar file when it is run in Tomcat. Even after that, Tapestry still can't find say the Copyright component type because as mentioned before, Tapestry will only look into the application specification or the WEB-INF folder in order to find the .jwc file (actually it will also look in some rarely used locations), but in this case the .jwc file is somewhere deep inside CompLib.jar. To tell Tapestry about it, list the library in the application specification:
ComponentUser.application <?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> <library id="myLib" specification-path="/com/ttdev/components/Components.library"/> </application> A library is listed here. It is assigned an id "myLib". In the rest of the application you can refer to this library as "myLib". It's library specification is in /com/ttdev/components/Components.library. This is a resource path and thus loaded from the classpath.

How does it load the library specification (Components.library) from the classpath? For example, if classpath contains the following locations:
c: classes c: j2sdk lib rt.jar c: workspace ComponentUser context WEB-INF lib CompLib.jar

Then it will try to load the library specification at the following locations:

Creating Custom Components

153

c: classes com ttdev components Components.library The relative path

c: j2sdk lib rt.jar com ttdev components

c: workspace ComponentUser context WEB-INF lib CompLib.jar com ttdev components Components.library

Components.library

It will be found here

After introducing the library into the application as myLib, whenever you use a component type in myLib, you need to say so. For example, in Home.page: <page-specification> <component id="copyright" type="myLib:Copyright"> <binding name="holder" value="literal:ABC Inc."/> </component> </page-specification>

This Copyright component type is defined in the "myLib" library As Tapestry already loaded the Components.library file, it can look for the Copyright component type in there:
<library-specification> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> <component-type type="Copyright" specification-path="Copyright.jwc"/> <component-type type="Box" specification-path="Box.jwc"/> <component-type type="RGB" specification-path="RGB.jwc"/> </library-specification>

Remember that the "Copyright.jwc" highlighted above is a path relative to the library specification (Components.library). So, Tapestry will try to find the Copyright.jwc file in the same folder in CompLib.jar:

154

Chapter 5 Creating Custom Components

c: workspace ComponentUser context WEB-INF lib CompLib.jar com ttdev components Components.library Copyright.jwc Try to load it

In fact, you can simplify the Components.library as:


<library-specification> <meta key="org.apache.tapestry.component-class-packages" value="com.ttdev.components"/> <component-type type="Copyright" specification-path="Copyright.jwc"/> <component-type type="Box" specification-path="Box.jwc"/> <component-type type="RGB" specification-path="RGB.jwc"/> </library-specification>

When Tapestry tries to find the .jwc file for say Copyright in the library specification but it is not listed there, Tapestry will try to find it in the same folder as the library specification. As this is indeed the case here (Copyright.jwc and Components.library are in the same folder inside CompLib.jar), it will work just fine. Now, run the ComponentUser application and it should be working:

Summary
Whenever you see duplicated code in HTML files or .page files, it's high time that you considered extracting the duplicated code into a component. If the component is to be used in this project only, just put the .jwc file into the WEBINF folder of this project and Tapestry will find it. If it is to be used in more than one projects, you need to create a library specification and list each component type in it. For each component type, you specify the path of the .jwc file relative to the library specification. To use the library in an application, list that library in the application specification and specify the resource path for the library specification. Once the library specification is found, the .jwc file of the components can also be found using relative paths. If your component has a template (HTML file), it should use the BaseComponent class which has a concept of component template or subclass it. If it will output HTML code directly in Java code, it should subclass the AbstractComponent class and override the renderComponent() method. To output HTML code in Java, use the markup writer. For a component that has a template, mostly you should use the $content$ id to get rid of the elements like <html> and <body> in the template. Your component can accept parameters so that the client can customize its behavior. A parameter can be formal or informal. To support a formal parameter, you list it in the .jwc file or declare it in Java code using @Parameter. To support informal parameters, you just need to call renderInformalParameters() in renderComponent(). To access a formal parameter, access it just like a property.

Creating Custom Components

155

When a Form component is rendered, it will call renderComponent() on all the component in it. When the form is submitted, it will also call renderComponent() on those components. This latter process is called "rewind". There is a flag in the request cycle to tell whether it is a rewind or just a normal rendering. If your component is to be used in a Form, its renderComponent() should be written to work for both render and rewind. Whenever you'd like to set the attribute of an HTML element at runtime, you should consider using the Any component.

157

Chapter 6
Chapter 6

Supporting Other Languages

158

Chapter 6 Supporting Other Languages

What's in this chapter


In this chapter you'll learn how to develop an application that can appear in two or more different languages to suit users in different countries.

A sample application
Suppose that you have an application that displays the current date:

This is easy. Create an project named CurrentDate in Eclipse and perform the setup as usual. Then modify Home.html as:
<html> <head><title>Current date</title></head> <body> Today is: <span jwcid="today">January 20, 2005</span>. </body> </html>

Home.page is like:
<page-specification class="com.ttdev.currentdate.Home"> <component id="today" type="Insert"> <binding name="value" value="today"/> </component> </page-specification>

Home.java is like:
public abstract class Home extends BasePage { public String getToday() { return new Date().toString(); } }

Supporting Chinese
Suppose that some of your users are Chinese. They would like to see the application in traditional Chinese when they run the application. To do that, create a file Home.properties in the same folder as Home.page:

Similarly, create a text file Home_zh_TW.properties. "zh" represents Chinese in general and "zh_TW" represents Chinese used in Taiwan, i.e., traditional Chinese. Right click the file and choose "Properties" and set its encoding to Big5:

Supporting Other Languages

159

Input the text strings in Chinese:

To make use of the properties files, modify Home.html:


<html> <head><title><span jwcid="title">Current date</span></title></head> <body> Today is: <span jwcid="today">January 20, 2005</span>. </body> </html>

Modify Home.page:

160

Chapter 6 Supporting Other Languages

<page-specification class="com.ttdev.currentdate.Home"> <component id="today" type="Insert"> <binding name="value" value="today"/> 2: What language does </component> the user prefer? HTTP <component id="title" type="Insert"> <binding name="value" value="message:current-date"/> request </component> </page-specification> 1: Evaluate it. The prefix is message. It means it needs to lookup a property file. 3a: Lookup this file (if the user prefers Chinese) 3b: Lookup this file (if the user prefers something other than Chinese). That is, this file is the default. Home.properties current-date=current date today-is=Today is: Home_zh_TW.properties current-date= today-is=


4a: This string is the value of the "value" parameter

4b: This string is the value of the "value" parameter

As shown above, when the Insert component needs to evaluate its "value" parameter, Tapestry will first check what is the language that the browser prefers the most to choose between Home.properties and Home_zh_TW.properties. If it prefers traditional Chinese the most, it will choose Home_zh_TW.properties. Otherwise, it will choose Home.properties. So, you can consider Home.properties the default. Then, it will search the chosen file using "currentDate" as the key. Then it will return the string on the right hand side as the value for the "value" parameter. This string is called the "message". Therefore, the Insert component will insert the message found as the page title. However, how does Tapestry know that Home_zh_TW.properties is encoded in Big5 but not say UTF-8 or some other encoding? It doesn't. You have to tell it. Do it in the application specification (CurrentDate.application): All XXX_zh_TW.properties files are in Big5 encoding
<application> <meta key="org.apache.tapestry.messages-encoding_zh_TW" value="Big5"/> <meta key="org.apache.tapestry.messages-encoding_ja" value="JIS"/> <meta key="org.apache.tapestry.messages-encoding" value="UTF-8"/> </application> These are not needed for this application. They are telling Tapestry to assume JIS encoding for XXX_ja.properties (Japanese) files and assume UTF-8 for all other .properties files.

As the application specification is only read on start up, you need to restart your application. Now you're about to run the application. To test it, configure the preferred languages in your browser. For example, in FireFox, choose "Tools | Options | Advanced":

Supporting Other Languages

161

Click "Choose" and make sure Chinese is listed as the first entry (most preferred):

Run the application and you should see the Chinese version (note the title of the web page):

If you can't see Chinese on your computer, make sure it has a font that supports Chinese. For example, if you're using Windows 2000, login as the Administrator, open the Control Panel and choose "Regional Settings" and ensure traditional Chinese support is enabled:

162

Chapter 6 Supporting Other Languages

Now you can see the Chinese version. To see the English version (the default), either delete Chinese/Taiwan:

Or simply move it down on the list:

Supporting Other Languages

163

Then reload the page and you should see the English version:

Because Tapestry will only look at the most preferred language to choose which properties file to use, sometimes the effect can be counter-intuitive. For example, if the browser prefers French the most, then Chinese and then English. Intuitively, as your application supports both Chinese and English, it should display the Chinese version because the browser prefers it to English. But in reality, Tapestry will only note that French is the most preferred language, but as there is no Home_fr.properties file, it will use the default (the English) version. Anyway, you are done with the page title. It is said that you have "internationalized" this part of the application (let it use the "message" binding prefix) and "localized" it to Chinese (provide Home_zh_TW.properties). If in the future you add support for say French, you will not need to internationalize it again but just need to localize it to French (provide Home_fr.properties). As the word "internationalization" is very long, sometimes people use "i18n" as its short form because there are 18 characters between the starting "i" and the ending "n". Similarly, people use "l10n" as a short form for "localization". You could now continue to internationalize and localize the page content, but let's consider another issue first.

How to internationalize an implicit component


Currently you're using a declared Insert component. What if you use an implicit component instead:
<html> <head><title><span jwcid="@Insert" value="???">Current date</span></title></head> <body> Today is: <span jwcid="today">January 20, 2005</span>. </body> </html>

The question is, how to set the "value" parameter? If you set it to "currentDate", it will be a constant string "currentDate". If you set it to "ognl:currentDate", it will call getCurrentDate() to find the value. Neither will read a properties file. The solution is to set it to "message:current-date":
<html> <head><title><span jwcid="@Insert" value="message:current-date">Current date</span></title></head> <body> Today is: <span jwcid="today">January 20, 2005</span>. </body> </html>

Then it will lookup the appropriate properties file using "current-date" as the key.

164

Chapter 6 Supporting Other Languages

An easier way to insert a message


Using an Insert component works, but there is an easier way to achieve the same effect. Modify Home.html:
<html> <head><title><span jwcid="@Insert" valuekey="current-date">Current date</span></title></head> <body> Today is: <span jwcid="today">January 20, 2005</span>. </body> </html>

When Tapestry sees a <span> element with a "key" attribute, it will replace the element using a message read from the properties file. Now run the application again and it should continue to work.

Internationalize the page content


Now, let's internationalize the page content. Modify Home.html:
<html> <head><title><span key="current-date">Current date</span></title></head> <body> <span key="today-is">Today is:</span> <span jwcid="today">January 20, 2005</span>. </body> </html>

The properties files already have the message: Home.properties


current-date=Current date today-is=Today is:

Home_zh_TW.properties
current-date= today-is=

Run the application, the two versions are: English Chinese

Obviously the Chinese version is not done yet: The date display is still in English. This is because you are simply calling toString() to convert the Date object to a string:
public class Home extends BasePage { public String getToday() { return new Date().toString(); } }

The right way is to check the most preferred language indicated by the browser and then format the Date object appropriately: Run the application and it should be like:

Supporting Other Languages

165

The getLocale() method is provided by BasePage. It returns the most preferred language indicated by the browser. The language is represented by a "Locale" object. What's the difference between a locale and a language? Strictly speaking, things like "fr" or "zh_TW" are locales. The locale "zh_TW" indicates the use of traditional Chinese language, the way to represent dates in Taiwan, the way to represent currency in Taiwan and etc. Therefore, a locale is broader than a language. public abstract class Home extends BasePage { public String getToday() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return dateFormat.format(new Date()); } } Request the "long" Use the DateFormat Create a date format (what object to format the Date DateFormat object does it means to get a string for this locale depends on the locale).

English

Chinese

It seems to be working fine. But what if the most preferred language is say French? Then you will be in trouble:

That is, the Date object is formatted into a French string, but the page title and other strings are in English because you don't have Home_fr.properties. To solve this problem, you should tell Tapestry that your application only supports English and Chinese. If the browser requests any other language, Tapestry will treat the request as for English instead. To implement this idea, modify the application specification: <application> <meta key="org.apache.tapestry.messages-encoding_zh_TW" value="Big5"/> <meta key="org.apache.tapestry.messages-encoding_ja" value="JIS"/> <meta key="org.apache.tapestry.messages-encoding" value="UTF-8"/> <meta key="org.apache.tapestry.accepted-locales" value="en,zh_TW"/> </application>
Only accepts English and traditional Chinese. Request for any other language will be converted to one of them. This way, if the browser requests for say zh, then zh_TW will be used because at least the base language matches. If it requests for fr, then the base language has no match so it will assume the first one listed is the default (en in this case) and use it. Now, restart the application so that the change takes effect. Then run the application with French as the most preferred language. It should display the English version.

166

Chapter 6 Supporting Other Languages

Letting the user change the locale


Suppose that a user is using a browser that prefers Chinese the most, but he would like to show the application to his friend who doesn't understand Chinese but understands English. To support this, you should enhance the application to allow the user to explicitly choose a locale:

After choosing a locale, he can click "Change":

Let's do it. Modify Home.html:


<html> <head><title><span key="current-date">Current date</span></title></head> <body> <span key="today-is">Today is:</span> <span jwcid="today">January 20, 2005</span>. <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change"/> </form> </body> </html>

Define the components in Home.page: <page-specification class="com.ttdev.currentdate.Home"> <component id="today" type="Insert"> You will need to provide this <binding name="value" value="today"/> listener </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> You will need to provide a <binding name="model" value="supportedLocales"/> getSupportedLocales() <binding name="value" value="selectedLocale"/> method to return an </component> IPropertySelectionModel </page-specification> You will need to provide this property to hold the selected locale in the combo box Let's see how to implement getSupportedLocales(). A first attempt may be:

Supporting Other Languages

167

public abstract class Home extends BasePage { public String getToday() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return dateFormat.format(new Date()); No good. This has to be a string } but you'd like it to be a Locale public abstract String getSelectedLocale(); object. public IPropertySelectionModel getSupportedLocales() { return new StringPropertySelectionModel(new String[]{"English", " }

"});

} This is no good because the selected Locale will be a string, not a Locale object. To solve this problem, you need to understand what an IPropertySelectionModel represents. In short, it represents a list of options:

IPropertySelectionModel
Value "a" "b" "c" Display label "Foo" "Bar" "XYZ" Object Object foo Object bar Object xyz

The values must be strings. You can consider the value of each option the id of that option.

The display The objects can label is a string be any object to be displayed to the user

When a PropertySelection component renders itself, it will use the IPropertySelectionModel like this:

168

Chapter 6 Supporting Other Languages

<component id="???" type="PropertySelection"> <binding name="value" value="???"/> </component> 2: Suppose it is the object foo 9: As the object is object foo, equals to the "value" parameter, output the "selected" attribute.

1: Evaluate its "value" parameter

PropertySelection
3: How many options do you have?

7: Output "a" 8: Output "Foo"

5: What is the value, display label and object for the 0'th option?

4: I have three options

6: They're "a", "Foo" and object foo respectively

IPropertySelectionModel
Value "a" "b" "c" Display label "Foo" "Bar" "XYZ" Object Object foo Object bar Object xyz

<select> <option value="a" selected="selected">Foo</option> </select>

When the form is submitted, it uses the IPropertySelectionModel like this:

Supporting Other Languages

169

<component id="???" type="PropertySelection"> <binding name="value" value="???"/> </component> 5: Set the "value" parameter to object foo

1: What's the selected value?

PropertySelection
2: It's "a"

HTTP request

3: What's the object whose value is "a"?

4: It's the object foo

IPropertySelectionModel
Value "a" "b" "c" Display label "Foo" "Bar" "XYZ" Object Object foo Object bar Object xyz

In your case, the objects are Locale objects. You can get the display label for each Locale by call getDisplayName() on it. This is method provided by the Locale class. Finally, what value to use for each Locale? You could call toString() on it which will return something like "zh_TW" or you store the Locale objects on a List and the index as the value. As the index is an int but the value must be a string, you could convert them to strings easily: Value Display label Object Value Display label Object "en" "zh_TW" "English" "Chinese" Locale English Locale Chinese "0" "1" "English" "Chinese" Locale English Locale Chinese

... ... ... ... ... ... To implement the latter idea, create a new class LocaleSelectionModel to act as an IPropertySelectionModel:

170

Chapter 6 Supporting Other Languages

public class LocaleSelectionModel implements IPropertySelectionModel { private List locales; public LocaleSelectionModel(List locales) { this.locales = locales; Get the object, display label and value } for the option at the index. These are public int getOptionCount() { used for rendering. return locales.size(); } public Object getOption(int index) { Get the display name for that locale as shown in that return getLocale(index); locale. That is, if it is Chinese, show the word } "Chinese" in Chinese, not in English. public String getLabel(int index) { return getLocale(index).getDisplayName(getLocale(index)); } public String getValue(int index) { Use the index (converted to a return Integer.toString(index); string) as the value } public Object translateValue(String value) { Get the object whose value is return getLocale(Integer.parseInt(value)); given. This is used on form } submission. private Locale getLocale(int index) { return (Locale) locales.get(index); The value is just the index } (converted to a string). So, This method is used internally. It is not required by IPropertySelectionModel. convert back to an int and then lookup the List to return the Locale object.

Return such a selection model in Home.java: public abstract class Home extends BasePage { public String getToday() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return dateFormat.format(new Date()); } Now, it is a Locale object public abstract Locale getSelectedLocale(); public IPropertySelectionModel getSupportedLocales() { List locales = new ArrayList(); locales.add(Locale.ENGLISH); locales.add(Locale.TAIWAN); return new LocaleSelectionModel(locales); } Return a LocaleSelectionModel In order to run the application now, let's define an empty listener first to make the Form component happy:
public abstract class Home extends BasePage { public String getToday() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return dateFormat.format(new Date()); } public abstract Locale getSelectedLocale(); public IPropertySelectionModel getSupportedLocales() { List locales = new ArrayList(); locales.add(Locale.ENGLISH); locales.add(Locale.TAIWAN); return new LocaleSelectionModel(locales); } public void onSetLocale() { } }

Now run the application and it should be like:

Supporting Other Languages

171

Of course as the listener is empty, so clicking on "Change" will not change the locale. Now, let's do it. You may try:
public abstract class Home extends BasePage { public abstract Locale getSelectedLocale(); ... public void onSetLocale() { setLocale(getSelectedLocale()); } } Try to set the Get the selected locale of the page locale

However, this will not work because the setLocale() method is supposed to be used only once, immediately after the page object is created, to initialize the locale of the page. It cannot be used to change the locale. If it is called twice on the same page, an exception will be thrown. The right way is:
public abstract class Home extends BasePage { public abstract Locale getSelectedLocale(); ... public void onSetLocale() { getEngine().setLocale(getSelectedLocale()); } }

This will set the locale in the engine for this application. It is the engine that creates new pages to handle requests. When the engine creates a new page object, it will use its locale to initialize the locale of the page. However, if the locale of the engine is changed, will this affect all the users using this application? No. The reason is that in fact an application has an engine for each locale (see the diagram below). When an HTTP request arrives, Tapestry will get the most preferred language in the browser to select an engine to serve the request:

Browser 1
Preferred language: en

1: Send a request

HTTP request

Application

Preferred language: en

They are exactly the same except for the locale

en 2: Pick the engine for en because the preferred language is en

Engine Engine

zh_TW

If the browser prefers say French the most and if there is no engine for fr yet, it will create one:

172

Chapter 6 Supporting Other Languages

Browser 2
Preferred language: fr

1: Send a request

Application HTTP request


en

Preferred language: fr

Engine Engine Engine

zh_TW

fr 2: There is no engine for fr yet, so create one and then use it to serve this request.

When you call setLocale() on the engine, not only its locale is set, but also a cookie is created in the browser to store the locale (see the diagram below). The next time the browser sends a request to your application, it will include that cookie in the request. Tapestry will find such a cookie in the HTTP request and will use it to select the engine and will not look at the preferred languages of the browser at all. This ensures that the user will use the locale he selected earlier. The process is like this:

Browser 1
Preferred language: en Cookie: locale=zh_TW

1: Send a request

Application HTTP request


en 2: You set its locale to zh_TW zh_TW

Preferred language: en 3: Create a cookie to store the locale

Engine Engine

HTTP request
4: Send another request. This time the cookie is included in the request. Preferred language: en Cookie: locale=zh_TW

5: Because a locale cookie is there, use it and ignore the preferred language. So choose the zh_TW engine.

Why not store the locale in a session? The cookie by default will stay in the browser for a week. It means if the user returns to the site within one week, the preferred language selected will be in effect. This effect can't be achieved with a session. To verify this, choose traditional Chinese in the combo box and then click "Change", on the browser you will find such a cookie:

Even though the cookie is there, however, you'll find that after changing the locale to Chinese, the page is still displayed

Supporting Other Languages

173

in English: Before clicking Change After clicking Change

If you reload the page, then the Chinese page will be displayed:

Why the change is not taking effect immediately? Check the code again:
public abstract class Home extends BasePage { public abstract Locale getSelectedLocale(); ... public void onSetLocale() { getEngine().setLocale(getSelectedLocale()); } }

The idea is that after setting the locale in the engine, you don't call activate() so the same page (Home) will be rendered again in the new locale. But as mentioned before, as the page object already exists, its existing locale will not change (the engine's locale is used to initialize the page locale only). As a result, the Home page will be displayed use the existing locale. To solve this problem, the idea is that if the page is created again, then it will use the engine's locale. To implement this idea:
public abstract class Home extends BasePage { abstract public Locale getSelectedLocale(); Need to use the ... request cycle public void onSetLocale(IRequestCycle cycle) { getEngine().setLocale(getSelectedLocale()); cycle.cleanup(); throw new PageRedirectException(getPageName()); } } Tell Tapestry to Release the pages loaded render the Home back to the pool page again

How does it work? When you need to load the Home page (see the diagram below), if the locale of the engine is en, then the request cycle will try to find a Home page in the pool whose locale is also en. If not found, it will create one and initialize its locale to en. Now, you set the engine's locale to zh_TW and then tell the request cycle to release the Home page back to the pool. Finally you tell Tapestry to load the Home page again. Again, it will try to find a Home page in pool whose locale is the engine's locale, but the engine's locale has changed to zh_TW. Even though there is indeed a Home page in the pool (the one you just released) but its locale is en, not zh_TW. So, this is not a match. If there is another Home page in the pool whose locale is zh_TW, then it will be used, otherwise a new one will be created and its locale will be initialized to the engine's locale (zh_TW).

174

Chapter 6 Supporting Other Languages

1: Load the Home page

5: Load the Home page again

Engine Request cycle Home


en en 3: Set the locale to zh_TW

Page pool
4: Release back to the pool

2b: If no such a page in the pool, create one. Use the locale of the engine (en) to initialize its own locale.

... Home
en

2a: Try to find a Home page in the pool with the engine's locale (en)

... ...

6b: If no such a page in the pool, create one. Use the locale of the engine (zh_TW) to initialize its own locale.

6a: Try to find a Home page in the pool with the engine's locale (zh_TW)

Instead of throwing a PageRedirectException, you could call activate the Home page again:
public abstract class Home extends BasePage { public abstract Locale getSelectedLocale(); ... public void onSetLocale(IRequestCycle cycle) { getEngine().setLocale(getSelectedLocale()); cycle.cleanup(); cycle.activate(getPageName()); } }

The activate() method will create a new Home page if it can't find one in the pool. You may wonder what's the difference between calling activate() and throwing a PageRedirectException. The former will load the page and set it as the response page for rendering. The rendering will start only after the listener returns. In contrast, throwing a PageRedirectException will cause the rendering to start immediately. In addition, you should call activate() in a listener only. If you call it when the response page is rendering, obviously it will have no effect. In contrast, throwing a PageRedirectException will work no matter you're in a listener or has started rendering the response page (the output so far will be discarded). Let's return to the code again. What if you don't throw a PageRedirectException nor call activate():
public abstract class Home extends BasePage { public abstract Locale getSelectedLocale(); ... public void onSetLocale(IRequestCycle cycle) { getEngine().setLocale(getSelectedLocale()); cycle.cleanup(); cycle.activate(getPageName()); } }

Usually it should have the effect of rendering the current page (Home), but this time it won't work. Instead, an error will occur because Tapestry will try to use this existing page for rendering which has been released to the pool. Now run the application and the "Change" button should work fine. However, there is a minor problem: The "Change" button is not localized. It's easy. Just add a line to Home.properties and Home_zh_TW.properties: Home.properties
current-date=Current date today-is=Today is: change=Change

current-date= today-is= change=

Home_zh_TW.properties

Supporting Other Languages

175

Now, all you need is to let the "Change" button read the message from them. Modify Home.html:
<html> <head><title><span key="current-date">Current date</span></title></head> <body> <span key="today-is">Today is:</span> <span jwcid="today">January 20, 2005</span>. <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

Define the component in Home.page:


<page-specification class="com.ttdev.currentdate.Home"> <component id="today" type="Insert"> <binding name="value" value="today"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component> </page-specification>

Note that the Submit component knows nothing about a parameter named "value". That is, it is an informal parameter and will be added as an attribute for the HTML submit element to be generated. If you'd like, you could use the Any component here instead of a Submit component to achieve exactly the same effect. Now run the application again and the button should be localized: English Chinese

Selecting the current locale in the combo box


There is still a minor issue here. Note above that when the page is displayed, the locale selected in the combo box is always English, even if the current locale is Chinese. This is because you are not initializing the "selectedLocale" property at all, so it is null and in that case the PropertySelection component will simply select the first option. This is no good. You'd like to it to select the current locale. That is, if the current locale is Chinese, Chinese should be selected in the combo box. To achieve this effect, you need to set the current locale into the "selectedLocale" property of the page object before the PropertySelection component calls getSelectedLocale(). The idea is that, you can initialize this property just before the Home page is rendered. To do that, let the Home class implement the PageBeginRenderListener interface:

176

Chapter 6 Supporting Other Languages

public abstract class Home extends BasePage implements PageBeginRenderListener { public String getToday() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return dateFormat.format(new Date()); } public abstract Locale getSelectedLocale(); public abstract void setSelectedLocale(Locale locale); public IPropertySelectionModel getSupportedLocales() { List locales = new ArrayList(); locales.add(Locale.ENGLISH); locales.add(Locale.TAIWAN); return new LocaleSelectionModel(locales); } public void onSetLocale(IRequestCycle cycle) { getEngine().setLocale(getSelectedLocale()); cycle.cleanup(); cycle.activate(getPageName()); } public void pageBeginRender(PageEvent event) { setSelectedLocale(getLocale()); } } Get the locale of the page and set it into the "selectedLocale" property The PageBeginRenderListener interface declares a pageBeginRender() method. So, you must implement this method here. Before the Home page is rendered, Tapestry will note that the page object implements the PageBeginRenderListener, so it will call the pageBeginRender() method on the page object.

Now, run the application again and you should see that the current locale is selected in the combo box: English Chinese

Localizing the full stop


There is still a minor issue here. The full stop used at the end of the sentence above is the English one, not the Chinese one (yes, there is a Chinese full stop). To solve this problem, you could add a new entry to your properties files for the full stop: Home.properties
current-date=Current date today-is=Today is: change=Change fullStop=.

current-date= today-is= change= fullStop=

Home_zh_TW.properties

Then change Home.html to:


<html> ... <span key="today-is">Today is:</span> <span jwcid="today">January 20, 2005</span><span key="fullStop">.</span> ... </html>

This would work. But you are now breaking the sentence up into three parts:

Today is: May 15, 2005.

Supporting Other Languages

177

This is getting too complicated. As an alternative, you could put the whole sentence into the properties files: Home.properties
current-date=Current date today-is=Today is: {0}. change=Change

current-date= today-is= change=

{0}

Home_zh_TW.properties

Then modify Home.html as:


<html> <head><title><span key="current-date">Current date</span></title></head> <body> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

"todayIs" can be just an Insert component:


<page-specification class="com.ttdev.currentdate.Home"> <component id="today" type="Insert"> <binding name="value" value="today"/> </component> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> ... </page-specification>

As shown above, you no longer need to output the current date through the "today" component as you will be outputing the whole sentence using the "todayIs" component. Therefore, you no longer need the getToday() but need a getDateSentence() method instead. Do these in Home.java:
This getter will load the message for the key "today-is" from the properties file. It gets the key from the method name. If the method were named getFooBar(), it will use the key "foobar". public abstract class Home extends BasePage implements PageBeginRenderListener { @Message You could pass more than one public abstract String getTodayIs(); objects and it will replace them for ... {0}, {1} and so on in the pattern public String getToday getDateSentence() { respectively. Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); return String dateStr = dateFormat.format(new Date()); String pattern = getTodayIs(); return new MessageFormat(pattern).format(new Object[] { dateStr }); } } It should be a string like "January 29, 2005" Load the message from the properties file for the key "today-is"

Replace for

It should be a string like "Today is: {0}."

"Today is: {0}." Result

"January 29, 2005"

"Today is: January 29, 2005."

In fact, it can load a message and perform the formatting in one step:

178

Chapter 6 Supporting Other Languages

public abstract class Home extends BasePage implements PageBeginRenderListener { @Message Load the message for "todaypublic abstract String getTodayIs(String dateStr); is", take it as a pattern and ... replace dateStr for {0} in it. public String getDateSentence() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); String dateStr = dateFormat.format(new Date()); return getTodayIs(dateStr); } }

You don't have to use annotations. To load a message in plain old Java code, do it this way:
public abstract class Home extends BasePage implements PageBeginRenderListener { public String getDateSentence() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); String dateStr = dateFormat.format(new Date()); String pattern = getMessages().getMessage("today-is"); return new MessageFormat(pattern).format(new Object[] { dateStr }); } ... }

To load a message and perform the formatting in one step:


public abstract class Home extends BasePage implements PageBeginRenderListener { public String getDateSentence() { Locale locale = getLocale(); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); String dateStr = dateFormat.format(new Date()); return getMessages().format("today-is", new Object[] { dateStr }); } ... }

Now, run the application and the Chinese version should display the Chinese full stop: English Chinese

Displaying a logo
Suppose that you'd like to display a logo on the Home page. You have created a logo (a GIF image) as shown below:

Supporting Other Languages

179

To display it, it's easy. Let's save it into c:\workspace\CurrentDate\context as Logo.gif:

Then modify Home.html:


<html> <head><title><span key="current-date">Current date</span></title></head> <body> <img src="/CurrentDate/Logo.gif"><p> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

Note that this is just plain HTML code and has got nothing to do with Tapestry at all. Now, run the application and it should work:

However, there is a problem with this approach. In this case your application is accessed as /CurrentDate (the context path) because the context descriptor is named CurrentDate.xml (Tomcat 5.5) or because it is specified in your context descriptor:
<Context docBase="c:/workspace/CurrentDate/context" path="/CurrentDate" reloadable="true"/>

The problem is, when you're writing the application, generally you do not know what its context path will be, because one company may deploy it as /CurrentDate while another may deploy it as /Foo. Therefore, you just can't hard code the context path in Home.html:
<html> ... <img src="/CurrentDate/Logo.gif"><p> ... </html>

To solve this problem, can you use a relative path?

180

Chapter 6 Supporting Other Languages

<html> ... <img src="../Logo.gif"><p> ... </html>

The idea is that as Home.html is in c:\workspace\CurrentDate\context\WEB-INF, you could use ".." to go back one level and expect to find Logo.gif there. If on the server, you launch a browser and use "File | Open" to open Home.html, then it will work fine (i.e., you can see the logo). But if you use a browser on a client to try to access Home.html using a URL like http://localhost:8080/CurrentDate/WEB-INF/Home.html, then you'll see that access is denied:

Why? For security reason, a web application server including Tomcat will not allow browsers to access any files under the WEB-INF folder. That is, only your application can access files under this folder, but browsers can't. You may wonder how you have been displaying Home.html to the client? The answer is that you are not displaying Home.html at all. You are displaying the Home page, but not Home.html. To display the Home page, the client must ask the application to show the Home page for it, using the URL http://localhost:8080/CurrentDate/app?service=page&page=Home. That is, you are calling the page service object to display the Home page. The page service will load your Home page and ask it to render itself. It will consult Home.page and Home.html and ask the components to generate HTML code on the fly. This is a very important point. As the HTML code is generated on the fly, it is meaningless to say "the location" of this chunk of HTML code. It has no location at all. Therefore, if the HTML code contains a relative path like:
<html> ... <img src="../Logo.gif"><p> ... </html>

The meaning is completely undefined. Now, back to the question of how to refer to the Logo.gif file in the <img> element? On one hand, you can't use a relative path because the HTML code has no location. On the other hand, if you use an absolute path you must hard code the context path which varies from one deployment to another. To solve this problem, you can make use of the asset concept in Tapestry. An asset is a Java object that represents some data that can be retrieved by a URL. For example, see the diagram below, if an asset object represents an image on a server, then you can ask it for the URL to retrieve its image data:
1: I'd like to retrieve your data. What is the URL?

Some asset object


2: It's http://www.foo.com/Logo.gif

There are several kinds of assets in Tapestry: For the case at hand, you can use a context asset and store the path /Logo.gif in it. At runtime you can ask it for the full URL and then it will return something like http://localhost/CurrentDate/Logo.gif. To implement this idea, modify Home.html:

Supporting Other Languages

181

IAsset

An interface. It only knows it can return a URL but doesn't know how.

Implements

Implements

Implements

To be explained later

ContextAsset
/bar/image.gif

ExternalAsset
http://www.abc.com/bar/image.gif

PrivateAsset

What's your URL?

What's your URL? It's http://www.foo.com/xyz/bar/image.gif

It's http://www.abc.com/bar/image.gif

The context asset object will find out the hostname of the server (www.foo.com) and the context path of this application (/xyz) automatically, then append the path stored in it to form the full URL.
<html> <head><title><span key="current-date">Current date</span></title></head> <body> <img src="/CurrentDate/Logo.gif" jwcid="logo"><p> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

You'd like to turn the <img> HTML element into a component. Define the component in Home.page:

182

Chapter 6 Supporting Other Languages

Tell Tapestry to create a context asset object and add it into a Map named "assets" in the page object. This happens when the page is created.

Home
assets Key "logo" ... ... ... ... Object

ContextAsset
/Logo.gif

Home.page <page-specification ...> <asset name="logo" path="context:/Logo.gif"/> ... <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

The context prefix states that this is a context asset

This retrieves the asset object from the map. The Image component will ask it for the URL and then generate the HTML code:

<img src="xyz">

Now, run the application. But it doesn't work:

Supporting Other Languages

183

It is saying that the line 5 shown in Home.html above should not "appear inside an ignored block". Why? Note that your Image component only has a start tag <img>, but there is no end tag </img>. So, Tapestry will consider everything following as the body of the Image component. But the Image component will not use its body, so in Image.jwc, the "allow-body" is set to "no". So, as you're having some components in the body, Tapestry is telling you that you're doing something unreasonable. Of course, Tapestry is missing the real problem. You should really provide a </img> tag:
<html> <head><title><span key="current-date">Current date</span></title></head> <body> <img src="/CurrentDate/Logo.gif" jwcid="logo"></img><p> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

Or just add a slash to the end of the start tag:


<html> <head><title><span key="current-date">Current date</span></title></head> <body> <img src="/CurrentDate/Logo.gif" jwcid="logo"/><p> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> </body> </html>

Note that this slash is needed only when it is a Tapestry component. At the beginning it was just an HTML <img> element. HTML is not XML and it does allow tags like <img>, <br>, <hr> that don't have a corresponding end tag. But once it is turned into a component, you must end it properly. Now run the application again and it should work:

184

Chapter 6 Supporting Other Languages

Localizing the logo


However, there is still a problem. This logo is not suitable for the Chinese version because it is in English:

In addition, "4" in Chinese doesn't mean "for" at all. In fact, it is pronounced just like the word "death" in Chinese so people tend to avoid it in names. So, you'd like to have a Chinese version of the logo. Suppose it is like this: To use it for the Chinese version, just save this image as Logo_zh_TW.gif along with Logo.gif:

There is no need to change Home.html or Home.page at all. Just run the application and it will work:

Supporting Other Languages

185

If you view the HTML source code of the web page, you'll see that it is really using the Logo_zh_TW.gif file:
<html> <head><title> </title></head> <body> <img src="/CurrentDate/Logo_zh_TW.gif" border="0"/><p> ... </html

But as Home.page only refers to the Logo.gif file:


<page-specification class="com.ttdev.currentdate.Home"> <asset name="logo" path="context:/Logo.gif"/> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component> <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

How does the Image component get to use the Logo_zh_TW.gif file instead of Logo.gif file? This is because when Tapestry loads the Home.page file and is about to create the context asset (see the diagram below), it notes the path is /Logo.gif. Then it will check to see if that file really exists. Then, it notes the locale of the engine is say zh_TW. So it will see if Logo_zh_TW.gif exists in the same folder as Logo.gif. As it does exist, it will store the path /Logo_zh_TW.gif into the context asset object instead of /Logo.gif. As the Image component will simply ask the context asset for the URL, the Image component really doesn't know that the image file path has been replaced!

186

Chapter 6 Supporting Other Languages

1: When Tapestry is about to create the context asset object, it notes this path.

<page-specification ...> <asset name="logo" path="context:/Logo.gif"/> ... <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

File system
2: It checks if this file really exists under the document base. If not, it will throw an exception. c: workspace CurrentDate context Logo.gif 3: It finds the current locale of the engine. Suppose that it is zh_TW, so it will check if Logo_zh_TW.gif exists in the same folder of Logo.gif. Suppose it does exist. Logo_zh_TW.gif

ContextAsset
/Logo_zh_TW.gif

4: Create a context asset containing /Logo_zh_TW.gif

Putting the images into other places


It is common for people to put their images into an image folder. For example, you could put your two GIF files into c:\workspace\CurrentDate\context\images:

Then all you need is to change the path:


<page-specification ...> <asset name="logo" path="context:/images/Logo.gif"/> ... <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

Then everything will work. Instead of putting them into an images folder, can you put them into WEB-INF?

Supporting Other Languages

187

After changing the path in the <asset> element to /WEB-INF/Logo.gif, if you run the application, the HTML code generated will be like:
<html> ... <img src="/CurrentDate/WEB-INF/Logo.gif" border="0"/><p> ... </html>

This will not work. As mentioned before, a web application server will not allow the browser to access any files under the WEB-INF folder. So, the browser will fail to retrieve the GIF file.

Creating a license page


Suppose that you'd like to add a link on the Home page to display the license agreement like this:

Of course, the license page must also support both English and Chinese. Let's create a page named License. License.page is simple:
<page-specification> <asset name="logo" path="context:/images/Logo.gif"/> <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

Then let your lawyers and web designers work together to edit License.html. Suppose it is like: The corresponding HTML code is:

188

Chapter 6 Supporting Other Languages

<html> <head> <title>License</title> </head> <body> <img src="../images/Logo.gif" jwcid="logo"/> <h1>LICENSE AGREEMENT</h1> <ol> <li>Definitions.</li> "Application" refers to this "Date4U" application.<p> "You" refers to the licensee.<p> "We" refers to "Date4U Inc."<p> <li>Grant of Copyright License.</li> Subject to the terms and conditions of this License, you are granted a non-exclusive right to use this Application.<p> </ol> </body> </html>

To localize it, you can put all the text into properties files, but this is quite troublesome. In fact, after putting the text into properties files, your web designers will no longer be able to use a WYISWYG editor like DreamWeaver to visually edit the page. They will have to edit the properties files but they can't see the changes in DreamWeaver. Instead of using properties files, you could put the above HTML code into License.html and create another file License_zh_TW.html for the Chinese version:

Supporting Other Languages

189

This way your web designers can use DreamWeaver to visually edit both files. License_zh_TW.html is like this:

This file is in the Big5 encoding, which is the default encoding for traditional Chinese. Now, when the browser requests for the License page, as usual Tapestry will need to find the HTML template for it. If the locale of the page is traditional Chinese, it will try to see if License_zh_TW.html exists. In your case, it does exist. So, Tapestry will use it as the template for the License page and will not use License.html at all. However, how does Tapestry know that this file is in Big5 encoding? It doesn't. You have to tell it. You can do it in the application specification:
<application> <meta key="org.apache.tapestry.messages-encoding_zh_TW" value="Big5"/> <meta key="org.apache.tapestry.messages-encoding_ja" value="JIS"/> <meta key="org.apache.tapestry.messages-encoding" value="UTF-8"/> <meta key="org.apache.tapestry.accepted-locales" value="en,zh_TW"/> <meta key="org.apache.tapestry.template-encoding_zh_TW" value="Big5"/> </application>

It is telling Tapestry that if a template file is named like XXX_zh_TW.html, then assume it is in Big5 encoding. Don't forget to restart the application. To check that if it's working, set the preferred language to Traditional Chinese in the browser and then go to URL http://localhost:8080/CurrentDate/app?service=page&page=License, you will see that it is working. Now, create a PageLink on the Home page. Modify Home.html:
<html> <head><title><span key="current-date">Current date</span></title></head>

190

Chapter 6 Supporting Other Languages

<body> <img src="/CurrentDate/Logo.gif" jwcid="logo"/><p> <span jwcid="todayIs">Today is: January 20, 2005.</span> <form jwcid="setLocale"> <select jwcid="locale" name="locale"> <option value="0">English</option> <option value="1">Chinese</option> </select> <input type="submit" value="Change" jwcid="change"/> </form> <a jwcid="license"><span key="license">License</span></a> </body> </html>

Define the component in Home.page:


<page-specification class="com.ttdev.currentdate.Home"> <asset name="logo" path="context:/images/Logo.gif"/> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component> <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> <component id="license" type="PageLink"> <binding name="page" value="literal:License"/> </component> </page-specification>

Define the message for the key "license" in the properties files: Home.properties
current-date=Current date today-is=Today is: {0}. change=Change license=License

current-date= today-is= change= license=

{0}

Home_zh_TW.properties

Run the application and it should work: English Chinese

Now, there are two ways to localize a page. You could use a single template and a properties file for each language (e.g., Home page) or use a template for each language (e.g., License page). Which way should you use? It's up to you to judge which way is easier. If you have a template for each language, whenever you need to add a component, you need to add it to each template. So, when you have quite some components, the using a single template with different

Supporting Other Languages

191

properties file should be easier. If you have quite a lot of static text, then using different templates may be easier because it allows the web designers to edit the text visually. If you need to support say Hebrew which layouts the text from right to left, then using a separate template for Hebrew should be a good idea.

Observing the output encoding


Now, let's display the License page in Chinese. Then in the browser, check the encoding used. For example, in FireFox, choose "Tools | Page Info":

Note that the encoding used is UTF-8. But your License_zh_TW.html is in Big5. Why? If you also check the English version, you'll find that it's also in UTF-8. This is because Tapestry always use UTF-8 to encode the output, regardless the encoding of the template. Tapestry provides a way to let you specify the encoding for the output, however generally there is little motivation to do that.

Creating a Logo component


Both the Home page and the License page have a logo at the top. For the logo each page must also have a context asset. It means their .page files contain quite some duplicate code:

192

Chapter 6 Supporting Other Languages

<page-specification class="com.ttdev.currentdate.Home"> <asset name="logo" path="context:/images/Logo.gif"/> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component> <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> <component id="license" type="PageLink"> <binding name="page" value="literal:License"/> </component> </page-specification>

Duplicate

<page-specification> <asset name="logo" path="context:/images/Logo.gif"/> <component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> </page-specification>

Whenever you see duplication in .page files, it is a good time to extract what's in common to create a component. So, create a new component type Logo. Logo.html should be:
<html> <body jwcid="$content$"> <img src="../images/Logo.gif" jwcid="logo"/> </body> </html>

Logo.jwc should be:


It will render the components listed here. It will not use its body. So, no component are allowed in its body. Allow informal parameters. If there are any, put them to the generated <img> element.

<component-specification allow-body="no" allow-informal-parameters="yes"> <asset name="logo" path="context:/images/Logo.gif"/> The meaning is the same as <component id="logo" type="Image"> before, except that the asset <binding name="image" value="asset:logo"/> object will be added to the </component> assets Map of the Logo </component-specification> component, not a page object. Access the context asset in the component

To use the Logo component in the Home page, modify Home.page:


<page-specification class="com.ttdev.currentdate.Home"> <asset name="logo" path="context:/images/Logo.gif"/> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component>

Supporting Other Languages

193

<component id="logo" type="Image"> <binding name="image" value="asset:logo"/> </component> <component id="logo" type="Logo"/> <component id="license" type="PageLink"> <binding name="page" value="literal:License"/> </component> </page-specification>

Perform similar changes to the License page. Now run the application and it should continue to work.

Setting the ALT attribute of the logo


It is a good idea to set the ALT attributes of all images so that they work even in text-based browsers. Of course, the ALT text should also be localized. OK, let's modify Logo.jwc:
<component-specification allow-body="no" allow-informal-parameters="yes"> <asset name="logo" path="context:/images/Logo.gif"/> <component id="logo" type="Image" inherit-informal-parameters="yes"> <binding name="image" value="asset:logo"/> If there are any informal parameters <binding name="alt" value="message:date4U"/> for us (this Logo component), pass </component> all of them to this Image </component-specification> component. The Image component knows nothing about a parameter named "alt". That is, it is an informal parameter and will be rendered as is for the <img> element to be generated.

Load the message for the key "date4U" and use it as the ALT attribute

From where it will load the messages? A component, just like a page, should have its own properties files. So, create two properties files for it: Logo.properties and Logo_zh_TW.properties in the same folder as Logo.jwc: Logo.properties
date4U=Date4U

date4U=

Logo_zh_TW.properties

Now run the application and the "alt" attribute should be set:

To check if you can really pass informal parameters to the <img> element, modify Home.html:
<html> <head><title><span key="current-date">Current date</span></title></head> <body> <img src="/CurrentDate/Logo.gif" jwcid="logo" height="50" width="100"/><p> ... </body> </html>

View the Home page again and the logo should appear larger (50 pixels x 100 pixels):

194

Chapter 6 Supporting Other Languages

Packaging the Logo component


Suppose that your company starts to provide some other applications but the Date4U image has become a logo for the company, not just for that single application. So, you would like to use this logo in the other applications. Therefore, you need to package the Logo component. To do that, let's create a new Tapestry project named Date4ULibrary and copy the Logo.html, Logo.jwc, Logo.properties and Logo_zh_TW.properties into an appropriate package (e.g., com.ttdev.date4ulibrary):

Create a library specification Date4ULibrary.library in that package with the following content:
<?xml version="1.0"?> <!DOCTYPE library-specification PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN" "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd"> <library-specification> <meta key="org.apache.tapestry.messages-encoding_zh_TW" value="Big5"/> </library-specification> This setting is required so that Tapestry will know that Logo_zh_TW.properties is in Big5. Even though you already have this setting in your application specification, it has no impact on the components inside a library. The rule is, if a component or a page is in an application, it is affected by the application specification. If it is in a library, it is affected by the library specification.

As the Logo.jwc is in the same folder as the library specification, there is no need to list the Logo component type in the specification. Let's consider Logo.jwc again:
<component-specification allow-body="no" allow-informal-parameters="yes"> <asset name="logo" path="context:/images/Logo.gif"/> <component id="logo" type="Image" inherit-informal-parameters="yes"> <binding name="image" value="asset:logo"/>

Supporting Other Languages

195

<binding name="alt" value="message:date4U"/> </component> </component-specification>

At the moment it is using a context asset to locate the GIF files (Logo.gif and Logo_zh_TW.gif). It means that an application using this library must provide these two files in /images in its context path. This is no good. You'd like the library to provide these files so that the applications don't need to worry about this. To do that, copy the two GIF files into the same package folder:

Then, do not use a context asset anymore: <component-specification allow-body="no" allow-informal-parameters="yes"> <asset name="logo" path="context:/Logo.gif"/> <asset name="logo" path="classpath:Logo.gif"/> <component id="logo" type="Image" inherit-informal-parameters="yes"> <binding name="image" value="asset:logo"/> <binding name="alt" value="message:date4U"/> </component> </component-specification> This is a relative path to the Logo.jwc The classpath prefix states that the file. As Logo.jwc will be put into a jar asset should be loaded from the file, Logo.gif should also be put into that classpath. Such an asset is called a jar file in the same resource folder. "private asset" as it is usually private to a component (Logo in this case). If later the Logo component is packaged into Date4U.jar and this jar file is used by the CurrentDate application, then the Logo.gif should be loaded at the location shown:
c: workspace CurrentDate context WEB-INF lib Date4U.jar com ttdev date4u Date4ULibrary.library Logo.jwc Logo.gif Logo_zh_TW.gif Try to load them Later, the component is packaged as Date4U.jar and deployed into the CurrentDate application.

Now you're about to package the files into a jar file. You could use Eclipse to do that. But having to do it manually is quite time consuming. Fortunately, you can automate this process.

196

Chapter 6 Supporting Other Languages

Automating the package process


Create a new text file "build.xml" in the project folder in Eclipse. Use auto-completion and choose "<> project":

This will create a <project> element. Then use auto-completion to input the following:
This is a project. It is named "Date4U". You can use any name you'd like. There is a "target" named "makeJar". A target contains some steps. Each step is called a "task". When you execute a target, its tasks will be executed one by one.

The default target is the target named "makeJar"

The tasks of this target are listed here. In this case there is only one task.

<project name="Date4U" default="makeJar"> <target name="makeJar"> Create this jar file <jar destfile="Date4U.jar" basedir="context/WEB-INF/classes" includes="**/*.library, **/*.html, **/*.jwc, **/*.class, **/*.properties, **/*.gif"/> </target> </project> When looking for files to include in the jar file, use context\WEBINF\classes as the base directory. As the current directory for Ant is c:\workspace\Date4U, it will look for files under c:\workspace\Date4U\context\WEB-INF\classes. Include .library files, .html files, .jwc files, .class files, .properties files and .gif files under the base directory.

How to run this build.xml file? Just right click it and choose "Run | Ant Build". Then you should see some output messages in the console:

Refresh the project in Eclipse to see the jar file if you don't see it yet:

Now, let's use this library (which includes the Logo component) in the original CurrentDate project. Delete all the Logo*.* files. As there is no images in the project now, you can also delete the "images" folder:

Supporting Other Languages

197

Create a folder c:\CurrentDate\context\WEB-INF\lib and copy Date4U.jar into there so that it is available to your application when it is run in Tomcat. In order to make it available in Eclipse, right click the project and choose "Properties". Then choose "Java Build Path" and add the jar file by clicking "Add JARs":

List the library in CurrentDate.application:


<application> <meta key="org.apache.tapestry.messages-encoding_zh_TW" value="Big5"/> <meta key="org.apache.tapestry.messages-encoding_ja" value="JIS"/> <meta key="org.apache.tapestry.messages-encoding" value="UTF-8"/> <meta key="org.apache.tapestry.accepted-locales" value="en,zh_TW"/> <meta key="org.apache.tapestry.template-encoding_zh_TW" value="Big5"/> <library id="Date4U" specification-path="/com/ttdev/date4ulibrary/Date4ULibrary.library"/> </application>

Note that this path is not a file path, but a resource path. That is, Tapestry will look into the various jar files for the Date4ULibrary.library file or under the folders specified in the classpath. In your case, it will find the file in the Date4U.jar file. Finally, let's refer to the Logo component as "Date4U:Logo" instead of just "Logo". For example, in Home.page:
<page-specification class="com.ttdev.currentdate.Home"> <component id="todayIs" type="Insert"> <binding name="value" value="dateSentence"/> </component> <component id="setLocale" type="Form"> <binding name="listener" value="listener:onSetLocale"/> </component> <component id="locale" type="PropertySelection"> <binding name="model" value="supportedLocales"/> <binding name="value" value="selectedLocale"/> </component> <component id="change" type="Submit"> <binding name="value" value="message:change"/> </component>

198

Chapter 6 Supporting Other Languages

<component id="logo" type="Date4U:Logo"/> <component id="license" type="PageLink"> <binding name="page" value="literal:License"/> </component> </page-specification>

Do something similar in License.page. Restart the application and run it. It should continue to work: English Chinese

How can the browser access the GIF files?


Before you created the Logo component, the two GIF files were in the c:\workspace\CurrentDate\context\images folder. It meant that the browser could refer to the Logo.gif file using an URL such as http://localhost/CurrentDate/images/Logo.gif. But now the GIF files are deep inside the Date4U.jar file, how can the browser refer to it using an URL? In fact, as the jar file is in c:\workspace\CurrentDate\context\WEB-INF\lib and everything under WEB-INF is inaccessible by the browser, there is no way for the browser to access the jar file at all, not to mention a file inside the jar file. To find out how it works, let's check the HTML code of the page: %2F means slash. It is encoded this It means &. It is encoded way in a URL. this way when it is used in a URL. <html> <head><title>Current date</title></head> <body> <img src="/CurrentDate/app?digest=8c391192d89e5d2808ae935795aec69c&amp;path= %2Fcom%2Fttdev%2Fdate4ulibrary%2FLogo.gif&amp;service=asset" border="0" alt="Date4U"/> <p> ... What this URL really </html> means
/CurrentDate/app?digest=8c391192d89e5d2808ae935795aec69c&path=/com/ttdev/date4ul ibrary/Logo.gif&service=asset This is the check sum of Logo.gif. The asset service will check if it is correct. If it is not correct, it will not return the asset data. This prevents hackers from using the asset service to retrieve any classes or files on the classpath. The asset service will load the file from the classpath

Call the asset service to get the data of the asset

Summary
To internationalize a page, you can extract the strings into properties files, one for each supported language. To display a string in a properties file directly, use a <span key="...">. To load a string and bind it to a parameter, use the message prefix. To load it in Java code, use @Message or call getMessages() and then getMessage(). Tapestry will load the correct properties file according to the current locale for the user. To determine the current locale, Tapestry will check the most preferred language as specified in the HTTP request. It may cause trouble is the requested locale is not

Supporting Other Languages

199

supported by your application. So it is good to tell Tapestry what locales your application supports so that it will convert the requested locale into the one that will be honored. To do that, just set the org.apache.tapestry.accepted-locales option. To change the locale for the user, you can set the current locale in the engine, Tapestry will store it into a cookie and use it to determine the current locale instead. If a properties file is not in ASCII, you need to tell Tapestry the encoding used by setting org.apache.tapestry.messages-encoding. To compose a sentence from various bits and pieces, you can use {0} and {1} and so on in a pattern string and at runtime substitute string values for them. Tapestry can load the pattern string and perform formatting in one step. Instead of using properties files, you can have a different HTML template for each language. Tapestry will load the correct template according to the current locale. This is suitable for pages that contain a lot of static text but very few components. If a template is not encoded in the default encoding of the OS, you need to specify it by setting the option org.apache.tapestry.template-encoding in your application specification. To display an image, you can't use a path relative to your HTML file because the HTML code is generated on the fly. Instead, use an asset object with an Image component. An asset represents some data that can tell you its URL. If the image file is somewhere under your context folder (except WEB-INF), use a context asset so that the path is independent of the context path. If it is at some other place accessible with an arbitrary URL, use an ExternalAsset instead. If it is bundled with a component in a library, use a private asset instead. To use a private asset, list it in the . jwc file of a component and specify the path to the image file relative to the .jwc file. This way, after finding the .jwc file, the image file can be found by following the relative path. As a private asset is deep down inside a jar file, it is retrieved through the asset service. To internationalize an image, it must be a context asset or a private asset. You just need to create a image file for each language. Tapestry will load the correct image file according to the current locale. If you need to redisplay the current page in another locale, you need to release the page (with the old locale) to the pool by calling cycle.cleanUp() so that a page with the current locale is created or retrieved from the pool. If you have a component that contains a primary component such as your Logo component that contains an Image component, you can let that primary component inherit the informal parameters of its container (your component) by setting inherit-informal-parameters to yes. An IPropertySelectionModel is a list of (value, display label, object) pairs. The value can be considered the id of the object. The display label is what is shown to the user in the combo box. The object is the selected value. Using an IPropertySelectionModel a PropertySelection component can display the combo box, set the currently selected option and etc. If you need to initialize some of the properties of your page, you should implement PageBeginRenderListener and implement the pageBeginRender() method. It is called just before the page is rendered. To make a jar file easily, you can use write an Ant build file to do that. It can match source files any level inside a folder structure.

201

Chapter 7
Chapter 7

Using the Table Component

202

Chapter 7 Using the Table Component

What's in this chapter?


In this chapter you'll learn how to use the Table component to display a long list of data entries.

Creating a phone book


Suppose that you'd to have an application that displays a phone book like:

First create a Tapestry project named PhoneBook as usual. Create a class PhoneBookEntry to represent an entry in the phone book:
package com.ttdev.phonebook; public class PhoneBookEntry { private int id; private String firstName; private String lastName; private String telNo; public PhoneBookEntry( int id, String firstName, String lastName, String telNo) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.telNo = telNo; } public String getFirstName() { return firstName; } public int getId() { return id; } public String getLastName() { return lastName; } public String getTelNo() { return telNo; } }

For this application, all the classes will be in the com.ttdev.phonebook package. Next, create a PhoneBook class to present a phone book. Usually, it should store the entries in a database, but here let's simulate the database using a Java List:
public class PhoneBook { private List entries;

Using the Table Component

203

public PhoneBook() { entries = new ArrayList(); } public void addEntry(PhoneBookEntry entry) { entries.add(entry); } public List getEntries() { return entries; } }

There should be a concept of a global phone book. So, let's create a GlobalPhoneBook class. In addition, to test the application, let's hard code some entries:
public class GlobalPhoneBook extends PhoneBook { public GlobalPhoneBook() { addEntry(new PhoneBookEntry(0, "Alan", "Turing", "111111")); addEntry(new PhoneBookEntry(1, "Bill", "Gates", "111222")); addEntry(new PhoneBookEntry(2, "Martin", "Fowler", "654321")); addEntry(new PhoneBookEntry(3, "Kent", "Beck", "999001")); addEntry(new PhoneBookEntry(4, "Howard", "Ship", "554433")); addEntry(new PhoneBookEntry(5, "Linus", "Torvalds", "888777")); } }

Specify an application state object to represent this global phone book. To do that, create hivemodule.xml in src/METAINF:
<?xml version="1.0"?> <module id="com.ttdev.phonebook" version="1.0.0"> <contribution configuration-id="tapestry.state.ApplicationObjects"> <state-object name="phoneBook" scope="application"> <create-instance class="com.ttdev.phonebook.GlobalPhoneBook"/> </state-object> </contribution> </module>

To display the entries, the web designer may modify Home.html like:
<html> <head><title>Phone Book</title></head> <body> <table border="1"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr><td>1</td><td>Britney</td><td>Spears</td><td>376926</td></tr> <tr><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

Home.html looks like this in a browser:

To display the real entries, you will use a For component in Home.html to display each entry:
<html> <head><title>Phone Book</title></head> <body> <table border="1"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr jwcid="eachEntry">

204

Chapter 7 Using the Table Component

<td><span jwcid="id">1</span></td> <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

Home.page is like:
<page-specification class="com.ttdev.phonebook.Home"> <component id="eachEntry" type="For"> For each entry, call setIndex(???) where ??? is the index <binding name="source" value="entries"/> of the entry. For the first entry, the index is 0; for the <binding name="index" value="index"/> second entry, the index is 1 and etc. You'll need to </component> provide this "index" property. <component id="id" type="Insert"> <binding name="value" value="entries[index].id"/> </component> <component id="firstName" type="Insert"> Call getEntries() to get the list, then <binding name="value" value="entries[index].firstName"/> call getIndex() to get the index </component> property, then get the list element at <component id="lastName" type="Insert"> that index (a PhoneBookEntry object). <binding name="value" value="entries[index].lastName"/> OGNL uses the syntax: xxx[i] to </component> lookup the i'th element in a list or an <component id="telNo" type="Insert"> array xxx. Finally call getId() on this <binding name="value" value="entries[index].telNo"/> object. Because you already have this </component> getId() method in PhoneBookEntry, </page-specification> nothing more needs to be done. Provide the "index" Call it to get the list of entries property public abstract class Home extends BasePage { public abstract int getIndex(); @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public List getEntries() { return getPhoneBook().getEntries(); } }

Access the global phone book

Of course, instead of setting the index, you could set the current item instead:
<page-specification ...> <property name="currentEntry"/> <component id="eachEntry" type="For"> <binding name="source" value="entries"/> <binding name="index" value="index"/> <binding name="value" value="currentEntry"/> </component> <component id="id" type="Insert"> <binding name="value" value="entries[index].id"/> <binding name="value" value="currentEntry.id"/> </component> <component id="firstName" type="Insert"> <binding name="value" value="entries[index].firstName"/> <binding name="value" value="currentEntry.firstName"/> </component> <component id="lastName" type="Insert"> <binding name="value" value="entries[index].lastName"/> <binding name="value" value="currentEntry.lastName"/> </component> <component id="telNo" type="Insert"> <binding name="value" value="entries[index].telNo"/> <binding name="value" value="currentEntry.telNo"/> </component> </page-specification>

Or you could set both the index and the current item. Anyway, let's use the index for this application. Now run the application and it should be like: Note that something is wrong. You should have only six people (Linus is the last one), but why you have Elton and David in extra? This is because they are indeed there in Home.html:

Using the Table Component

205

<html> <head><title>Phone Book</title></head> <body> <table border="1"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr jwcid="eachEntry"> <td><span jwcid="id">1</span></td> <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

The first row has been replaced by the rows generated by the For component, but the second and third rows are staying there. To get rid of them, add a special jwcid to them:

206

Chapter 7 Using the Table Component

<html> <head><title>Phone Book</title></head> <body> <table border="1"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr jwcid="eachEntry"> <td><span jwcid="id">1</span></td> <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr jwcid="$remove$"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr jwcid="$remove$"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> When Tapestry reads this HTML Everything is ignored until it sees </html> file, it notes this <tr> element has a the </tr> special jwcid "$remove$". Then it will simply ignore the whole <tr> element including its body.

Now run the application and it should work:

List the entries in alternating colors


Suppose the web designer would like list the entries in alternating colors. So he changes Home.html to look like:

The HTML code is:

Using the Table Component

207

Set the background color of the row (<tr>) to royal blue <html> Defines some styles <head> These styles are called "CSS styles". CSS stands for <title>Phone Book</title> cascading style sheet. <style text="text/css"> Set the background color of the row (<tr>) to tr.odd {background-color: RoyalBlue} green yellow tr.even {background-color: GreenYellow} </style> This style is named "odd" </head> This style is named "even" <body> <table border="1"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd" jwcid="eachEntry"> <td><span jwcid="id">1</span></td> Its background color will be royal blue <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr class="even" jwcid="$remove$"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd" jwcid="$remove$"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html> Their background colors will be royal blue and green yellow respectively when the file is viewed in DreamWeaver. But they will be removed at runtime.

The style can be applied to a <tr> element only

However, if you run the application now, all the rows will use the "odd" style and appear in royal blue:

This is because only the first <tr> is used to generate the entries. The "class" attribute is treated as an informal parameter and thus added to each <tr> element generated by the For component. To alternate the style used, the "class" attribute must be changed for each entry. So, modify Home.page:

208

Chapter 7 Using the Table Component

<page-specification class="com.ttdev.phonebook.Home"> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"/> <component id="eachEntry" type="For"> <binding name="source" value="entries"/> EvenOdd is a class coming with <binding name="index" value="index"/> Tapestry. It works like this. <binding name="class" value="beans.evenOdd.next"/> </component> ... </page-specification> The For component knows nothing about The first time set the "class" to "even", such a "class" parameter. It means it is an informal parameter to be added to the the second time set it to "odd" and etc. <tr> generated.

EvenOdd
even: true The flag is negated in the process

EvenOdd
even: false The flag is negated in the process

EvenOdd
even: true

1: Call next()

2: Return a string "even"

3: Call next()

4: Return a string "odd"

5: Call next()

6: Return a string "even"

Now, run the application and it will be like:

This is very close to what the web designer wants except that the colors are green => blue => green, not blue => green => blue. This is because the EvenOdd bean starts from "even", not "odd". To make it start from "odd", you can initialize its "even" property to false in Home.page:
<page-specification ...> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"> <set name="even" value="false"/> </bean> ... </page-specification>

Now run the application and it should work fine:

Using the Table Component

209

Storing the styles in a file


Suppose that the web designer needs to use the same colors in some other pages. So, he'd like to extract the styles and put them into a file. Suppose that he creates a new folder c:\workspace\PhoneBook\context\css and create a file PhoneBook.css there to hold the styles:
tr.odd {background-color: RoyalBlue} tr.even {background-color: GreenYellow}

Such a file is called a "style sheet". Then you need to "link" this style sheet to Home.html:
<html> <head> <title>Phone Book</title> <style text="text/css"> tr.odd {background-color: RoyalBlue} tr.even {background-color: GreenYellow} </style> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> <table border="1"> Load the style sheet from the file ../css/PhoneBook.css. <tr class="odd" jwcid="eachEntry"> You need to use ".." to go back one level to the context ... folder because the Home.html is in WEB-INF and then </tr> go into the css folder. <tr class="even" jwcid="$remove$">...</tr> <tr class="odd" jwcid="$remove$">...</tr> </table> </body> </html>

If you view Home.html using a browser, you should see that the styles are indeed working:

210

Chapter 7 Using the Table Component

However, it will not work when the application is run because the HTML code is generated dynamically. So this chunk of HTML code has no concept of "its location". It is not in WEB-INF (although Home.html is there); it is just some HTML code generated on the fly. Therefore a relative path in this chunk of code makes no sense at all. The situation is just like that of a <img>. Using a relative path makes no sense. If you use an absolute path such as:
... <link rel="stylesheet" text="text/css" href="/PhoneBook/css/PhoneBook.css"/> ...

Then you will be using the context path which varies from one deployment to another. To solve this problem, you will use the same solution, i.e., use an asset to represent the path to that css file. To generate such a <link> HTML element, it's convenient to use the Shell component:
It is a context asset, so this path is relative to the context path. Home.page <page-specification ...> <asset name="phoneBook" path="context:/css/PhoneBook.css"/> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"> <set name="even" value="false"/> </bean> ... </page-specification>

You have an (implicit) Shell component here. You could use a declared component and it would not make any difference. It will generate the HTML code:

Use this asset

Home.html <html jwcid="@Shell" title="Phone Book" stylesheet="asset:phoneBook"> <head> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> The "stylesheet" parameter is <body> The Shell component will generate used to generate the <link ... this href="...">. </body> </html> The "title" parameter is required (you must bind it) <html> <head> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="/PhoneBook/css/PhoneBook.css"/> </head> ... ... ... </html>

You can see that the Shell component will generate the standard boilerplate HTML code that each HTML page should have: <html><head><title>...</title></head>...</html>. Because this code is the outer part of the page (the "shell"),

Using the Table Component

211

while the real meat is in the <body> element, this component is called the "Shell" component. Who will fill in the real meat? Actually, after generating the <html> and <head> elements, the Shell component will also render its body as the real meat:
Home.html <html jwcid="@Shell" title="Phone Book" stylesheet="asset:phoneBook"> <head> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> ... </body> </html>

<html> <head> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="/PhoneBook/css/PhoneBook.css"/> </head> <head> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> ... </body> </html>

Obviously this is a little problem here because there is an extra <head> element. This extra <head> element is your static <head> element. To get rid of it, mark it as "$remove$":
<html jwcid="@Shell" title="Phone Book" stylesheet="asset:phoneBook"> <head jwcid="$remove$"> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> ... </body> </html>

Now run application and you should have only one <head> element (the one generated by the Shell component):

212

Chapter 7 Using the Table Component

If you had two or even more style sheets, you could do it this way:
Call getAssets() to get the Map of assets. Then lookup the one named "phoneBook". <html jwcid="@Shell" title="Phone Book" stylesheets="ognl:{assets.phoneBook, assets.foo, assets.bar}> ... </html> Note the "s" at {...} creates an array in OGNL. the end In this sample case there are three elements in the array. Use an OGNL expression

Sorting the entries


Suppose that you'd like to allow the user to sort the entries by the id, first name, last name. For example, the entries below are sorted by their first names:

Using the Table Component

213

Let's do it. Create PhoneBook.application:


<?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 name="PhoneBook"> <library id="Contrib" specification-path="/org/apache/tapestry/contrib/Contrib.library"/> </application>

Why list this library here? You will need to use a component in this library. Next, modify Home.html:
<html jwcid="@Shell" title="Phone Book" stylesheet="asset:phoneBook"> <head jwcid="$remove$"> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd" jwcid="eachEntry"> <td><span jwcid="id">1</span></td> <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr class="even" jwcid="$remove$"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd" jwcid="$remove$"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

Now the whole table is a component. Define the component in Home.page:


<page-specification ...> ... <component id="table" type="Contrib:Table"> </component> </page-specification>

It is a Table component provided by the "Contrib" library. Unlike components such as Insert and For, the Table component is not built-into Tapestry. It was contributed by other people (a Tapestry expert named Mind Bridge). Components like this are put into the tapestry-contrib.jar file that is distributed along with Tapestry. You should already have a copy of it in c:\tomcat\shared\lib:

214

Chapter 7 Using the Table Component

To use this Table component, you must specify some parameters:


The Table component will call getEntries() to get the list of rows (it can be a Java List, a Java Collection, an array or an Iterator). There are four columns. Each column has a unique id.

<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName, lastName, telNo"/> </component> </page-specification> Use the column id as the column title Suppose these are the entries {entry1, entry2, entry3, ...} Call getId() on entry1 Call getId() on entry2 Call getId() on entry3 id firstName lastName

telNo

getId() getFirstName() getLastName() getTelNo() getId() getFirstName() getLastName() getTelNo() getId() getFirstName() getLastName() getTelNo() ... ... ... ...

As shown in the diagram above, the "source" parameter provides the list of entries. The "columns" parameter specifies the ids of the columns. The column ids are used as column titles in the HTML table. Each entry will generate a row (<tr>) in the table. For example, for the first entry, entry1, the Table component will call getId() on it to get the cell value for the first column. It will call getFirstName() on it to get the cell value for the second column and etc. As the Table component will loop through each row and output the values, you no longer need to use a For component and Insert components to output the data:

Using the Table Component

215

<html ...> ... <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd" jwcid="eachEntry"> <td><span jwcid="id">1</span></td> <td><span jwcid="firstName">Britney</span></td> <td><span jwcid="lastName">Spears</span></td> <td><span jwcid="telNo">376926</span></td> </tr> <tr class="even" jwcid="$remove$"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd" jwcid="$remove$"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

They are here just to make the web designer happy. When Table component renders, it will discard its body entirely. So, they will all be discarded.

Accordingly, delete the For component and the Insert components in Home.page:
<page-specification ...> ... <component id="eachEntry" type="For"> Don't need the For <binding name="source" value="entries"/> and the Insert <binding name="index" value="index"/> components <binding name="class" value="beans.evenOdd.next"/> </component> <component id="id" type="Insert"> <binding name="value" value="entries[index].id"/> </component> <component id="firstName" type="Insert"> <binding name="value" value="entries[index].firstName"/> </component> <component id="lastName" type="Insert"> <binding name="value" value="entries[index].lastName"/> </component> <component id="telNo" type="Insert"> <binding name="value" value="entries[index].telNo"/> </component> <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName, lastName, telNo"/> </component> </page-specification>

You don't need the "index" property anymore:


public abstract class Home extends BasePage { public abstract int getIndex(); @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public List getEntries() { return getPhoneBook().getEntries(); } }

Now run the application and it will look like this:

216

Chapter 7 Using the Table Component

The styles are not working yet, but let's ignore it for the moment. If you click on say the "firstName" link, it will sort the entries according to the first names:

Note that there is a small triangle next to the "firstName" link. It means that the sorting is ascending (from A to Z). If you click on the "firstName" link again, the triangle will turn upside down and the sorting will be descending (from Z to A):

Similarly, you can click on another link to sort on that column.

Using the Table Component

217

Customizing how to get the cell value


At the moment, for the "id" column, the Table component will call getId() to get the cell value. For the "firstName" column, it will call getFirstName(). However, this way of handling may not always work for you. For example, if the PhoneBookEntry class is like this:
public class PhoneBookEntry { private int id; private Name name; private String telNo; public PhoneBookEntry(int id, Name name, String telNo) { this.id = id; this.name = name; this.telNo = telNo; } public Name getName() { return name; } public int getId() { return id; } public String getTelNo() { return telNo; } }

The Name class is like this:


public class Name { private String firstName; private String lastName; public Name(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } }

The Global class must be changed accordingly:


public class GlobalPhoneBook extends PhoneBook { public GlobalPhoneBook() { addEntry(new PhoneBookEntry(0, new Name("Alan", "Turing"), "111111")); addEntry(new PhoneBookEntry(1, new Name("Bill", "Gates"), "111222")); addEntry(new PhoneBookEntry(2, new Name("Martin", "Fowler"), "654321")); addEntry(new PhoneBookEntry(3, new Name("Kent", "Beck"), "999001")); addEntry(new PhoneBookEntry(4, new Name("Howard", "Ship"), "554433")); addEntry(new PhoneBookEntry(5, new Name("Linus", "Torvalds"), "888777")); } }

Then there is a problem because the PhoneBookEntry class doesn't have a getFirstName() method nor a getLastName () method anymore. Of course, you could create two such methods to make it work, but let's do it in another way. Modify Home.page: In summary:

218

Chapter 7 Using the Table Component

The whole thing is called a "column definition" <page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:name.firstName, lastName:name.lastName, telNo"/> </component> </page-specification> This is still the id The Table component will interpret this of the column as an OGNL expression to return the cell value. In this case, it will call getName() on the row object (a PhoneBookEntry) and get a Name object, then call getFirstName() on that Name object.

Column definition XXX XXX is the column id.

Meaning

XXX:YYY XXX is the column id. YYY is the OGNL expression. Now run the application and it should continue to work.

Customizing the column titles


At the moment the column titles don't look so good. For example, instead of "firstName", you'd like to display "First name". To do that, specify the column title in the column definition in Home.page:
Column definition <page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> </component> </page-specification> OGNL expression Column title for cell value Column id

So, in summary: Form XXX XXX:YYY XXX is the column id. XXX is the column id. YYY is the OGNL expression. Meaning

XXX:YYY:ZZZ XXX is the column id. YYY is the column title. ZZZ is the OGNL expression. Now run the application and the title should change to "First name":

Using the Table Component

219

What if the PhoneBookEntry does have a getFirstName() method?


public class PhoneBookEntry { private int id; private Name name; private String telNo; public PhoneBookEntry(int id, Name name, String telNo) { this.id = id; this.name = name; this.telNo = telNo; } public Name getName() { return name; } public String getFirstName() { return getName().getFirstName(); } public int getId() { return id; } public String getTelNo() { return telNo; } }

In principle, you don't need to specify an OGNL expression anymore. So, can you write the column definition as "id:title" like below?
<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> </component> </page-specification>

However, this will not work because if it is in the form of XXX:YYY, then the Table component will interpret XXX as the id (no surprise here) and interpret YYY as the OGNL expression, not as the title. It means that to specify the title, you must write something like XXX:YYY:ZZZ, then it will interpret YYY as the title:
<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:firstName, lastName:name.lastName, telNo"/> </component> </page-specification>

What if you need to support both English and Chinese? Then you should get the column title from properties files. This is easy. Just create Home.properties and Home_zh_TW.properties:

220

Chapter 7 Using the Table Component

Home.properties firstName=First name

Home_zh_TW.properties

firstName= Note that you're using the column id as the key of the message. When the Table component can find a message with the column id as the key, it will ignore the column title in the column definition. So, you can set the column title in the column definition to anything. For example, you could set it to empty:
<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:firstName, lastName:name.lastName, telNo"/> </component> </page-specification>

or not specify it at all:


<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:firstName, lastName:name.lastName, telNo"/> </component> </page-specification>

Making the styles work again


Now you'd like make the styles work again. Just modify Home.page:
<page-specification class="com.ttdev.phonebook.Home"> <asset name="phoneBook" path="context:/css/PhoneBook.css"/> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"> <set name="even" value="false"/> </bean> <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> </component> </page-specification> When the Table component generates each <tr> element, it will evaluate this parameter. In this case it will get a string "odd" for the first row and get "even" for the second row and etc. Then it will set the "class" atrtibute of the <tr> to "odd" for the first row and etc.

<tr class="odd">...</tr> <tr class="even">...</tr> <tr class="odd">...</tr> ...

Now, run the application and the styles will work again:

Using the Table Component

221

As the title row doesn't look so good, let's change its background color to pink. First, create a style in PhoneBook.css:
tr.odd {background-color: RoyalBlue} tr.even {background-color: GreenYellow} th.title {background-color: Pink}

Then modify Home.page:


<page-specification class="com.ttdev.phonebook.Home"> <asset name="phoneBook" path="context:/css/PhoneBook.css"/> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"> <set name="even" value="false"/> </bean> <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> </component> This will set the "class" </page-specification> attributes of the column titles

<tr><th class="title">id</th><th class="title">First name</th>...</tr> <tr class="odd">...</tr> <tr class="even">...</tr>

Now run the application and it should work:

222

Chapter 7 Using the Table Component

Making the first name a link


Suppose that you'd like to make the first name a link, so that the user can click on it to view the further details of that person. To do that, let's modify Home.html:
When the Table component needs to render the value for the column whose id is xxx, it will look for a component named "xxxColumnValue". If it exists, it will let the component render the cell value. Only if it is not found, it will call getXxx().

<html ...> ... <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd"> <td>1</td> <td jwcid="firstNameColumnValue"> The Table component will <a href="" jwcid="firstNameLink">Britney</a> render this component as the </td> cell value for the "firstName" <td>Spears</td> column <td>376926</td> </tr> <tr class="even"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

Define the components in Home.page:

Using the Table Component

223

A Block component is very simple. It will simply renders its body (just a <page-specification ...> single DirectLink in this case) when it ... is "forced" to, such as when the Table <component id="table" type="Contrib:Table"> component forces it to render. ... </component> <component id="firstNameColumnValue" type="Block"/> <component id="firstNameLink" type="DirectLink"> <binding name="listener" value="listener:onShowDetails"/> <binding name="parameters" value="components.table.tableRow.id"/> </component> </page-specification> Call getTableRow() on the Table Call getComponents() on the page object to component, which will return the current get a Map containing all the components in row object (a PhoneBookEntry). Finally, the page, then lookup the Map for a call getId() on the PhoneBookEntry to component named "table". get its id. This id is the only parameter to pass to the listener. public abstract class Home extends BasePage { @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public List getEntries() { Retrieve the id of the phone return getPhoneBook().getEntries(); book entry from the } DirectLink public void onShowDetails(int entryId) { System.out.println("Showing details for " + entryId); } }

If you run the application now, you'll see that the links are working but the text is all "Britney":

To display the correct first name, you can use an Insert component inside the DirectLink:
<html jwcid="@Shell" title="Phone Book" stylesheet="asset:phoneBook"> <head jwcid="$remove$"> <title>Phone Book</title> <link rel="stylesheet" text="text/css" href="../css/PhoneBook.css"/> </head> <body> <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd"> <td>1</td> <td jwcid="firstNameColumnValue"> <a href="" jwcid="firstNameLink"><span jwcid="firstName">Britney</span></a> </td> <td>Spears</td>

224

Chapter 7 Using the Table Component

<td>376926</td> </tr> <tr class="even"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> </body> </html>

Define it in Home.page:
<page-specification ...> ... <component id="firstNameColumnValue" type="Block"/> <component id="firstNameLink" type="DirectLink"> <binding name="listener" value="listener:onShowDetails"/> <binding name="parameters" value="components.table.tableRow.id"/> </component> <component id="firstName" type="Insert"> <binding name="value" value="components.table.tableRow.firstName"/> </component> </page-specification> Get the current row from the table and then get the first name

Provide the getFirstName() method in PhoneBookEntry:


public class PhoneBookEntry { private int id; private Name name; private String telNo; public PhoneBookEntry(int id, Name name, String telNo) { this.id = id; this.name = name; this.telNo = telNo; } public Name getName() { return name; } public int getId() { return id; } public String getTelNo() { return telNo; } public String getFirstName() { return getName().getFirstName(); } }

Now run the application and it should work:

At the moment you're using a <td> as the Block. But what if you move the Block to the end of the HTML file like:
<html ...> ...

Using the Table Component

225

<table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th></tr> <tr class="odd"> <td>1</td> <td>Britney</td> <td>Spears</td> <td>376926</td> </tr> <tr class="even"><td>2</td><td>Elton</td><td>John</td><td>285984</td></tr> <tr class="odd"><td>3</td><td>David</td><td>Letterman</td><td>877357</td></tr> </table> <span jwcid="firstNameColumnValue"> <a href="" jwcid="firstNameLink"><span jwcid="firstName">Britney</span></a> </span> </body> </html>

Let's run the application and you'll see that it will continue to work. In particular, the Block will not render itself at the end of the HTML page. It means that a Block will not render itself unless forced by others. For example, as you have seen above, the Table component will force the Block to render itself.

Listing more entries


At the moment you have only six entries in the phone book. What if you have more? Let's add six more entries:
public class GlobalPhoneBook extends PhoneBook { public GlobalPhoneBook() { addEntry(new PhoneBookEntry(0, new Name("Alan", "Turing"), "111111")); addEntry(new PhoneBookEntry(1, new Name("Bill", "Gates"), "111222")); addEntry(new PhoneBookEntry(2, new Name("Martin", "Fowler"), "654321")); addEntry(new PhoneBookEntry(3, new Name("Kent", "Beck"), "999001")); addEntry(new PhoneBookEntry(4, new Name("Howard", "Ship"), "554433")); addEntry(new PhoneBookEntry(5, new Name("Linus", "Torvalds"), "888777")); addEntry(new PhoneBookEntry(6, new Name("Ward", "Cunningham"), "222222")); addEntry(new PhoneBookEntry(7, new Name("James", "Gosling"), "333333")); addEntry(new PhoneBookEntry(8, new Name("Erich", "Gamma"), "4444444")); addEntry(new PhoneBookEntry(9, new Name("Grady", "Booch"), "5555555")); addEntry(new PhoneBookEntry(10, new Name("Bruce", "Eckel"), "6666666")); addEntry(new PhoneBookEntry(11, new Name("Richard", "Stallman"), "777777")); } }

Now run the application and it will be like:

The entries are displayed in two pages. This is the first page. There are only 10 entries. Click on "2" at the top to see

226

Chapter 7 Using the Table Component

the second page:

You can click on "<<" to go to the first page, "<" to the previous page (from page 2 to page 1), ">" to the next page (none before page 2 is the last page), ">>" to the last page (page 2 itself). Suppose that you'd like to show only three entries per page, you can set a parameter of the Table:
<page-specification class="com.ttdev.phonebook.Home"> <asset name="phoneBook" path="context:/css/PhoneBook.css"/> <bean name="evenOdd" class="org.apache.tapestry.bean.EvenOdd"> <set name="even" value="false"/> </bean> <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> </component> ... </page-specification>

Then it will be like:

Tuning the performance of the Table component


Let's run an experiment. Create a class PhoneBookTable to simulate a database table of phone book entries:

Using the Table Component

227

public class PhoneBookTable { private List entries; public PhoneBookTable() { entries = new ArrayList(); } public void insert(PhoneBookEntry entry) { entries.add(entry); } public ResultSet selectAll() { System.out.println("Selecting all"); return new ResultSet(entries); } } This is not the ResultSet in JDBC. For simplicity, you will create your own ResultSet class to simulate the JDBC ResultSet. public class ResultSet { private List entriesInResultSet; private int currentIdx; public ResultSet(List entriesInResultSet) { this.entriesInResultSet = entriesInResultSet; this.currentIdx = -1; The index of the current record. It is set to } -1 so it is before the first record. public boolean next() { Just like the JDBC ResultSet, it has if (currentIdx < entriesInResultSet.size() - 1) { a next() method that moves the currentIdx++; current record to the next position. return true; } else { return false; } } public PhoneBookEntry readEntry() { PhoneBookEntry entry = (PhoneBookEntry) entriesInResultSet.get(currentIdx); System.out.println("Reading entry with id " + entry.getId()); return entry; } } The JDBC ResultSet has methods like readInt() and readString() to read each field of the current record. For simplicity, you will have a readEntry() that can load a PhoneBookEntry from the record. If you were really using a database, you would get a JDBC ResultSet and call readInt() and readString() to initialize the fields of the PhoneBookEntry. Print the id of the entry that is being read from the "database"

Insert a record (i.e., a phone book entry) into the table. If you were really using a database, you would issue an "insert into table" SQL statement. It simulates the effect of a "select * from table" statement that has no condition Print a message to show that it is selecting all the entries The ResultSet will iterate on this list and return the entries one by one

Now, modify the PhoneBook class so that it uses a PhoneBookTable to access the entries:

228

Chapter 7 Using the Table Component

public class PhoneBook { private List entries; private PhoneBookTable table;

public PhoneBook() { entries = new ArrayList(); One must pass a PhoneBookTable to its } constructor as a parameter. public PhoneBook(PhoneBookTable table) { this.table = table; } public void addEntry(PhoneBookEntry entry) { Because it no longer stores the entries, it doesn't entries.add(entry); need an addEntry() method anymore. Instead, } people should use the insert() method of public List getEntries() { PhoneBookTable. List result = new ArrayList(); Call the selectAll() method on the "database" table ResultSet rs = table.selectAll(); to get a ResultSet. Then it iterates through the while (rs.next()) { ResultSet and read each entry. Yes, it looks as if it result.add(rs.readEntry()); was really accessing a database. } return result; return entries; } }

As the entries are stored in the PhoneBookTable, PhoneBook no longer needs to store the entries itself. Instead, it will use a PhoneBookTable to do that.

Now, your Global class should add the hard coded entries into the PhoneBookTable instead of the PhoneBook:
public class GlobalPhoneBook extends PhoneBook { public GlobalPhoneBook() { super(makePhoneBookTable()); } private static PhoneBookTable makePhoneBookTable() { PhoneBookTable phoneBookTable = new PhoneBookTable(); phoneBookTable.insert(new PhoneBookEntry(0, new Name("Alan", "Turing"), "111111")); phoneBookTable.insert(new PhoneBookEntry(1, new Name("Bill", "Gates"), "111222")); phoneBookTable.insert(new PhoneBookEntry(2, new Name("Martin", "Fowler"), "654321")); phoneBookTable.insert(new PhoneBookEntry(3, new Name("Kent", "Beck"), "999001")); phoneBookTable.insert(new PhoneBookEntry(4, new Name("Howard", "Ship"), "554433")); phoneBookTable.insert(new PhoneBookEntry(5, new Name("Linus", "Torvalds"), "888777")); phoneBookTable.insert(new PhoneBookEntry(6, new Name("Ward", "Cunningham"), "222222")); phoneBookTable.insert(new PhoneBookEntry(7, new Name("James", "Gosling"), "333333")); phoneBookTable.insert(new PhoneBookEntry(8, new Name("Erich", "Gamma"), "4444444")); phoneBookTable.insert(new PhoneBookEntry(9, new Name("Grady", "Booch"), "5555555")); phoneBookTable.insert(new PhoneBookEntry(10, new Name("Bruce", "Eckel"), "6666666")); phoneBookTable.insert(new PhoneBookEntry(11, new Name("Richard", "Stallman"), "777777")); return phoneBookTable; } }

You insert the records into the table. Then you pass the table into the PhoneBook for its use. Now, run the application again and you should see: Browser Console
Selecting all Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry with with with with with with with with with with with with id id id id id id id id id id id id 0 1 2 3 4 5 6 7 8 9 10 11

It means that even if only three entries are displayed, but all the entries are read from the "database". Why? Let's review the whole process. For example, when you click on a particular page such as "2", the Table component will need to find out the entries on that page. It does that by evaluating its "source" parameter. This will call getEntries() on

Using the Table Component

229

the Home object:


<page-specification class="com.ttdev.phonebook.Home"> <component id="table" type="Contrib:Table"> <binding name="source" value="entries"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> </component> ... </page-specification>

The Home object will in turn call getEntries() on the PhoneBook:


public abstract class Home extends BasePage { public List getEntries() { return getPhoneBook().getEntries(); } ... }

The PhoneBook object will issue a "select * from table" to the "database" table and read all the entries one by one:
public class PhoneBook { ... public List getEntries() { List result = new ArrayList(); ResultSet rs = table.selectAll(); while (rs.next()) { result.add(rs.readEntry()); } return result; } }

Finally the whole list (containing entries 0-11) is returned to the Table component. Then the Table component will just pick entries 3-5 from the list and display them. If the entries should be sorted on say the first name, the Table component will sort the list before it picks the entries. In summary:
1: Give me the source 2: Give me all the entries 3: select * from table

Table
6: List

Home
5: List

PhoneBook
4: Entries 0-11

DB

7: Sort the List if required

This is a problem if there are a thousand entries but only three entries are displayed. It is worse if the database server and Tomcat are running on two computers because a thousand entries will have to be transferred over the network. The root cause of the problem is here:
It is asking for all the entries

1: Give me the source

2: Give me all the entries

3: select * from table

Table
6: List

Home
5: List

PhoneBook
4: Entries 0-11

DB

7: Sort the List if required

To solve this problem, the Table component must not ask for all the entries. Instead, it should only ask for the entries for the current page. To implement this idea, you can tell the Table component to work with an IBasicTableModel object. It works like this: That is, the Table component just asks for entries 3-5. If the entries should be sorted on say the first name, the Table component can't ask for entries 3-5 and then sort them. It needs to ask the IBasicTableModel to sort all the entries first and then return entries 3-5:

230

Chapter 7 Using the Table Component

Table Table

1: Give me me entries entries 3-5 sorted on the first name IBasicTable

2: A short List 3: A short List

IBasicTable Model Model

2: Sort the entries

To implement this idea, modify Home.page:


<page-specification ...> <component id="table" type="Contrib:Table"> <binding name="source" value="entries model"/> ... </component> ... </page-specification>

Define getModel() in Home.java:


public abstract class Home extends BasePage { @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public List getEntries() { return getPhoneBook().getEntries(); } public IBasicTableModel getModel() { Return an IBasicTableModel object return new IBasicTableModel() { public int getRowCount() { return getPhoneBook().getNoEntries(); Return the total number of entries. } The Table component needs to public Iterator getCurrentPageRows( calculate how many pages there are. int nFirst, int nPageSize, Return the rows (entries) for the ITableColumn objSortColumn, current page boolean bSortOrder) { //return just some entries Ascending or descending? } There are constants defined }; for it: } The index of the first ... How many It is column to entry on the current } sort on. If no interface ITableSortingState { page. For page 2, this entries in this page. It should sorting is public boolean SORT_DESCENDING = true; index should be 3. be 3 in this needed, it is public boolean SORT_ASCENDING = false; case. null. }

As you don't have such a getNoEntries() method in PhoneBook yet, define it:

Using the Table Component

231

public class PhoneBook { private PhoneBookTable table; ... public int getNoEntries() { return table.selectCount(); } Simulate a select count(*) from table } public class PhoneBookTable { private List entries; ... public int selectCount() { return entries.size(); } }

Next, implement the getCurrentPageRows() method in the anonymous class:


public abstract class Home extends BasePage { @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public IBasicTableModel getModel() { return new IBasicTableModel() { public int getRowCount() { return getPhoneBook().getNoEntries(); } public Iterator getCurrentPageRows( int nFirst, Find out the id of the sort column. What you int nPageSize, have is the column object, not the column id. ITableColumn objSortColumn, Just Basically, you can just call getColumnName() boolean bSortOrder) { pass to get the id. However, if the column object is String sortColumnId = objSortColumn == null them null (no sorting), you can't call any method on it. ? null along In that case, you just set the column id to null. : objSortColumn.getColumnName(); return getPhoneBook().getSomeEntries( nFirst, Id of the column to sort on nPageSize, (may be null) sortColumnId, bSortOrder == ITableSortingState.SORT_ASCENDING).iterator(); } }; True if ascending. False if Get some entries as a List As the Table component wants } descending. ... an Iterator from the } IBasicTableModel, so you call iterator() on the List and return that iterator. public class PhoneBook { private PhoneBookTable table; ... Simulate a "select * from table public List getSomeEntries( limit xxx offset yyy" statement int startIdx, int noEntries, String sortColumnId, boolean isAscending) { ResultSet rs = table.selectLimit(startIdx, noEntries, sortColumnId, isAscending); List result = new ArrayList(); while (rs.next()) { Load the entries in the result.add(rs.readEntry()); ResultSet into a List } return result; } }

In the above diagram, the most important point is the call to selectLimit(). To implement this method with a real database, you will issue a SQL statement like "select * from table limit 3 offset 3". "limit 3" tells the database server it to return at most 3 records. "offset 3" tells the database server to start from the 3rd record (and ignore the previous ones). Therefore, it will return the 3rd - 5th records. Note that this "limit" keyword and "offset" keyword are not standard SQL keywords. Your database server may or may not support them or may support them using a different syntax (e.g., using the "top" keyword). If you need to sort on say the first name, you can issue a SQL statement like "select * from table order by firstName limit 3 offset 3". As you're not dealing a real database, you can implement selectLimit() like this:

232

Chapter 7 Using the Table Component

public class PhoneBookTable { private List entries; ... public ResultSet selectLimit( int startIdx, Sort the list Print a message int noRecords, String columnName, Make a copy of the list of the boolean isAscending) { entries because you will modify System.out.println("Selecting with limit"); the list List sortedEntries = new ArrayList(entries); if (columnName != null) { Need to sort? Collections.sort(sortedEntries, new PhoneBookEntryComparator(columnName)); if (!isAscending) { You need to provide a Comparator so that the Collections.reverse(sortedEntries); sort() method can use it to compare two phone } book entries to determine their ordering. } List subList = sortedEntries.subList(startIdx, startIdx + noRecords); return new ResultSet(subList); } } Get a sublist of the List. The start index It is the end index. It is exclusive. and the end index are specified. Reverse the List if descending Must implement the interface Comparator public class PhoneBookEntryComparator implements Comparator { private String columnName; Compare on this column public PhoneBookEntryComparator(String columnName) { Must implement a compare() method. The sort() method will call this method and pass two this.columnName = columnName; PhoneBookEntry objects to it. This method must } check which one is smaller (the smaller entry will be public int compare(Object obj1, Object obj2) { put in the front). If the first one is smaller, it should PhoneBookEntry entry1 = (PhoneBookEntry) obj1; return a negative integer. If the first one is larger, it PhoneBookEntry entry2 = (PhoneBookEntry) obj2; should return a positive integer. If they are equal, it if (columnName.equals("id")) { should return zero. return entry1.getId()-entry2.getId(); } else if (columnName.equals("firstName")) { return entry1.getFirstName().compareTo(entry2.getFirstName()); } else { //Similar for other columns. } } If to sort on the "id" column, you will compare the id's If to sort on the "firstName" } of the two entries. You can just subtract the id of the column, you will compare second entry from that of the first entry and return the the first names of the two value. entries. Get the first name of the first entry and call its compareTo() method and pass the first name of the second entry. This compareTo() method is provided by the String class in Java. It will compare two strings.

Now run the application and you should see:

Using the Table Component

233

Browser

Console
Selecting with limit Reading entry with id 0 Reading entry with id 1 Reading entry with id 2

Note that only 3 entries are loaded from the "database". Click on "First name" to sort on it, then you should see: Browser Console
Selecting with limit Reading entry with id 0 Reading entry with id 1 Reading entry with id 10

It is working. Still only 3 entries are loaded. Click on "First name" again to change to descending: Browser Console
Selecting with limit Reading entry with id 6 Reading entry with id 11 Reading entry with id 2

It works. Still only 3 entries are loaded.

234

Chapter 7 Using the Table Component

Session is used
Let's do an experiment. Run the application. Sort it on the first name and go to the third page:

Enter the URL http://localhost:8080/PhoneBook/app in the browser and hit Enter. You'll see that the third page is displayed and the sorting is still done on the first name. It means somehow the application is remembering which page is the current page (called "paging state") and whether sorting is done or not, and if done, on which column, ascending or descending (called "sorting state"). How can it remember these states? It saves them into the session (as persistent properties). It means that when you use the Table component, you will use a session and the performance may be affected.

Caching the entries


Now you have improved the performance. If the user views a page, only the entries on that page are loaded from the database. However, to view a page, you still need to issue an SQL statement. This is quite a slow operation. One idea is to load all the entries at the beginning and cache them into the session. Then to view another page, you can just get the entries from the session without accessing the database at all. Of course the application should run faster. The cost is that it will consume a lot of memory. To implement this idea, let's modify Home.java:
public abstract class Home extends BasePage { @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); public List getSource() { return getPhoneBook().getEntries(); } public IBasicTableModel getModel() { ... } ... }

You're getting the back the original getSource() method that return a Java List containing all the entries. Next, modify Home.page:
<page-specification ...> <component id="table" type="Contrib:Table"> <binding name="source" value="model source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> </component> ... </page-specification>

Now, the most important part. To save the List into the session, add a binding to the Table component:

Using the Table Component

235

<page-specification ...> ... <component id="table" type="Contrib:Table"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> </page-specification> A FullTableSessionStateManager. This class is provided by the author of the Table component. It will tell the Table component that everything should be saved (that's why it's called "Full") including the paging state, sorting state and the rows.

What is a session state manager? The Table component will ask it what to save in the session. If you don't specify a session state manager, by default it will save just the paging state and sorting state into the session.

Now close the browser to get rid of the session and then run the application again. You should see: Browser Console
Selecting all Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry with with with with with with with with with with with with id id id id id id id id id id id id 0 1 2 3 4 5 6 7 8 9 10 11

Click on "2" and you should see: Browser Console


Selecting all Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry Reading entry with with with with with with with with with with with with id id id id id id id id id id id id 0 1 2 3 4 5 6 7 8 9 10 11

Why it still loads the entries? This is due to an implementation detail. But it doesn't really matter. Click on "3" and you'll see that it no longer loads the entries:

236

Chapter 7 Using the Table Component

Browser
<no output>

Console

Click on "4", "2" or "First name" or whatever. You will see that it will not load anything from the "database". However, there is a still a minor problem here. Let's stop Tomcat and you'll something like this in the console:
INFO: Cannot serialize session attribute PhoneBook/Home/table.tableView/sessionState for session 1BF75B628E3683A953753B3DB64FA296 java.io.NotSerializableException: com.ttdev.phonebook.PhoneBookEntry at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1054) at java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1224) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1050)

This is because it is trying to save the list of entries into the session but your PhoneBookEntry is not serializable. To fix it, let it implement Serializable:
public class PhoneBookEntry implements Serializable { private static final long serialVersionUID = 639860023393294698L; private int id; private Name name; private String telNo; ... }

This is not enough. As it contains a Name object, the Name class must also implement Serializable:
public class Name private static private String private String ... } implements Serializable { final long serialVersionUID = -6056490677438712843L; firstName; lastName;

Now, start Tomcat, use the application and then stop Tomcat again. The above error will not occur again.

Adding a delete button


Suppose that you'd like to add a "Delete" button to each entry like:

Using the Table Component

237

To do that, let's add a new column to the Table component in Home.page:


<page-specification ...> <component id="table" type="Contrib:Table"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> ... </page-specification>

How to render it in the cell? You will render it as a form with a delete button. So, you need to use a Block to render it:
<page-specification ...> <component id="table" type="Contrib:Table"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> <component id="deleteColumnValue" type="Block"/> <component id="deleteForm" type="Form"> <binding name="listener" value="listener:onDelete"/> </component> ... </page-specification>

As the column id is "delete", the Block must be called "deleteColumnValue". Of course, you must define the Block in Home.html:
<html ...> ... <table border="1" jwcid="table"> ... <tr class="odd"> <td>1</td> <td>Britney</td> <td>Spears</td> <td>376926</td> <td jwcid="deleteColumnValue"> <form jwcid="deleteForm"> <input type="submit" value="Delete"/> </form> </td> </tr> ... </table>

238

Chapter 7 Using the Table Component

... </html>

You can place the Block any where in the file. It just doesn't matter. However, it is good to just put it in the table cell so that it can be previewed by the web designer. Define an empty listener in Home.java first:
public abstract class Home extends BasePage { ... public void onDelete() { } }

Now, if the web designer previews Home.html, it will be like:

This is no good. The column title is missing and there is delete button on the second and third row. This is OK. Just add them:
<html ...> ... <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th><th>Delete</th></tr> <tr class="odd"> <td>1</td> <td>Britney</td> <td>Spears</td> <td>376926</td> <td jwcid="deleteColumnValue"> <form jwcid="deleteForm"> <input type="submit" value="Delete"/> </form> </td> </tr> <tr class="even"><td>2</td><td>Elton</td><td>John</td><td>285984</td><td>Delete</td></tr> <tr class="odd"><td>3</td><td>David</td><td>Letterman</td><td>877357</td><td>Delete</td></tr> </table> ... </html>

Then it should look better:

Using the Table Component

239

Finally, you are ready to implement the delete listener:


public abstract class Home extends BasePage { ... public void onDelete() { } }

But what should you do here? As there are several Delete buttons on a page, how do you know which Delete button was clicked on? To solve this problem, you could put a hidden form field to the form to store the entry id, but there is an easier way:
public abstract class Home extends BasePage { ... Get access to the component in @InjectComponent("table") this page named "table". You public abstract Table getTable(); don't have to use annotations. To use plain Java code: public void onDelete() { int id = ((PhoneBookEntry) getTable().getTableRow()).getId(); System.out.println("Deleting " + id); } } In theory, when the form is submitted, the Table component should render all the rows one by one in rewind mode. So you can just get the current row from the table to find out the phone entry id.

public abstract class Home extends BasePage { ... public void onDelete() { int id = ((PhoneBookEntry)((Table)getComponent("table")).getTableRow()).getId(); System.out.println("Deleting " + id); } }

If you click on the Delete button on the second row, when the first row is rendered, the Form component will not call its listener. When the second row is rendered, it will call its listener. Now, let's run the application:

240

Chapter 7 Using the Table Component

Click on the second Delete button. Unfortunately, it throws an exception:

If you use a debugger to check, you will find that getTableRow() is returning a null:
public abstract class Home extends BasePage { ... public void onDelete() { int id = ((PhoneBookEntry) getTable().getTableRow()).getId(); System.out.println("Deleting " + id); } }

Why? This is because when you click on the second Delete button, the form is submitted. But, only the form is rewound (The form will in turn rewind the components in it). It means for the components outside the form such as the Table component, they will not be rewound at all. To solve this problem, you need to make sure store the entry id into a hidden field in each form. In Home.html:

Using the Table Component

241

... <table border="1" jwcid="table"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th><th>Delete</th></tr> <tr class="odd"> <td>1</td> <td>Britney</td> <td>Spears</td> <td>376926</td> <td jwcid="deleteColumnValue"> <form jwcid="deleteForm"> <input type="hidden" jwcid="entryId"/> <input type="submit" value="Delete"/> </form> </td> </tr> <tr class="even"><td>2</td><td>Elton</td><td>John</td><td>285984</td><td>Delete</td></tr> <tr class="odd"><td>3</td><td>David</td><td>Letterman</td><td>877357</td><td>Delete</td></tr> </table> ...

In Home.page:
<page-specification class="com.ttdev.phonebook.Home"> ... <component id="deleteForm" type="Form"> <binding name="listener" value="listener:onDelete"/> </component> <component id="entryId" type="Hidden"> <binding name="value" value="entryId"/> </component> </page-specification>

In Home.java:
public abstract class Home extends BasePage { private int entryId; ... @InjectComponent("table") public abstract Table getTable(); public int getEntryId() { return ((PhoneBookEntry) getTable().getTableRow()).getId(); } public void setEntryId(int entryId) { this.entryId = entryId; } public void onDelete() { System.out.println("Deleting " + entryId); } }

Now run the application and click on the second Delete button. You should see a message like "Deleting 1" in the console. Next, write the code to actually delete the entry:

242

Chapter 7 Using the Table Component

public class Home extends BasePage { @InjectState("phoneBook") public abstract PhoneBook getPhoneBook(); ... public void onDelete(IRequestCycle cycle) { getPhoneBook().deleteEntry(entryId); } } public class PhoneBook { private PhoneBookTable table; public void deleteEntry(int id) { table.delete(id); } If you were using a real database, you would issue a "delete ... from table where id=???" SQL statement. Here, you will just } loop through the list to delete the entry. public class PhoneBookTable { private List entries; public void delete(int id) { for (Iterator iter = entries.iterator(); iter.hasNext();) { PhoneBookEntry entry = (PhoneBookEntry) iter.next(); if (entry.getId()==id) { iter.remove(); return; } } } ... }

Now run the application and try to delete the second entry: Before delete After delete

Nothing seems to have happened! You have indeed deleted the entry from the list, but as the list is cached in the session, the old entries are displayed. To verify that, comment out the session state manager binding in Home.page:
<component id="table" type="Contrib:Table"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <!--<binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/>-> </component>

Using the Table Component

243

Close the browser to get rid of the session. Then restart the application to get back that entry. Now, delete it again and it should work: Before delete After delete

But what if you do want to cache the list? Let's get back the session state manager:
<page-specification ...> <component id="table" type="Contrib:Table"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> ... </page-specification>

Modify Home.java:
Get the "table model" of the Table component. What is a table model? A table model contains the rows, the paging state and sorting state. By default, the Table component uses a SimpleTableModel as the table model. public abstract class Home extends BasePage { ... public void onDelete(IRequestCycle cycle) { getPhoneBook().deleteEntry(entryId); SimpleTableModel tableModel = ((SimpleTableModel) getTable().getTableModel()); tableModel.setDataModel(new SimpleListTableDataModel(getSource())); tableModel.tableDataChanged(null); getTable().fireObservedStateChange(); } } Get the updated entry list from the "source" parameter and wrap it as a SimpleListTableDataModel. The effect is that the table model will get the rows from the updated In turn, the table model gets the list. rows from a table data model. Tell it to update the cache in the session (the cache is a persistent property of the component that is stored in the session, not in the browser) Internally the table model also caches the rows, so you need to call tableDataChanged() on the table model to tell it to discard its internal cache.

Now, close the browser and restart the application. Run it. and it should work.

244

Chapter 7 Using the Table Component

Sort by Delete?
At the moment what happens if you click on the "delete" title?

It makes no sense to sort according to the Delete button. To fix the problem, modify Home.page:
<page-specification ...> The exclamation mark tells the ... Table component that this column <component id="table" type="Contrib:Table"> is non-sortable <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, !delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager ()"/> </component> </page-specification>

Now, close the browser to get rid of the sorting state in the session and then run the application. It will look like:

Moving the page links to the bottom


Suppose that you'd like to move the page links to the bottom like:

Using the Table Component

245

To do that, you need to understand one thing: A Table component contains a number of components in it:

As shown above, a Table is basically a TableView. Inside the TableView, the page links are generated by a TablePages component. The columns are generated by a TableColumns component. The rows are generated by a TableRows component. The cells are generated by a TableValues component. That is, you can consider a Table defined as:
Generate the page links <span jwcid="@TableView"> The first <tr> element is generated for the <span jwcid="@TablePages"/> column titles. The actual <th> elements are <table> generated by the TableColumns component. <tr><span jwcid="@TableColumns"/></tr> <tr jwcid="@TableRows"><td jwcid="@TableValues"/></tr> </table> </span> The <td> elements are The <tr> elements generated by the TableValues are generated by the component TableRows component

You may wonder what the TableView does. You can consider it as a Table with the only difference that you can put anything inside a TableView in any order (e.g., move the TablePages to the bottom) but a Table is a TableView with the above fixed internal structure. So, for your purpose, you should use a TableView instead of a Table. Modify Home.html:

246

Chapter 7 Using the Table Component

A TableView ... <span jwcid="table"> <table> <tr><span jwcid="@Contrib:TableColumns"/></tr> <tr jwcid="@Contrib:TableRows"><td jwcid="@Contrib:TableValues"/></tr> </table> <span jwcid="@Contrib:TablePages"/> The page links are at the bottom </span> <table border="1" jwcid="table $remove$"> <tr><th>ID</th><th>First name</th><th>Last name</th><th>Tel #</th><th>Delete</th></tr> <tr class="odd"> <td>1</td> The <table> element is for <td>Britney</td> preview only <td>Spears</td> You can't have a <td>376926</td> component in an element <td jwcid="deleteColumnValue"> that is marked as removed. <form jwcid="deleteForm"> So, move it to the end. <input type="hidden" jwcid="entryId"/> <input type="submit" value="Delete"/> </form> </td> </tr> <tr class="even">...</tr> <tr class="odd">...</tr> </table> <span jwcid="deleteColumnValue"> <form jwcid="deleteForm"> <input type="hidden" jwcid="entryId"/> <input type="submit" value="Delete"/> </form> </span> ...

Next, modify Home.page:


<page-specification ...> <component id="table" type="Contrib:TableView"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, !delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> ... </page-specification>

It is no longer a Table; it is now a TableView. There is no need to change the bindings at all. It accepts the same bindings as the Table component (well, basically). Next, update Home.java:
public abstract class Home extends BasePage { ... @InjectComponent("table") public abstract Table TableView getTable(); However, a TableView doesn't have this method!

public int getEntryId() { return ((PhoneBookEntry) getTable().getTableRow()).getId(); } }

This is right. A Table has such a property but a TableView doesn't. However, this property is ultimately provided by the TableRows. So, you can get it from the TableRows. In order to refer to the TableRows, you need to give it an id in Home.html:
<span jwcid="table"> <table> <tr><span jwcid="@Contrib:TableColumns"/></tr> <tr jwcid="rows@Contrib:TableRows"><td jwcid="@Contrib:TableValues"/></tr> </table> <span jwcid="@Contrib:TablePages"/> </span> ...

Modify Home.jave:

Using the Table Component

247

public abstract class Home extends BasePage { ... @InjectComponent("table") public abstract TableView getTable(); @InjectComponent("rows") public abstract TableRows getTableRows(); public int getEntryId() { return ((PhoneBookEntry) getTableRows().getTableRow()).getId(); } }

Note that there are some other places in Home.page that are trying to get the current row from the table. They need to be updated too:
<page-specification class="com.ttdev.phonebook.Home"> ... <component id="firstNameLink" type="DirectLink"> <binding name="listener" value="listener:onShowDetails"/> <binding name="parameters" value="components.rows.tableRow.id"/> </component> <component id="firstName" type="Insert"> <binding name="value" value="components.rows.tableRow.firstName"/> </component> </page-specification>

Now run the application and it will be like:

No borders and no background colors. The latter is due to the fact that the TableView doesn't understand the "rowsClass" and "columnsClass" parameters:
<page-specification ...> <component id="table" type="Contrib:TableView"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, !delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> ... </page-specification>

To get back the borders and apply the styles, edit Home.html: Now you no longer need to the two bindings:

248

Chapter 7 Using the Table Component

Standard HTML attribute and has got nothing to do with Tapestry Informal parameters <span jwcid="table"> <table border="1"> <tr><span jwcid="@Contrib:TableColumns" class="title"/></tr> <tr jwcid="rows@Contrib:TableRows" class="ognl:beans.evenOdd.next"> <td jwcid="@Contrib:TableValues"/> </tr> </table> <span jwcid="@Contrib:TablePages"/> </span> ...
<page-specification ...> <component id="table" type="Contrib:TableView"> <binding name="source" value="source"/> <binding name="columns" value="literal:id, firstName:First name:name.firstName, lastName:name.lastName, telNo, !delete"/> <binding name="rowsClass" value="beans.evenOdd.next"/> <binding name="columnsClass" value="literal:title"/> <binding name="pageSize" value="3"/> <binding name="tableSessionStateManager" value="new org.apache.tapestry.contrib.table.model.common.FullTableSessionStateManager()"/> </component> ... </page-specification>

Now run the application and it should look fine:

If you click on "Delete", it will work.

Summary
To use the Table component, just provide all the entries to it through its "source" parameter. You can control the column title and how to get the cell value (a string) using the column definition. You can also disable sorting on that column if that doesn't make sense. If the cell should not be a simple string, you can provide a Block that is named after the column id. Then you can render whatever you like (e.g., a link or a form) in the cell. By default the Table component will load all the entries from the "source" parameter whenever it needs to display a certain page. To remember the paging state and sorting state, it will store them into the session. To tell it to cache the entries too, use a FullTableSessionStateManager. In order to not to load all the entries, but just those on the current page, you can provide a IBasicTableModel through the "source" parameter. The Table component will tell it which entries it wants and the sorting state and expects it to return the right entries. Most usually you will use a SQL statement like "select ... from table offset xxx limit yyy order by zzz" to retrieve the records from the database. To delete an entry, if caching is enabled, you need to be very careful. A Table is just a TableView with a fixed internal structure: a TablePages, a <table> containing a <tr> with a TableColumns (like a <th>) inside and a TableRows (like a <tr>) with a TableValues (like a <td>) inside. To change the structure, you can use these components directly.

Using the Table Component

249

To list the rows in alternating colors, use the EvenOdd bean and setup two CSS styles. Most likely you should store your CSS styles in a file. In that case, use a context asset to access it and provide the asset to a Shell component. If you have more than one stylesheet files, pass an array to the Shell. OGNL allows you to create an array easily. To remove HTML elements used by the designer only, use the $remove$ id. A Form will only rewind the components in it. If you're using a loop to render a Form multiple times, chances are that you will need to store some information into hidden fields so that you can tell which HTML form is being submitted.

251

Chapter 8
Chapter 8

Handling File Downloads and Uploads

252

Chapter 8 Handling File Downloads and Uploads

What's in this chapter?


In this chapter you'll learn how to allow the user to download and upload files.

Downloading a photo
Suppose that you'd like to develop an online photo album. Suppose that the photos are stored in a database. A user can view or download the photos. For simplicity, let's first create a page that allows the user to download a particular photo, e.g., the photo whose id is 101:

Let's create a Tapestry application named Album and perform the setup as usual (use /Album as its context path). Then modify Home.html:
<html> <a href="" jwcid="download">Download photo #101</a> </html>

Define the component in Home.page. What kind of component it should be? You have learned PageLink and DirectLink. As you'd like to pass 101 as a parameter, PageLink is out because it doesn't accept parameters. So you have to use a DirectLink:
<page-specification class="com.ttdev.album.Home"> <component id="download" type="DirectLink"> <binding name="listener" value="listener:onDownload"/> <binding name="parameters" value="101"/> </component> </page-specification>

101 is passed as a parameter. Define the listener in Home.java:

Handling File Downloads and Uploads

253

public abstract class Home extends BasePage { public HttpServletResponse getResponse() { Get the Http response. But ... how? } The image id from the public void onDownload(int imageId) { direct link byte imageData[] = ImageDB.loadImage(imageId); HttpServletResponse response = getResponse(); response.setContentType("image/jpeg"); response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); try { OutputStream out = response.getOutputStream(); Set the content type to out.write(imageData); image/jpeg so that the } catch (IOException e) { browser knows that the throw new ApplicationRuntimeException(e); data is a JPEG file } } } Write the data to the OutputStream of the HTTP Tell the browser to Tell the browser to response. If there is any error when writing the save it a file named save the data instead data, you will get an IOException. In that case you foo.jpg of displaying it in the will throw an ApplicationRuntimeException window Load the image provided by Tapestry. As it is a RuntimeException, from the you don't need to declare it in the throws clause. database public class ImageDB { public static byte[] loadImage(int imageId) { return new byte[] { 68, 75, 78, 90 }; } } Just return some hard coded data

Eclipse probably can't find the HttpServletResponse class. In that case, just add c:\tomcat\common\lib\servlet-api.jar to the build path of the project. Now, the big question is, how to get access to the HTTP servlet response object? You can do it this way:
This prefix says that the object is a "Hivemind service". This is not a Tapestry service such as the page service or direct service. A Hivemind service is just a global Java object that can perform some work for you. Each Hivemind application has a list of Hivemind services. Inject an object

This part is called the "object locator" when injecting an object. Its meaning depends on the prefix. For the service prefix, it is saying please give me the Hivemind service whose id is tapestry.globals.HttpServletResponse which is the current HTTP response object. This can be found by looking up the list of Hivemind services.

public abstract class Home extends BasePage { ... @InjectObject("service:tapestry.globals.HttpServletResponse") public abstract HttpServletResponse getResponse(); ... The Java class of the service }

The whole thing is called an "object reference"

List of Hivemind services


Id tapestry.globals.HttpServletResponse ... ... ... ... Service

HttpServletResponse

Of course, you don't have to use annotations (@InjectObject) to inject an object. You could achieve the same effect in Home.page:
<page-specification class="com.ttdev.album.Home"> <inject type="object"

254

Chapter 8 Handling File Downloads and Uploads

property="response" object="service:service:tapestry.globals.HttpServletResponse"/> ... </page-specification>

However, if you run the application, it will throw an exception:

It is saying that getOutputStream() has already been called for this response. What does it mean? First, you need to understand that an HTTP request object contains both a OutputStream and a Writer (see the diagram below). If you need to send some bytes as the response to the client (e.g., send a binary file to the client), you ask the HTTP request for its OutputStream and then write the bytes to it. But if you'd like to send some text as the response to the client (e.g., send a web page to the client), you should ask the HTTP request for its Writer and then output the text such as "abc" to the Writer. You need to tell the Writer what encoding to use. Internally the Writer will convert the string to bytes using that encoding and then write them to the OutputStream. For example, if the encoding is ASCII, it will output three bytes 96, 97 and 98, the ASCII codes representing "a", "b" and "c".

HTTP response Writer

1: Please output the string "abc"

HTTP response Writer

OutputStream
1: Please output these bytes xx, yy, zz.

OutputStream
2: Please output the bytes 96, 97 and 98 (these are ASCII codes for "a", "b" and "c"), assuming the writer is using ASCII as the encoding.

Obviously it is unreasonable to ask for the OutputStream and then ask for the Writer later or vice versa. So Java treats this as an error. In fact, to be safe than sorry, it doesn't allow you ask for the OutputStream twice or the Writer twice. So, what's the above error about? It is saying that you're calling getOutputStream twice. Why? When the HTTP request

Handling File Downloads and Uploads

255

is sent to the Tapestry application (see the diagram below), it asks for the direct service. So Tapestry will ask the direct service to handle it. The direct service will first call the listener. Then, it is about to load the response page and ask it to render itself. However, before that, it will ask the HTTP response for its Writer. Then it will create an IMarkupWriter and tell it to pipe its output to that Writer. What's the difference between a IMarkupWriter and a Writer? The former can output markup data (elements and attributes), e.g., you can tell it to output a <tr> element by calling begin("tr") on it. The latter can output text data, e.g., you can tell it to output a string "abc" or "<tr>". When they're piped together, when you ask the former to output a <tr> element, it will ask the latter to output a string "<tr>". Of course, internally the Writer to convert the text into bytes and write them to the OutputStream. Anyway, after creating the IMarkupWriter, the direct service will load the response page, pass the IMarkupWriter to it and tell it to render itself to that IMarkupWriter. This is the normal process. But now when your listener is called, it asks the HTTP request for its OutputStream and sends the image file content (some bytes) to it. Then, as part of the normal processing, the direct service asks the HTTP request for its Writer. This is the problem if your listener has already asked for it in step 2. This is not allowed by Java so an exception is thrown.

HTTP response Writer


Output some bytes, e.g., four bytes representing "<tr>" if the encoding is ASCII.

OutputStream
1: Handle the HTTP request

Direct service

2: Call the listener

Listener

3: Give me your writer Output some text data, e.g., a string "<tr>".

IMarkupWriter
4: Create an IMarkupWriter and tell it to output to that writer in the HTTP request 5: Render yourself and send the output to that IMarkupWriter

Response page

Output some markup data, e.g., call write("tr") to output a <tr> element.

How to solve this problem? Can you let the response page output the data? For example, Let's create a Download page. Modify Download.java:

256

Chapter 8 Handling File Downloads and Uploads

This method is defined in BasePage. What it does is to process the HTML template to generate the HTML code. However, in this case you don't want this. You'd public abstract class Download extends BasePage { like to output the data yourself. So you override this method. The code in the method is just the same as private int imageId; before. @InjectObject("service:tapestry.globals.HttpServletResponse") public abstract HttpServletResponse getResponse(); The caller must tell it the image id public void setImageId(int imageId) { The IMarkupWriter is passed to the page for it to this.imageId = imageId; output send to } public void renderPage(IMarkupWriter writer, IRequestCycle cycle) { byte imageData[] = ImageDB.loadImage(imageId); HttpServletResponse response = getResponse(); response.setContentType("image/jpeg"); response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); try { OutputStream out = response.getOutputStream(); out.write(imageData); } catch (IOException e) { throw new ApplicationRuntimeException(e); } } }

You don't care the content of Download.html as you're not outputing it at all. Next modify Home.java:
public abstract class Home extends BasePage { @InjectPage("Download") public abstract Download getDownloadPage(); public IPage onDownload(int imageId) { Download download = getDownloadPage(); download.setImageId(imageId); return download; } }

You just get the Download page object, tell it the image id and then activate it. However, this won't work because before the renderPage() method is called, the direct service has asked the HTTP response for the Writer. So, you can't ask for the OutputStream in renderPage(). You can't output the bytes to the IMarkupWriter either because it can output markup data only.

Using a service
The essence of the problem here is that the direct service assumes that there is always a response page and Tapestry assumes that a page can only output text data. So there is no where that you can output bytes. To solve this problem, you can't use the direct service. Instead, you need to create your own service. How to call the service? For example, you may call this service using the URL: http://localhost:8080/Album/app?service=image&imageId=101
The name of the service. Each service must have a unique name. Tapestry uses this name to find out which service to call.

Pass the image id as the only service parameter

How does it work? First, in each Tapestry application there is a list of services (called "engine services"). A service is just a Java object. Each service has a name and a service() method. This list includes the page service and the direct service that you have seen. When a request such as http://localhost:8080/Album/app?service=image&imageId=101 arrives (see the diagram below), Tapestry notes from the URL that the service being requested is "image". So it asks each service in turn to see if its name is "image". In this case, it will find your service. Then it will call the service() of your service. You can do whatever you like in your service() method:

Handling File Downloads and Uploads

257

http://localhost:8080/Album/app?service=image&imageId=101

1: A request arrives

Tapestry Direct service


Name: "direct" 2: Is your name "image"? No. 3: Is your name "image"? Yes! void service() { ... }

4: Call its service() method

Your service
Name: "image" void service() { ... }

To implement this idea, create a new class ImageService:


Return the name of the service ("image" in this case) You need to implement this interface This is the most important method for a service. It performs the real work. In your case, you output the image data to the response.

public class ImageService implements IEngineService { public String getName() { return "image"; Get the image id. As any } parameter is treated as a public void service(IRequestCycle cycle) throws IOException { string, so you need to parse int imageId = Integer.parseInt(cycle.getParameter("imageId")); it into an int. byte imageData[] = ImageDB.loadImage(imageId); WebResponse response = cycle.getInfrastructure().getResponse(); //response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); try { OutputStream out = response.getOutputStream(new ContentType("image/jpeg")); out.write(imageData); } catch (IOException e) { throw new ApplicationRuntimeException(e); Specify the content type and } then get the OutputStream } public ILink getLink(boolean post, Object parameter) { return null; Get the infrastructure from the } request cycle. The } infrastructure contains useful helper objects used by the engine. Here, you get the It should return the link that You can't set the content-disposition response from it. Note that the can be used to call this header because a WebResponse response is not a service. Let's ignore it for the doesn't have a concept of a HttpServletResponse. It is a moment. response header. But let's ignore WebResponse instead. How is this problem for the moment. a WebResponse different from an HttpServletResponse?

How is a WebResponse different from an HttpServletResponse? Normally, a Tapestry application is responsible for displaying the whole HTML page in the browser. If the user clicks a link or submits a form on the page (see the diagram below), an HttpServletRequest will be sent to that Tapestry application and it will return an HttpServletResponse representing the new HTML page. In addition to the HTML code, it can also specify headers such as the Content-type header and Content-disposition header. In this case, Tapestry is acting as a "servlet":

258

Chapter 8 Handling File Downloads and Uploads

1: If the user clicks a link in the page, a regular HTTP request (HttpServletRequest) will be sent to Tapestry.

HttpServlet Request HttpServlet Response


Content-type: text/html Content-disposition: ... <html> ... ... </html>

Tapestry acting as a servlet

2: Tapestry, acting as a servlet, will return a HttpServletResponse representing the result HTML page.

However, a Tapestry application can also be used to control just a (child) window in a portal page (see the diagram below). In that case, Tapestry is acting as a "portlet":
A whole HTML page representing a portal A window controlled by portlet 1 A window controlled by portlet 2

What does a portlet do? For example, if the user clicks a link or submits a form in a window in the portal page shown above, a PortletRequest will be sent to that Tapestry portlet controlling that window. Then Tapestry will return a PortletResponse representing the new HTML code for that window. In addition to the HTML code, it can also specify extra information such as the content type using properties. But these properties are sent to the portal so that it can compose the whole HTML page. They aren't sent to the browser and thus aren't the same thing as HTTP headers. As a portlet can only return a part of the HTML page, a portlet can't send a file to the client and can't specify the content disposition:
1: If the user clicks a link in a portlet, a PortletRequest will be sent to the portlet.

PortletRequest
A property

Tapestry acting as a portlet

PortletResponse
Content-type: text/html <p> ... ... </p>

2: Tapestry, acting as a portlet, will return a PortletResponse representing the content for its window in the HTML page.

As Tapestry is designed to work in both environments, it takes the common denominators of HttpServletRequest and PortletRequest to form a WebRequest class. Similarly, it takes the common denominators of HttpServletResponse and PortletResponse to form a WebResponse class. As PortletResponse doesn't allow setting the content disposition, neither does WebResponse. Anyway, let's go ahead without the content disposition first. At least the image should be displayed in the browser. Now you have created a Java class for the service, you still need to add it to the list of services kept by Tapestry. To do that, create a file META-INF/hivemodule.xml under the root of the src folder:

Handling File Downloads and Uploads

259

<?xml version="1.0"?> <module id="com.ttdev.album" version="1.0.0"> <contribution configuration-id="tapestry.services.ApplicationServices"> <service name="image" object="instance:com.ttdev.album.ImageService"/> </contribution> The name of the </module> service To add an engine service to the list, just add a contribution to that configuration. In this object reference, the prefix is instance. It means what is following (the object locator) is a fully qualified class name (the ImageService class in this case). Hivemind will create an object of this class as the result. The resulted object will be used as the engine service.

The list of engine services in this Tapestry application is a configuration with id tapestry.services.ApplicationServices

The new entry to add to the list of engine services

Now you don't need the Download page anymore (which didn't work anyway), so delete the Download.* files. In the Home page, the link is no longer a DirectLink, but just a regular HTML link:
Home.html <html> <a href="/Album/app?service=image&amp;imageId=101" jwcid="download">Download photo #101</a> </html>

Home.page <page-specification class="com.ttdev.album.Home"> <component id="download" type="DirectLink"> <binding name="listener" value="onDownload"/> <binding name="parameters" value="101"/> </component> </page-specification> Home.java public abstract class Home extends BasePage { @InjectPage("Download") public abstract Download getDownloadPage(); public IPage onDownload(int imageId) { Download download = getDownloadPage(); download.setImageId(imageId); return download; } }

For the Hivemind module descriptor take effect, you need to restart the application. Then run it and it should be like:

This is because the image data is fake: It is just four arbitrary bytes and not a real JPEG image. To see a real image, find a real jpeg file and copy it into c:\workspace\Album\src\com\ttdev\album as 101.jpg. This way, it will also appear in WEB-INF/classes/com/ttdev/album, then modify ImageDB.java to read it:

260

Chapter 8 Handling File Downloads and Uploads

public class ImageDB { public static byte[] loadImage(int imageId) { InputStream input = ImageDB.class.getResourceAsStream("/com/ttdev/album/"+imageId+".jpg"); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte buf[] = new byte[1024]; Copy the bytes from the jpg file into for (;;) { the ByteArrayOutputStream try { int noBytesRead = input.read(buf); if (noBytesRead == -1) { On end of file (EOF), return the bytes return output.toByteArray(); in the ByteArrayOutputStream. } output.write(buf, 0, noBytesRead); } catch (IOException e) { throw new RuntimeException(e); } } return new byte[] { 68, 75, 78, 90 }; } }

Get the class object for ImageDB. Then read /com/ttdev/album/101.jpg from the class path as an input stream.

Now run the application and it should work:

In order to really download the image, you need to get access to the HttpServletResponse. Working with WebResponse just isn't enough. If the ImageService class were a page class, then you could inject the HttpServletResponse as a service like this:
public class ImageService implements IEngineService { public String getName() { return "image"; } @InjectObject("service:tapestry.globals.HttpServletResponse") public abstract HttpServletResponse getResponse(); public void service(IRequestCycle cycle) throws IOException { //call getResponse() } ... }

However this wouldn't work because it is not a page class. Annotations work because when Tapestry loads the page and notes the annotations, it will perform the necessary work (e.g., create a subclass with getters to access the injected objects). For a regular Java class like ImageService, no one will be performing this step. So the question is, how can a regular Java class get access to a Hivemind service? It turns out this is quite difficult to do. However, if this class is used to create a Hivemind service, then it is very easy for that Hivemind service to get access to other Hivemind services. So the idea is that you can use ImageService to create a Hivemind service, which can easily access other Hivemind service, while remaining a Java object and can still be used as an engine service. To use ImageService to create a Hivemind service, edit hivemodule.xml: How does <invoke-factory> work? When Hivemind sees the <invoke-factory> element (see the diagram below), it will ask a pre-defined object factory in Hivemind to create the Java object which will act as the Hivemind service. This object factory notes from the <construct> element the Java class name is ImageService. So it creates an ImageService object. Then, it checks the ImageService to see if there is any public setter whose property type is an interface. In this case it will find one: setResponse(). The corresponding property type is the HttpServletResponse interface. Then it will

Handling File Downloads and Uploads

261

Id of the Hivemind module

Create a Hivemind service (on demand)

The id of the service. This is just the short id. The full id is:

com.ttdev.album.ImageSerivce

<module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <invoke-factory> The object locator <construct class="com.ttdev.album.ImageService"/> is the id of the </invoke-factory> Hivemind service </service-point> <contribution configuration-id="tapestry.services.ApplicationServices"> <service name="image" object="instance:com.ttdev.album.ImageService"/> <service name="image" object="service:com.ttdev.album.ImageService"/> </contribution> </module> In Hivemind, when people try to access Here you tell Hivemind how to create the Now, use a Hivemind service as the a service, the object returned must engine service. If you continue to Java object. In the simplest case, you have a Java type and it must be an can tell Hivemind a class name so that it use the instance prefix, it will just interface. As ImageService is can create an instance from it. For this create a new instance without using implementing the IEngineService the global Hivemind service. you can use a <create-instance> interface, you can just use element like: IEngineService. If it implemented some However, for your case, <create-instance> is not enough. After creating the instance, you'd like Hivemind to get the tapestry.globals.HttpServletResponse service and store it into a property of that instance. To do that, you can use <invoke-factory>. But how does it work? other interfaces, you could use any of them too.

<module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <create-instance class="com.ttdev.album.ImageService"/> </service-point> ... </module>

ask Hivemind to check if there is exactly one Hivemind service whose interface is HttpServletResponse. In this case yes. The tapestry.globals.HttpServletResponse Hivemind service is the one and only in this application for this interface. So the object factory will get this service and pass it to the setter. Now, this ImageService object (your Hivemind service) can access the HttpServletResponse. This feature is called "auto-wiring of Hivemind services":

262

Chapter 8 Handling File Downloads and Uploads

2: It is for you. Please handle it.

Hivemind
6: Is there exactly one Hivemind service whose interface is HttpServletResponse?

Object factory

4: Create

7: Yes, I have one whose id is tapestry.globals.HttpServletRes ponse. 1: Look, there is a <invoke-factory> element. <module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <invoke-factory> <construct class="com.ttdev.album.ImageService"/> The Hivemind </invoke-factory> service </service-point> 5: Look, it has a public setter ... ImageService to set a property whose type </module> is an interface (HttpServletResponse).

3: Look, they want to create an object of this ImageService class.

8: Call the setter and pass the tapestry.globals.Htt pServletResponse service as the argument.

public class ImageService implements IEngineService { private HttpServletResponse response; ... public void setResponse(HttpServletResponse response) { this.response = response; } public void service(IRequestCycle cycle) { ... } }

Now, you can send the JPEG data to the HttpServletResponse instead of the WebResponse:
public class ImageService implements IEngineService { private HttpServletResponse response; ... public void setResponse(HttpServletResponse response) { this.response = response; } public void service(IRequestCycle cycle) throws IOException { int imageId = Integer.parseInt(cycle.getParameter("imageId")); byte imageData[] = ImageDB.loadImage(imageId); WebResponse response = cycle.getInfrastructure().getResponse(); response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); response.setContentType("image/jpeg"); try { OutputStream out = response.getOutputStream(new ContentType("image/jpeg")); OutputStream out = response.getOutputStream(); out.write(imageData); } catch (IOException e) { throw new ApplicationRuntimeException(e); } } }

To test if it is working, restart the application so that the hivemodule.xml file is read again. Then just enter http://localhost:8080/Album/app?service=image&imageId=101 in the browser and hit Enter. You'll see:

Handling File Downloads and Uploads

263

Click "OK" and it will save the JPEG file. You can double check it by opening the JPEG with your browser.

Generating the link to call the service


At the moment your Home.html is:
<html> <a href="/Album/app?service=image&amp;imageId=101">Download photo #101</a> </html>

This is no good because it hardcodes the context path which is determined by the person deploying your application, not by you. To solve this problem, the idea is that you can let your ImageService generate the link for you at runtime. To do that, you need to implement the getLink() method in ImageService.java:

264

Chapter 8 Handling File Downloads and Uploads

A LinkFactory can create links for you. LinkFactory is an interface so you can't use it directly.

It is an array containing all the parameters. You should have only a single string parameter (the image id). In each Tapestry there is a Hivemind service implementing the LinkFactory interface. So you can use auto-wiring to get access to it.

public class ImageService implements IEngineService { private HttpServletResponse response; private LinkFactory linkFactory;

public void setResponse(HttpServletResponse response) { Query parameters to be included in the this.response = response; link (except for "service" parameter). In } this case, this will generate one public void setLinkFactory(LinkFactory linkFactory) { parameter. The link factory expects that this.linkFactory = linkFactory; the value of a parameter is always } string. public ILink getLink(boolean post, Object parameter) { String imageId = (String)((Object[])parameter)[0]; Is the link used to in a POST (a form Map parameters = new HashMap(); submission) or GET (regular link)? parameters.put("imageId", imageId); return linkFactory.constructLink(this, post, parameters, false); } ... Whether the link factory should try to put a Create the link such as: } jsessionid parameter to the URL. If you pass true, it will do that only if there is a session and it is http://localhost:8080/Album/app?service=image&imageId=101 maintained using URL rewriting instead of a cookie. If you pass false, it will never add the jsessionid. Because the link will trigger a This part can be determined by The LinkFactory can find out download, there is no need to maintain the the LinkFactory implementation the name of the service session (if any). If your service would display automatically from the "this" argument and another web page, then you would pass a true. generate the "service" query parameter.

Next, to generate the link to call the service on the Home page, you should use a ServiceLink component. So, modify the Home page:
<html> <a href="" jwcid="download">Download photo #101</a> </html> <page-specification class="com.ttdev.album.Home"> <component id="download" type="ServiceLink"> <binding name="service" value="literal:image"/> <binding name="parameters" value="{'101'}"/> </component> </page-specification> Always bind it to an array. Note that you must quote 101 so that it is treated as string.

Specify the name of the service ("image" in this case)

public class ImageService implements IEngineService { ... public ILink getLink(boolean post, Object parameter) { String imageId = (String)((Object[])parameter)[0]; Map parameters = new HashMap(); parameters.put("imageId", imageId); return linkFactory.constructLink(this, post, parameters, false); } }

When this ServiceLink component renders itself, how can it know the URL? It doesn't. It simply calls the getLink() method of your service object to find the URL. Now run the application, but unfortunately it doesn't work:

Handling File Downloads and Uploads

265

It is saying that the link factory property is null. It means auto-wiring isn't working. The reason is that there are two Hivemind services in this application implementing the LinkFactory interface: Hivemind service id tapestry.url.LinkFactory Purpose Generate links for normal web applications

tapestry.portlet.PortletLinkFactory Generate links for portlets So, you need to tell the Hivemind object factory to wire the tapestry.url.LinkFactory Hivemind service to the ImageService Hivemind service in hivemodule.xml:
<module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <invoke-factory> <construct class="com.ttdev.album.ImageService"> <set-object property="linkFactory" value="service:tapestry.url.LinkFactory"/> </construct> </invoke-factory> </service-point> <contribution configuration-id="tapestry.services.ApplicationServices"> <service name="image" object="service:com.ttdev.album.ImageService"/> </contribution> </module> Tell the object factory to set the Set the property to this value. This is an object "linkFactory" property of the reference. Here you use the service prefix so object after creating it. the value is a Hivemind service (the link factory).

Restart the application so that the hivemodule.xml is read again. The URL generated is:

266

Chapter 8 Handling File Downloads and Uploads

Displaying a photo
Suppose that you'd like to display the photo in along with the link:

As shown above, the photo here is a hammer. To achieve this effect, modify the Home page:
<html> <a href="" jwcid="download">Download photo #101</a> <img jwcid="@Any" src="ognl:getImageUrl(101)"/> </html> Use an Any component to set the "src" attribute dynamically Must be IEngineService. You Inject the engine service named can't write ImageService here. "image". Note that the prefix is Why? engine-service, not serivce, public abstract class Home extends BasePage { otherwise you'll be injecting a @InjectObject("engine-service:image") Hivemind service. public abstract IEngineService getImageService(); It is a regular link, public String getImageUrl(int imageId) { not a form POST. return getImageService().getLink(false, new Object[] { Integer.toString(imageId) }).getURL(); } getLink() will return } a ILink. You need Create a string to call getURL() to Ask the image array as the get a URL string. service to generate parameters the URL

Note that the getter must be marked as returning IEngineService, not ImageService. Why? Your ImageService Hivemind service is marked as implementing IEngineService. When Hivemind needs to create this service, it usually will not create an ImageService object yet. Instead, it hopes to delay creating it as long as possible. So, it will create a proxy class implementing IEngineService and create an object of that class as the service. Only when you actually call a method on that service, will it in turn create the ImageService object and call its method:

Handling File Downloads and Uploads

267

IEngineService
1: Get me the com.ttdev.album.ImageService service 3: Create a proxy class implementing IEngineService Implements

Hivemind
2: Look, what's the interface this service implements. 4: Create an object of that proxy class 5: Someone calls its method, i.e., getLink() or service().

EngineService Proxy class

EngineService Proxy object

6: Create the ImageService object and call it

ImageService object

<module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <invoke-factory> <construct class="com.ttdev.album.ImageService"> <set-object property="linkFactory" value="service:tapestry.url.LinkFactory"/> </construct> </invoke-factory> </service-point> ... </module>

Therefore, the Hivemind service object you get will implement IEngineService, but is not really an ImageService. That's why your getter can't be marked as returning an ImageService. Now run the application and it should work:

Using friendly URL


At the moment the parameters passed to the services are encoded as query parameters: URL http://localhost:8080/Album/app?service=image&imageId=101 http://localhost:8080/Album/app?service=page&page=Home With some additional work, you can change the URLs to: URL http://localhost:8080/Album/images/101.jpg Purpose Get image whose id is 101 Display the Home page Purpose Get image whose id is 101

http://localhost:8080/Album/Home.html Display the Home page That is, the information is encoded in the path instead of the query parameters. Why would you want to do that? For one thing, many search engines will not index URLs containing query parameters like http://localhost:8080/Album/app?service=page&page=Home, but will normal URLs like http://localhost:8080/Album/Home.html. The latter are also shorter and easier to type. As they are more friendly to search engines and human, they are traditionally called "friendly URLs".

268

Chapter 8 Handling File Downloads and Uploads

How to let the services generate friendly URLs? The tapestry.url.LinkFactory has a list of so called "service encoders". What is a service encoder? What it does is simple. You give it the path relative to a context (/Album is the context path) and it may transform it to another path:
/app?service=page&page=Home

ServiceEncoder

/Home.html

When a service asks the link factory to generate a link (see the diagram below), it will put the path relative to the context (e.g., /app) and the query parameters into a data structure called a "service encoding". Then it passes the service encoding to each of its service encoder to see if anyone can transform it. It will finish and return the (modified) link as soon as a service encoder can transform the service encoding. Finally, it uses the service encoding to create the link:

Service encoding
Relative path: /app Query parameters: service: image imageId: 101

Service encoding
Relative path: /images/101.jpg Query parameters: nothing

1: Generate a link. Here are the path after the context path (/app) and the parameters:

5: Yes, I've changed the service encoding to:

Link factory

3: Sorry, no.

2: Can you transform it? 6: Generate the link using the modified service encoding

ServiceEncoder

4: Can you transform it?

ServiceEncoder

http://localhost:8080/Album/images/101.jpg

ServiceEncoder

The idea is that you can have a service encoder to deal with links for the page service, another service encoder for the image service, and yet another service encoder for the direct service and etc. To implement this idea, modify hivemodule.xml:

Handling File Downloads and Uploads

269

<module id="com.ttdev.album" version="1.0.0"> <service-point id="ImageService" interface="org.apache.tapestry.engine.IEngineService"> <invoke-factory> <construct class="com.ttdev.album.ImageService"> <set-object property="linkFactory" value="service:tapestry.url.LinkFactory"/> </construct> </invoke-factory> </service-point> <contribution configuration-id="tapestry.services.ApplicationServices"> <service name="image" object="service:com.ttdev.album.ImageService"/> </contribution> The list of service encoders <contribution configuration-id="tapestry.url.ServiceEncoders"> is a configuration with this <page-service-encoder id="page" extension="html" service="page"/> id <encoder id="image" object="instance:com.ttdev.album.ImageServiceEncoder"/> </contribution> </module> Add the first service encoder Each service encoder It will map say the It will transform the to it. This service encoder is must have a unique Home page to service encoding only provided by Tapestry. It can id. You can give it Home.html. If you set if the "service" query deal with links for the page any id you'd like. it to htm, it will map parameter is "page". service. the Home page to Home.htm. Again, give the For the service encoders This is an object reference. service encoder a created by yourself, you will Here you use the instance unique id. use an <encoder> element to prefix and specify a Java add it to the list. class. Of course you need to provide this class. Must implement this interface

public class ImageServiceEncoder implements ServiceEncoder { public void encode(ServiceEncoding encoding) { Try to transform (or encode) String service = encoding.getParameterValue("service"); the service encoding if (!service.equals("image")) { return; Transform it only if the } "service" parameter is String imageId = encoding.getParameterValue("imageId"); "image" encoding.setServletPath("/images/"+imageId+".jpg"); encoding.setParameterValue("service", null); Set the context relative path encoding.setParameterValue("imageId", null); ("servlet path") to something } like /images/101.jpg public void decode(ServiceEncoding encoding) { Setting their values to null will String path = encoding.getServletPath(); remove them from the service if (!path.equals("/images")) { encoding return; When Tapestry receives a } request like /images/101.jpg, it String filename = encoding.getPathInfo().substring(1); will ask the service encoders String imageId = filename.substring(0, filename.length()-4); to try to decode it. Here, you encoding.setServletPath("/app"); should restore the service encoding.setParameterValue("service", "image"); encoding. encoding.setParameterValue("imageId", imageId); } } Remove .jpg Restore the service encoding back to the original. In fact, you don't need to set the It will return /101.jpg. Call servlet path anymore. Only the query substring(1) to get rid of parameters are used for further the leading slash to get processing. the image filename. For a reason explained later, it will return /images, not /images/101.jpg To get the rest of the path (/101.jpg), call getPathInfo().

However, this doesn't quite work yet. If you enter http://localhost:8080/Album/Home.html in a browser, Tomcat will try to return a static file c:/workspace/Album/context/Home.html, by passing Tapestry at all. Normally a link like / app?service=page will be forwarded to Tapestry for handling, because web.xml is:

270

Chapter 8 Handling File Downloads and Uploads

Information about a web application

It defines a servlet. A servlet is just a Java object.

<web-app> <display-name>Album</display-name> <servlet> <servlet-name>Album</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> </web-app> The name of the servlet is "Album". You can later refer to it using this name. Create just one You must specify the Java class for such object to act the servlet. Here the class is as the servlet ApplicationServlet coming with Tapestry. It is telling Tomcat to create an object of this class to act It tells Tomcat that if a request starts as the servlet. Therefore, this servlet with /app, then let the servlet named is called the "application servlet" in "Album" handle it. Tapestry.

How does it work? Suppose a user tries to access your application using a (unfriendly) URL http://localhost:8080/Album/app?service=page&page=Home (see the diagram below). When Tomcat receives this request, it notes /Album is the context path for your application. So, it looks into the web.xml file of your application. Then it continues to check the rest of the URL and finds /app. As specified by the web.xml, this request should be sent to the application servlet (named "Album") for processing:
1: A request comes in like http://localhost:8080/Album/app?service=page&page=Home 7: Create and call it

Tomcat
4: Read its web.xml

Album servlet

2: Match a context path? <Context docBase="c:/workspace/Album/context" path="/Album" reloadable="true"/> <Context docBase="c:/workspace/Foo/context" path="/Foo" reloadable="true"/>

3: Check its docBase c: workspace Album context

5: Match a servlet's URL pattern?

6: Check its Java class

WEB-INF web.xml

<web-app> <display-name>Album</display-name> <servlet> <servlet-name>Album</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> </web-app>

After receiving the request (see the diagram below), the application servlet will get an engine object and pass the request to it. The engine object notes that the service being requested is "page", so it will pass the request to the page service for processing, i.e., call your service() method in the page service class:

Handling File Downloads and Uploads

271

1: A request comes in like http://localhost:8080/Album/app?service=page&page=Home

Album servlet
2: Call the engine

3: Check what service it is trying to call?

Engine
4: Get the page service and call its service() method

Page service
Now as the path doesn't start with /app, the request will not be passed to Tapestry (the application servlet) for handling. To solve this problem, modify web.xml:
<web-app> <display-name>Album</display-name> <servlet> <servlet-name>Album</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Album</servlet-name> If the URL matches *.html, such as / <url-pattern>*.html</url-pattern> Home.html or /pages/Home.html, </servlet-mapping> then let the Album servlet handle it. <servlet-mapping> <servlet-name>Album</servlet-name> If the URL matches /images/*, such <url-pattern>/images/*</url-pattern> as /images/101.jpg or / </servlet-mapping> images/foo.gif, then let the Album </web-app> servlet handle it.

Note that for the URL pattern you can either specify a filename pattern (*.html or *.jpg) or specify a folder (/images/* or / foo/bar/*), you can't specify both a folder and a filename pattern (/images/*.jpg). This is a restriction placed by the servlet specification. Now, if the user enters say http://localhost:8080/Album/images/101.jpg, as mentioned before Tapestry will create a service encoding for the service encoders to work on. In this case the service encoding should be like:

Service encoding
Servlet path: /images Path info: /101.jpg Query parameters: nothing

Note that the servlet path is just /images, not /images/101.jpg because only /images is specified as the url-pattern for the Album servlet. What is matched by the * in the url-pattern will be stored as the "path info". That's why your service encoder checks if it's applicable by checking if the servlet path is /images. If so, it will then retrieve the image id from the path info:
public class ImageServiceEncoder implements ServiceEncoder { ... public void decode(ServiceEncoding encoding) { String path = encoding.getServletPath(); if (!path.equals("/images")) { return; } String filename = encoding.getPathInfo().substring(1); String imageId = filename.substring(0, filename.length() - 4); encoding.setServletPath("/app");

272

Chapter 8 Handling File Downloads and Uploads

encoding.setParameterValue("service", "image"); encoding.setParameterValue("imageId", imageId); } }

Now, restart the application so that web.xml and hivemodule.xml http://localhost:8080/Album/Home.html in the browser and it should work:

are

read

again.

Then

enter

The download link generated will also use friendly URL:

Clicking on the link will continue to work. It means both encoding and decoding are working. For the built-in engine services in Tapestry, there are built-in service encoders to allow them to use friendly URLs. You don't need to write them yourself: For service Direct How to configure hivemodule.xml
<contribution ...> <direct-service-encoder id="direct" stateless-extension="direct" stateful-extension="sdirect"/> </contribution>

Sample URL Original /app?service=direct&page=Home&component=form

web.xml
<servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>*.direct</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Album </servlet-name> <url-pattern>*.sdirect</url-pattern> </servlet-mapping>

Encoded /Home,form.direct

Asset

hivemodule.xml
<contribution ...> <asset-encoder id="asset" path="/assets"/> </contribution>

Original /app?service=asset&digest=12345&&path=/com/ttdev/ album/foo.gif

web.xml
<servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>/assets/*</url-pattern> </servlet-mapping>

Encoded /assets/12345/com/ttdev/album/foo.gif

Handling File Downloads and Uploads

273

For service

How to configure /app?service=home

Sample URL Original

Home, Restart or hivemodule.xml any service that <contribution ...> doesn't take any <extension-encoder id="extension" parameter
extension="svc" after="*"/> </contribution>

web.xml
<servlet-mapping> <servlet-name>Album</servlet-name> <url-pattern>*.svc</url-pattern> </servlet-mapping>

Encoded /home.svc

Downloading a photo using a form


Suppose that you'd like to allow the user to enter an image id in a form to download the image:

To do that, modify Home.html:


<html> <a href="" jwcid="download">Download photo #101</a> <img jwcid="@Any" src="ognl:getImageUrl(101)"/> <form jwcid="form"> <input type="text" jwcid="imageId"/> <input type="Submit" value="OK"/> </form> </html>

Define the components in Home.page:


<page-specification class="com.ttdev.album.Home"> <component id="download" type="ServiceLink"> <binding name="service" value="literal:image"/> <binding name="parameters" value="{'101'}"/> </component> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="imageId" type="TextField"> <binding name="value" value="imageId"/> </component> </page-specification>

Define the listener in Home.java:

274

Chapter 8 Handling File Downloads and Uploads

public abstract class Home extends BasePage { @InjectObject("engine-service:image") public abstract IEngineService getImageService(); public String getImageUrl(int imageId) { return getImageService().getLink(false, new Object[] { Integer.toString(imageId) }).getURL(); } Declare a property public abstract String getImageId(); public void onOk() { throw new RedirectException(getImageUrl(Integer.parseInt(getImageId()))); } } Redirect to that URL Get the URL to download the image

You have seen the PageRedirectException before which accepts a page name. In order to redirect to an arbitrary URL, you need to throw a RedirectException instead. If that URL is not inside this application, it will send a redirect to the browser, otherwise the redirect is also internally to Tomcat. Now run the application and it should work (but don't enter 102 because there is no 102.jpg on the classpath).

Telling the size of the download


Sometimes when you download a file, the browser will display a progress bar and the total size of the file:

How does the browser know the total size? You must tell it. Otherwise it will be unable to do that. Let's modify ImageService.java:
public class ImageService implements IEngineService { ... public void service(IRequestCycle cycle) throws IOException { int imageId = Integer.parseInt(cycle.getParameter("imageId")); byte imageData[] = ImageDB.loadImage(imageId); response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); response.setContentType("image/jpeg"); response.setContentLength(imageData.length); try { OutputStream out = response.getOutputStream(); out.write(imageData); } catch (IOException e) { throw new ApplicationRuntimeException(e); } } }

That's it! Run the application and it should continue to work.

Uploading a photo
Suppose that you'd like to allow the user to upload a photo:

Handling File Downloads and Uploads

275

To do that, create an Upload page. Then modify Upload.html:


<html> <form jwcid="uploadForm"> <input type="file" jwcid="upload"/><p> <input type="submit" value="OK"/> </form> </html>

Define the components in Upload.page:


<page-specification class="com.ttdev.album.Upload"> <component id="uploadForm" type="Form"> <binding name="listener" value="listener:onOk"/> </component> It will render itself <component id="upload" type="Upload"> as: <binding name="file" value="file"/> </component> </page-specification> When the user chooses a file and clicks "OK", the file content will be sent to the server. The Upload component will save the file content to a temporary file, create a IUploadFile object containing the path to the temporary file, the filename of the original file and etc., then it will set the IUploadFile object to its "file" parameter.

Path: c:\temp\foo

IUploadFile

Next, create Upload.java to define the onOk() listener:


public abstract class Upload extends BasePage { public abstract IUploadFile getFile(); Declare a property to hold the IUploadFile object

public void onOk() { Find out the size of byte imageData[] = new byte[(int) getFile().getSize()]; the file so that you can InputStream fileInput = getFile().getStream(); allocate a buffer try { fileInput.read(imageData); Get an InputStream from the } catch (IOException e) { IUploadFile. Then you can read throw new RuntimeException(e); the bytes from the InputStream. } //save the image data } }

But where to save the image data on the server? Can you save it into somewhere in c:\workspace\Album? Generally you shouldn't do that because your context folder may have been packaged and deployed as a jar file (usually such a jar file is assigned the .war extension to mean a "web application archive"). For example, one may package the context folder of this application as Album.war and then copy it into the c:\tomcat\webapps folder, then it will be deployed as / Album. In that case, there is no way for you to write into it. One way to do it is to let the deployer specify a folder to store the images (or to store them into a database). For example, he could do it in the context descriptor:
<Context docBase="c:/workspace/Album/context" path="/Album" reloadable="true"> <Parameter name="com.ttdev.album.image-folder" value="c:/tmp/images"/> </Context>

Then you can read this parameter to find out where to save the image data:

276

Chapter 8 Handling File Downloads and Uploads

JVM
public abstract class Upload extends BasePage { public abstract IUploadFile getFile(); @InjectMeta("com.ttdev.album.image-folder") public abstract String getImageFolder(); Inject a meta property. It will try to find a setting named "com.ttdev.album.image-folder" at various places: 5: JVM parameter

public void onOk() { 1: Page specification byte imageData[] = new byte[(int) getFile().getSize()]; InputStream fileInput = getFile().getStream(); Upload.page try { <page ...> fileInput.read(imageData); <meta key="???" value="???"/> } catch (IOException e) { </page> throw new RuntimeException(e); } 2: Application specification ImageDB.saveImage(101, imageData, getImageFolder()); (or library specification if it } was in a library) } Album.application For simplicity, always Tell it the <application ...> save it as 101.jpg in image <meta key="???" value="???"/> the image folder. folder </application> 3: Deployment descriptor 4: Context descriptor Album.xml <Context ...> <Parameter name="???" value="???"/> </Context>

web.xml <web-app ...> <context-param> <param-name>???</param-name> <param-value>???</param-value> </context-param> </web-app>

As shown above, you don't have to define the meta property in the context descriptor; You could do it in the page specification (no good here as it affects a single page, not the whole application), the application specification (no good because this image folder should be set by the person deploying the application, not by you, the developer) or a library specification if the page was in a library (no good for the previous reason), web.xml (no good for the previous reason) or as a JVM parameter (no good because it will affect all web applications running in Tomcat, not just this one). Note that the way to set parameters in the context descriptor shown above is specific to Tomcat. It may or may not in other products. To access a meta property in code, again, you don't have to use annotations. You can do it in Upload.page:
<page-specification class="com.ttdev.album.Upload"> <inject property="imageFolder" type="meta" object="com.ttdev.album.image-folder"/> <component id="uploadForm" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="upload" type="Upload"> <binding name="file" value="file"/> </component> </page-specification>

Next, modify ImageDB to provide the saveImage() method:

Handling File Downloads and Uploads

277

public class ImageDB { public static byte[] loadImage(int imageId, String imageFolder) { InputStream input = ImageDB.class .getResourceAsStream("/com/ttdev/album/" + imageId + ".jpg"); FileInputStream input; Read the image data from a file in the image folder, instead try { of from the classpath. input = new FileInputStream(new File(imageFolder, imageId + ".jpg")); } catch (FileNotFoundException e1) { throw new RuntimeException(e1); Use the image id as } the filename ByteArrayOutputStream output = new ByteArrayOutputStream(); byte buf[] = new byte[1024]; for (;;) { try { int noBytesRead = input.read(buf); if (noBytesRead == -1) { return output.toByteArray(); } output.write(buf, 0, noBytesRead); } catch (IOException e) { throw new RuntimeException(e); Use the image id as } the filename } } public static void saveImage(int imageId, byte[] imageData, String imageFolder) { File imageFile = new File(imageFolder, imageId + ".jpg"); try { FileOutputStream output = new FileOutputStream(imageFile); try { output.write(imageData); } finally { output.close(); } } catch (IOException e) { throw new RuntimeException(e); } } }

Now in order to call loadImage(), you must provide the image folder. So, you need to find out the image folder in ImageService.java:
public class ImageService implements IEngineService { private HttpServletResponse response; private LinkFactory linkFactory; private Infrastructure infrastructure;

public void setInfrastructure(Infrastructure infrastructure) { this.infrastructure = infrastructure; } public void service(IRequestCycle cycle) throws IOException { int imageId = Integer.parseInt(cycle.getParameter("imageId")); byte imageData[] = ImageDB.loadImage(imageId, Get the property source from infrastructure the infrastructure and then read .getApplicationPropertySource() the property .getPropertyValue("com.ttdev.album.image-folder")); response.setHeader("Content-disposition", "attachment; filename=foo.jpg"); response.setContentType("image/jpeg"); response.setContentLength(imageData.length); try { OutputStream out = response.getOutputStream(); out.write(imageData); } catch (IOException e) { throw new ApplicationRuntimeException(e); } } ... }

Use auto-wiring to get access to a Hivemind service implementing the Infrastructure interface

Now to load an image, enter http://localhost:8080/Album/Upload.html in the browser:

278

Chapter 8 Handling File Downloads and Uploads

Click "Browse" to locate any JPEG file:

Click "OK". You'll see the Upload page again because you're not activating any page in the listener. To see if it is really uploaded and saved into 101.jpg, go to the Home page:

You should see that the image has changed. It means that it is working. However, what if the user doesn't choose a file but still clicks "OK"? Then the IUploadFile will be null. You should watch out for this case and quit:
public abstract class Upload extends BasePage { public abstract IUploadFile getFile(); @InjectMeta("com.ttdev.album.image-folder") public abstract String getImageFolder(); public void onOk() { if (getFile() == null) { return; } byte imageData[] = new byte[(int) getFile().getSize()]; InputStream fileInput = getFile().getStream(); try { fileInput.read(imageData); } catch (IOException e) { throw new RuntimeException(e); } ImageDB.saveImage(101, imageData, getImageFolder()); } }

Summary
Each Hivemind application has a list of Hivemind services. Each Hivemind service has a unique id and can perform some work for you. One of such Hivemind services in each Tapestry application is the HTTP response whose id is

Handling File Downloads and Uploads

279

tapestry.globals.HttpServletResponse. To access it, use @InjectObject or <inject type="object"> and specify "service:tapestry.globals.tapestry.globals.HttpServletResponse" as the object reference. @InjectObject can be used inject things other than Hivemind services. For example, you can use "instance:com.foo.Bar" as the object reference to create an instance of the com.foo.Bar class, or use "engine-service:page" to access the Tapestry page service. To send a file download to the user, you need to get the HTTP response object, set its content-disposition header and content-type header and send the file data to its output stream. However, you can't do it in a listener nor during the rendering of a page because the direct service and page service can only output markup data. Therefore you need to use a (engine) service. You can invoke a service and pass it some parameters and it will send the output to the HTTP response. For a service, there is no concept of response page or markup writer. To register a service with Tapestry, add a contribution to the tapestry.services.ApplicationServices configuration. In the contribution, specify an object reference. For simple cases, use the instance prefix and specify a class name. But if your service needs to access some Hivemind services to get access to the HttpServletResponse, link factory, Infrastructure and etc, you need to use a Hivemind service as the engine service. Then you'll use the service prefix to add the engine service to the list. To create a Hivemind service, use a <service-point> in hivemodule.xml. Inside it you can use a <create-instance> and specify a class name for simple cases. If you need to store references to other Hivemind services into it, you need to use a <invoke-factory> to call the pre-defined Hivemind object factory and pass the class name to it. Then the object factory will perform auto-wiring: It checks each setter in your class. If the property type is an interface and it can find exactly one Hivemind service implementing that interface, it will set a reference to that Hivemind service into that property. If there are more than one Hivemind services implementing that interface, you need to use <set-object> to specify which Hivemind service you'd like. The link factory has a list of service encoders to transform the link to support friendly URLs. You can add your own service encoder to that list by adding a contribution to the tapestry.url.ServiceEncoders configuration. The link factory will put the context relative path and the query parameters into a service encoding object and pass it to each service encoder for processing. Each service encoder checks if it can encode it and if so, encodes it. Then the link factory will use the (modified) service encoding to generate the link. As the context relative path in the resulted link (friendly URL) probably doesn't start with /app, you need to add a servlet mapping in web.xml so that the request will be directed to the Tapestry application servlet for handling, otherwise Tomcat will just try to get a static file. After receiving the request, Tapestry will try to decode the service encoding by asking each service encoder to do that. To call a service from a link, use a ServiceLink. The flow is: browser => Tomcat => application servlet of your application => engine => service. To call it from a Submit button, inject that service into your page with @injectObject and use the service prefix in the object reference. Then ask the service for its URL and then throw a RedirectException in the listener. As this is an arbitrary URL, you can't use a PageRedirectException which accepts a page name only. When injecting a Hivemind service, your getter must be marked as returning the interface implemented by that Hivemind service instead of the implementation class. As Hivemind will create a proxy class to act as the service object to delay the creation of the real service object, the service object doesn't belong to the your implementation class. A service can get access to the WebRequest and WebResponse easily through the infrastructure. This allows the services to work in both a servlet web application and a portlet web application. A portlet can control the content of a window in a page only and can't download a file to the browser. To allow the user to upload a file, use the Upload component. It will extract the file from the HTTP request and save it into a temporary location on the server and store the path into an IUploadFile object. If the user didn't specify any file, the filename will be an empty string. In generally you shouldn't try to write into your context folder because it may have been packaged as a .war file. If your application accepts configuration parameters (called "meta" properties), they can be specified as a JVM parameter (for all web applications), in the application specification or the web.xml file (for this web application and is determined by you), in a page specification (for that particular page) or in the context descriptor (for this web application and is determined by the person deploying the application). In this case the best place is the context descriptor. To read a meta property from a Home, use @injectMeta. To do it in a .page file, use <inject type="meta">. To do it in a service, get access to the infrastructure and get the property source.

281

Chapter 9
Chapter 9

Providing a Common Layout

282

Chapter 9 Providing a Common Layout

What's in this chapter?


In this chapter you'll learn how to create pages that share a common layout.

Providing a common layout


Suppose that you'd like to develop an application shown below. It is unimportant what it does. What is important is that on each page there is a menu at the left:

Let's create a Tapestry application named Layout and perform the setup as usual (use /Layout as its context path). Create three pages: Home, Products and Contact. Home.html and Products.html may be like:

Providing a Common Layout

283

Home.html <html> <head><title>Home</title></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> This is the Home page. </td> </tr> </table> </body> </html>

Exactly the same

Products.html <html> <head><title>Products</title></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> This is the Products page. </td> </tr> </table> </body> </html>

As shown above, this is no good because the menu is duplicated in all the pages. This is a problem if later you decide to add a new menu item such as "Resources", then you will have to update all the pages! To solve this problem, you could extract the code for menu to form a Menu component:

284

Chapter 9 Providing a Common Layout

Home.html <html> <head><title>Home</title></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> This is the Home page. </td> </tr> </table> </body> </html> Home.html (updated) <html> <head><title>Home</title></head> <body> <table> <tr> <span jwcid="Menu"/> <td> This is the Home page. </td> </tr> </table> </body> </html>
Table Row Cell Menu Cell Content unique to the Home page

Extracted to form a component <span jwcid="Menu"/>

Table structure still duplicated

Products.html (updated) <html> <head><title>Products</title></head> <body> <table> <tr> <span jwcid="Menu"/> <td> This is the Products page. </td> </tr> </table> </body> </html>
Table Row Cell Menu

As shown above, it would be better but the table structure would still be duplicated, i.e.:

Cell Content unique to the Products page

Only this part is different, everything else is the same.

Later if you decide to say move the menu to the top:


Table Row Cell Menu Cell Content unique to the Home page

Then you will have to modify all the pages again. So, what you'd like is to extract the <table> element and the menu in it to form a component:

Providing a Common Layout

285

Table Row Cell Menu Cell Content unique to the Home page

Extract the table structure to form a component Border component

Content unique to the Home page

In Tapestry, commonly you call such a component a Border component because it will generate the border surrounding the content unique to each page. Let's a new component type Border. Border.jwc is very simple:
<component-specification/>

Border.html should be like:


What title to show? Each page has its own title <html> <head></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> This is the content unique to each page. How to show the content unique to </td> each page? </tr> </table> </body> </html>

It looks like a complete page: It contains a complete <html> element. The menu is still there. However, you don't know how to show the title and the content that is unique to each page yet. To show the content unique to each page, you can use a RenderBody component:

286

Chapter 9 Providing a Common Layout

Border.html <html> <head></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> <span jwcid="@RenderBody">This is the content unique to each page.</span> </td> It will render the body of Border component. Or </tr> more accurately, it will find out its container (in </table> this case the Border component) and then </body> render the body of that container. </html>

Home.html <span jwcid="@Border"> This is the Home page. </span>

Note that you no longer need the <html> element in Home.html because it will be generated by the Border component. At the moment the page content for the Home page is just some plain text. But it could contain HTML elements or Tapestry components such as:
<span jwcid="@Border"> This is the Home page. Today is: <span jwcid="@Insert" value="today"/>. </span>

If your web designer is using a visual tool to edit Home.html, it may insist that it contains a <html> element, a <head> element and so on like:
<html> <head><title>Home</title></head> <body> <span jwcid="@Border"> This is the Home page. </span> </body> </html>

In that case you should mark the <body> element as "$content$" to retain the real content only:
<html> <head><title>Home</title></head> <body jwcid="$content$"> <span jwcid="@Border"> This is the Home page. </span> </body> </html>

Products.html and Contact.html are similar: Products.html


<html> <head><title>Products</title></head> <body jwcid="$content$"> <span jwcid="@Border"> This is the Products page. </span> </body> </html>

Contact.html
<html> <head><title>Contact</title></head> <body jwcid="$content$"> <span jwcid="@Border"> This is the Contact page. </span> </body> </html>

Now run the application and it should work:

Providing a Common Layout

287

Setting the page title


At the moment you haven't set the page title yet (it is empty):

Next, let's set it. To do that, you could modify Border.html as:
Call getPage() on the container (the Border component). Every component knows the page it is on. This method will return the page object containing the Border component. If it is the Home page, then this is the Home page object. Get the page name from the page object

<html> <head><title><span jwcid="@Insert" value="ognl:page.pageName"/></title></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home">Home</a><br> <a href="" jwcid="@PageLink" page="Products">Products</a><br> <a href="" jwcid="@PageLink" page="Contact">Contact</a> </td> <td> <span jwcid="@RenderBody">This is the content unique to each page.</span> </td> </tr> </table> </body> </html>

Now run the application again and the title should be set:

288

Chapter 9 Providing a Common Layout

Of course this method may not work for all situations. That is, you may not want to use the Tapestry page name as the page title (e.g., in an i18n application). If you need more flexibility, you can provide the title to the Border component as a parameter. To do that, modify Border.jwc:
<component-specification> <parameter name="title"/> </component-specification>

Then modify Border.html:


<html> <head><title><span jwcid="@Insert" value="ognl:title"/></title></head> ... </html>

Then provide the title in Home.html:


<html> <head><title>Home</title></head> <body jwcid="$content$"> <span jwcid="@Border" title="Home"> This is the Home page. </span> </body> </html>

Do it similarly for Products.html and Contact.html. Then run the application and it should continue to work.

Disabling the link for the current page


Usually if you are displaying the Home page, you'd like the "Home" link be disabled:

Similarly, if the current page is the Products page, you'd like the "Products" link be disabled. To do that, modify Border.html:

Providing a Common Layout

289

All the Link components such as PageLink or DirectLink in Tapestry have a "disabled" parameter. If it is true, then the link will be disabled (i.e., rendered as normal text, not a <a> element).

Compare the current page name to see if it is "Home", if yes, "disabled" is set to true so that link will be disabled. Note that you can make method calls in OGNL this way.

<html> <head><title><span jwcid="@Insert" value="ognl:title"/></title></head> <body> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home" disabled="ognl:page.pageName.equals('Home')">Home</a><br> <a href="" jwcid="@PageLink" page="Products" disabled="ognl:page.pageName.equals('Products')">Products</a><br> <a href="" jwcid="@PageLink" page="Contact" disabled="ognl:page.pageName.equals('Contact')">Contact</a> </td> <td> <span jwcid="@RenderBody">This is the page content.</span> </td> </tr> </table> </body> </html>

Now run the application and it will be like:

Using a header
Suppose that each page may need to have a particular header:

The situation is like:

290

Chapter 9 Providing a Common Layout

Page Header unique to each page Table Row Cell Menu

Extract the table structure to form a component Border component Header unique to each page

Cell Content unique to the Home page Content unique to the Home page

The question is, how to render the header in the Border component? You can use RenderBody to render the content unique to each page, but the Body component can have one body only, you can't make the header as another body. To solve this problem, the header can be wrapped in a Block in Home.html:
<html> <head><title>Home</title></head> <body jwcid="$content$"> <span jwcid="header@Block"><h1>Home header</h1></span> <span jwcid="@Border" title="Home"> This is the Home page. </span> </body> </html>

To render this block in Border.html, use a RenderBlock component:


Force a block to render What block to Get the page Get its containing the component Map render? Border Lookup the component component (block) named "header"

<html> <head><title><span jwcid="@Insert" value="ognl:title"/></title></head> <body> <span jwcid="@RenderBlock" block="ognl:page.components.header"/> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home" disabled="ognl:page.pageName.equals('Home')">Home</a><br> <a href="" jwcid="@PageLink" page="Products" disabled="ognl:page.pageName.equals('Products')">Products</a><br> <a href="" jwcid="@PageLink" page="Contact" disabled="ognl:page.pageName.equals('Contact')">Contact</a> </td> <td> <span jwcid="@RenderBody">This is the page content.</span> </td> </tr> </table> </body> </html>

Now run the application and it will be like:

Providing a Common Layout

291

If you click on "Products", no header will be shown:

This is because the OGNL expression page.components.header will return a null. If the block is null, the RenderBlock component will simply do nothing. Instead of looking up a component named "header", you could make the block a parameter of the Border component. To do that, modify Border.jwc to accept such a parameter:
<component-specification> <parameter name="title"/> <parameter name="header"/> </component-specification>

Retrieve the block in Border.html is easy:


<html> <head><title><span jwcid="@Insert" value="ognl:title"/></title></head> <body> <span jwcid="@RenderBlock" block="ognl:header"/> <table> <tr> <td width="40%"> <a href="" jwcid="@PageLink" page="Home" disabled="ognl:page.pageName.equals('Home')">Home</a><br> <a href="" jwcid="@PageLink" page="Products" disabled="ognl:page.pageName.equals('Products')">Products</a><br> <a href="" jwcid="@PageLink" page="Contact" disabled="ognl:page.pageName.equals('Contact')">Contact</a> </td> <td> <span jwcid="@RenderBody">This is the page content.</span> </td> </tr> </table> </body> </html>

Pass the header block to it in Home.html:

292

Chapter 9 Providing a Common Layout

<html> <head><title>Home</title></head> <body jwcid="$content$"> <span jwcid="header@Block"><h1>Home header</h1></span> <span jwcid="@Border" title="Home" header="component:header"> This is the Home page. </span> </body> Lookup the component </html> named "header" on this page

As you can see above, the RenderBlock component and the RenderBody component are very similar. The difference is that RenderBlock is more flexible than RenderBody. The latter always renders the body of its container, while the former can accept any block provided through component lookup or through a parameter.

Summary
If you have pages with a common layout, extract the common layer into a Border component. Then in each page you will have a Border component and put the unique page content as its body. Then your Border component should render the layout and use a RenderBody component to render its body. However, if there are say two unique areas on each page such as a unique header and a unique page content, you can put them into a Block and use two RenderBlock components in the Border component to render them. As your Border is rendering a complete page, in your page template you should use $content$ for the <body> element so that you won't output the <html> element twice. In a component you can access the surrounding page by calling getPage(). This allows you to lookup other components on that page. All link components in Tapestry can be disabled by setting their "disabled" parameters to true.

293

Chapter 10
Chapter 10

Using Javascript

294

Chapter 10 Using Javascript

What's in this chapter?


In this chapter you'll learn how to use Javascript in a Tapestry page.

Are you sure to delete it?


Suppose that you'd like to develop an application shown below:

It has a "Delete" button. When the user clicks on it, it will delete something important. Therefore, you'd like to let the user confirm first:

If he clicks "OK", you'll go ahead to perform the deletion. Otherwise you just do nothing. Now, let's do it. Create a Tapestry application called ConfirmDelete and perform the setup as usual (use /ConfirmDelete as its context path). Modify Home.html:

Using Javascript

295

<html> <body> It is defined <script type="text/javascript"> here function getConfirmation() { You put some return confirm("Are you sure?"); Javascript in } here </script> <form> <input type="submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html> This code (also Javascript) will be executed Pop up a confirmation dialog with the when the button is clicked. It is called the message "Are you sure?". If the user clicks handler for the onclick event. Here the "OK", it will return true, otherwise it will handler calls the getConfirmation() function. return false. If it returns true, the processing will continue (i.e., submit the form). Otherwise the processing will be aborted.

The type is javascript. You could use other types of scripts such as vbscript.

Up until now this has nothing to do with Tapestry. This is just plain HTML. To test it, open Home.html using a browser:

However, no matter you click "OK" or "Cancel", nothing will happen. This is because the form has no "action" yet. To test it, set the action to some non-existing URL:
<html> <body> <script type="text/javascript"> function getConfirmation() { return confirm("Are you sure?"); } </script> <form action="http://www.foo.com"> <input type="submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html>

Then after clicking "OK", it will try to go to www.foo.com:

296

Chapter 10 Using Javascript

Reusing the script


Now it is working. But suppose that you need to have the same function on another page, but you'd like to set the message to "Really delete?" instead of "Are you sure?". Of course, you could copy and paste the Javascript, but this is no good. At the moment the script is short so it is not that harmful, but in general you'd like to reuse the script as much as possible. To reuse the script, create a file Confirm.script in the same folder of the Home.html:

Input the following as its content:

Using Javascript

297

The whole .script file is an XML file. It represents a piece of Javascript to be reused.

The DTD is always like this. You can just copy it. Note that the version is 3.0, not 4.0! It is independent of the Tapestry DTD version.

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE script PUBLIC "-//Apache Software Foundation//Tapestry Script Specification 3.0//EN" "http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd"> The root element is always <script>. It contains <script> <input-symbol key="msg" required="yes"/> some <input-symbol> elements and a <body> <body> element. function getConfirmation() { return confirm("${msg}"); To insert the script into a page, you must } provide a value for the "msg" symbol because it </body> is marked as required. A symbol is just a </script> parameter. For example, you can set the value of msg to a string say abc, then ${msg} will be Put the Javascript in the <body> element. replaced with abc: This script will be reused later. function getConfirmation() { return confirm("abc"); }

The final script to be inserted into the page

To insert this script file into the Home page, modify the Home page:
Home.html <html> <body> You don't need this "static" script anymore <script type="text/javascript"> function getConfirmation() { return confirm("Are you sure?"); } Use this component to insert the confirm </script> script <span jwcid="insertConfirmScript"/> <form action="http://www.foo.com"> <input type="submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html> Home.page <page-specification class="com.ttdev.confirmdelete.Home"> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="symbols" value="symbols"/> </component> </page-specification> It is a Map containing the symbols and their values Home.java public abstract class Home extends BasePage { public Map getSymbols() { Map symbols = new HashMap(); symbols.put("msg", "Are you sure?"); return symbols; } } Set the value of the "msg" symbol to a string "Are you sure?"
<html> <body jwcid="@Body"> <span jwcid="confirmDeleteScript"/>

The component type is Script. It is provided by Tapestry. It is used to insert a .script file into the page.

Where is the .script file? "Confirm.script" is the path. It is a path relative to the specification of its container. In this case, the container is the Home page. So this path is relative to Home.page. If you'd like, you can write an absolute path instead such as /scripts/Confirm.script. In that case it is relative to the context folder (doc base), i.e., c:\workspace\ConfirmDelete\context\scripts\ Confirm.script.

As you're generating scripts, you also need a Body component to collect the scripts. So, modify Home.html:

298

Chapter 10 Using Javascript

<form action="http://www.foo.com"> <input type="Submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html>

Now run it again and it should work. The generated HTML code should be:

At the moment you're creating the Map in Java code. If you'd like, you can do it using OGNL in Home.page:
Key A colon is here Value You don't need the getSymbols() method anymore. So you don't need Home.java.

<page-specification class="com.ttdev.confirmdelete.Home"> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="symbols" value="ognl:#{'msg': 'Are you sure?'}"/> </component> </page-specification> The ognl prefix is optional because it is the default. But it is required to work around a bug in beta 4. This will create a Map. This is a feature of OGNL.

As mentioned in the diagram, you no longer need the getSymbols() method in Home.java and can delete Home.java altogether. Now run the application again and it should continue to work. In fact, there is another way to provide the Map:
<page-specification> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="symbols" value="ognl:#{'msg': 'Are you sure?'}"/> <binding name="msg" value="literal:Are you sure?"/> </component> </page-specification> The Script component knows nothing about a "msg" parameter. So it is an informal parameter. The Script component will add all its informal parameters to the Map.

Generating a unique function name


Actually there is a potential problem in the code. In Confirm.script the function name is hard coded:

Using Javascript

299

<script> <input-symbol key="msg" required="yes"/> <body> function getConfirmation() { return confirm("${msg}"); } </body> </script>

What happens if the page inserts another .script file that defines a function that is also named "getConfirmation" by chance? Then the HTML page will contain two such functions. To solve this problem, it is best to let each .script file generate a unique name for its function:
<let> will create a Here create a symbol named "funcName" and symbol or assign set its value to a string "getConfirmation". a new value to a symbol. <script> <input-symbol key="msg" required="yes"/> <let key="funcName" unique="yes">getConfirmation</let> <body> function ${funcName}() { return confirm("${msg}"); } </body> Make sure the value is unique in the Body component. In this </script> case, register the name "getConfirmation" with the Body component. The Body component will check if the name has been used. If not, the registration will be successfully and the So, it will be getConfirmation(), value of the symbol will be "getConfirmation". Otherwise, the getConfirmation$0(), Body component will append a sequence number to the name getConfirmation$1() or something such as "getConfirmation$0" or "getConfirmation$1" until it similar. finds one that is unused.

As the function name is generated dynamically, how can the onclick event handler call it?
<html> <body jwcid="@Body"> <span jwcid="insertConfirmScript"/> <form action="http://www.foo.com"> <input type="Submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html>

The solution is to let the .script file generate the event handler for it. It is hoped that the generated HTML code will be like:

300

Chapter 10 Using Javascript

<hmtl> <body> <script> Assign XXX as the function XXX() { onclick event return confirm("Are you sure?"); handler for that } button </script> <form action="http://www.foo.com"> <input type="submit" value="Delete" id="button1"/> </form> </body> </hmtl>

The name that is generated dynamically

Executed this script after the HTML page has been loaded so that all the HTML elements such as form1 and button1 have been created in the browser.

document.getElementById("button1").onclick=XXX;

To implement this idea, modify Confirm.script:


You must pass the Submit component to it <script> so that you can find out the HTML element <input-symbol key="msg" required="yes"/> id of it <input-symbol key="button" required="yes"/> <let key="funcName" unique="yes">getConfirmation</let> <body> function ${funcName}() { Assign the function name return confirm("${msg}"); generated to the onclick } event </body> Everything inside the <initialization> <initialization> will be document.getElementById("${button.clientId}").onclick=${funcName}; executed after the </initialization> whole HTML page has </script> been loaded Call getClientId() on the Submit component. When the Submit component renders itself as a <input> element, it will generate a unique id as the HTML element id for that <input> element and remember that. Calling getClientId() will retrieve that id.

For this to work, the button must be Submit component which must be contained in a Form component:

Using Javascript

301

Home.html <html> <body jwcid="@Body"> <span jwcid="insertConfirmScript"/> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@Submit" type="submit" value="Delete" onclick="return getConfirmation()"/> </form> </body> </html> You don't need this anymore Home.page <page-specification> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="msg" value="literal:Are you sure?"/> <binding name="button" value="component:deleteButton"/> </component> </page-specification>

Must provide the Submit component as a symbol so that the .script file can find out the names for the <form> and for the <input> elements

Refer to the Submit component. That's why it must have a component id.

However, this won't work yet because when the "insertConfirmScript" component is rendered, it will try to insert the . script file, but at that time the Form component and the Submit component haven't rendered themselves yet and haven't set their names yet. So, you must put the "insertConfirmScript" component after the Submit component:
<html> <body jwcid="@Body"> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@Submit" type="submit" value="Delete"/> <span jwcid="insertConfirmScript"/> </form> </body> </html> Put it after the Submit component

Finally, whenever you have scripts to execute after the HTML page is completely loaded (that is, scripts inside <initialization>), Tapestry will make use of the dojo library to achieve this effect. Therefore, you need a Shell component to bring in dojo:
<html jwcid="@Shell" title="Confirm Delete"> <body jwcid="@Body"> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@Submit" type="submit" value="Delete"/> <span jwcid="insertConfirmScript"/> </form> </body> </html>

Now run the application and it should be working. The generated HTML code is like:

302

Chapter 10 Using Javascript

Using dojo to execute the script on page load

As you can see above, the function name and the id of the <input> are generated automatically.

Encapsulating the use of scripts in a component


While it is working fine, you'd like to hide the use of scripts in your code. For example, in Home.html you still see traces of Javascript:
<html> <body jwcid="@Body"> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@Submit" type="submit" value="Delete"/> <span jwcid="insertConfirmScript"/> </form> </body> </html>

This is not very good. A better way is to create a ConfirmButton component so that you could use it this way:

Using Javascript

303

It's no longer a Submit component, This is a (formal) parameter for the but a ConfirmButton. ConfirmButton component <html> <body jwcid="@Body"> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@ConfirmButton" msg="Are you sure?" type="submit" value="Delete"/> <span jwcid="insertConfirmScript"/> </form> </body> The Home page doesn't need to </html> insert any script itself. The ConfirmButton will it do for you. <page-specification> <component id="insertConfirmScript" type="Script"> <binding name="script" value="Confirm.script"/> <binding name="msg" value="literal:Are you sure?"/> <binding name="button" value="components.deleteButton"/> </component> </page-specification>

OK, let's do it. Create such a ConfirmButton component. It should be like:


ConfirmButton.html <html> <body jwcid="$content$"> <input jwcid="button" type="submit"/> <span jwcid="insertConfirmScript"/> </body> </html> It shouldn't have a body Insert the .script file after the Submit component is rendered

ConfirmButton.jwc <component-specification allow-body="no" allow-informal-parameters="yes"> <parameter name="msg" required="yes"/> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="msg" value="msg"/> <binding name="button" value="component:button"/> </component> <component id="button" type="Submit"/> </component-specification>

The caller must specify the "msg" parameter Insert this .script file. This path is relative to its container. Now its container is ConfirmButton. So it is relative to ConfirmButton.jwc.

Read the "msg" parameter

Tell the .script file the button so that it can attach the event handler to it

Now run the application and it should work:

However, the button name has been changed to "Submit Query" from "Delete". Why?

304

Chapter 10 Using Javascript

Home.html <html> <body jwcid="@Body"> <form jwcid="@Form" action="http://www.foo.com"> <input jwcid="deleteButton@ConfirmButton" msg="Are you sure?" type="submit" value="Delete"/> </form> </body> </html> You're specifying the label, but it is lost. This is because it is now an informal parameter for the ConfirmButton component, but the latter is ignoring its informal parameters. This informal parameter is not passed to the Submit component:

No label is specified ConfirmButton.html <html> <body jwcid="$content$"> <input jwcid="button" type="submit"/> <span jwcid="insertConfirmScript"/> </body> </html>

To solve this problem, you can pass all informal parameters of your ConfirmButton component to the Submit component:
ConfirmButton.jwc <component-specification allow-body="no" allow-informal-parameters="yes"> <parameter name="msg" required="yes"/> <component id="insertConfirmScript" type="Script"> <binding name="script" value="literal:Confirm.script"/> <binding name="msg" value="msg"/> <binding name="button" value="component:button"/> </component> <component id="button" type="Submit" inherit-informal-parameters="yes"/> </component-specification>

It will get a copy of all the informal parameters of this ConfirmButton component

Now run the application again and it should work:

This feature is very handy when your custom component is wrapping around a "primary" component (a button, a form and etc.) inside and you'd like to allow others to set the HTML attributes of that primary component.

Summary
To insert some Javascript into the HTML code, you put your script into a .script file and use a Script component to insert it. The power of this is that the .script file accepts symbols at runtime and it evaluates expressions like ${symbol. property} in the script and replace them with the values. This allows the script to come alive and be customized heavily (or adapted to the current page). To provide values for the symbols, you can use OGNL to create a Map and bind it to the "symbols" parameter of the Script component. Or you can specify each symbol as an informal parameter. Make

Using Javascript

305

sure you have a Body component whenever you need to insert any Javascript. Commonly if you have a Javascript function in the script, you probably should generate a unique name for it to avoid potential crashes. You can do that by marking the symbol as unique. To associate such a function to an HTML element, as the name is determined at runtime you can't do it in the HTML file. Instead, you will associate them after the HTML page is completely loaded. To do that, put the code assigning the event handler in the <initialization> section of the .script file. You're advised against using the Script component directly in your page. Instead, encapsulate its use in a component so that your pages don't see any traces of Javascript. Instead, it only sees a highly interactive component (like a DatePicker).

307

Chapter 11
Chapter 11

Building Interactive Forms with AJAX

308

Chapter 11 Building Interactive Forms with AJAX

What's in this chapter?


In this chapter you'll learn how to build forms that are more interactive than normal HTML forms using a technique called AJAX.

A sample AJAX application


Suppose that you'd like to develop an application that allows the user to edit the data of some customers. The main page is like:

To edit a single customer, the user can choose "Load a customer", then he will see the page below. He can type the initial part of the name of the customer. For example, if he types "P", all the customers whose names start with "P" will be listed (eg., Paul, Peter). Then he can choose one from the list and finally click "Load" to edit that customer.

On the other hand, instead of editing a single customer, he can choose to edit all the customers in one go. To do that, he can click "Edit all customers" on the main page and then he will see the page below. As you can see, each customer has a name, a country and city.

If the user clicks "Edit" on a particular row, that row will become a form for editing, while the rest of the page will remain unchanged and is not refreshed at all:

Building Interactive Forms with AJAX

309

These parts are not redraw/refreshed at all

The user can then edit the customer's data and then click "Post". Or he can click "Cancel" to cancel the changes. No matter which is the case, the form will become a table row again and only that row is refreshed:

To make it more interesting, when editing a customer, if the user changes the country say from US to China, then the list of cities will be refreshed to show the cities in China automatically. Again, only this row is refreshed: Choosing China as the country The list of cities will be refreshed

310

Chapter 11 Building Interactive Forms with AJAX

In addition to editing a customer, the user can delete it by clicking "Delete" and then the row will disappear: Clicking "Delete" for Mary2 The row will disappear

Also, the user can add a new customer by clicking "Add" at the bottom, then a new row will be added to allow him to enter the data:

Finally, the changes the user has made so far are stored in memory only. To commit the changes to the database, he needs to click "Commit" at the bottom.

Creating the Home page


Now, let's do it. Create a Tapestry application called AJAXApp and set it up as usual (use /AJAXApp its context path). Modify Home.html as:
<html> <a href="" jwcid="@PageLink" page="LoadCustomer">Load a customer</a><br> <a href="" jwcid="@PageLink" page="CustomersCRUD">Edit all customers</a> </html>

Home.page is:
<page-specification/>

You'll create the LoadCustomer page and CustomersCRUD page loader. Now run it and it will be like:

Building Interactive Forms with AJAX

311

Loading a single customer


Let's create the LoadCustomer page that can load a single customer. LoadCustomer.html is like:
<html> <form jwcid="form"> <input type="text" jwcid="customer"/> <input type="submit" value="Load"/> </form> </html>

LoadCustomer.page is show below. The most important part is the Autocompleter component which is used to let the user enter a partial customer name. It is very much like a PropertySelection. On render, it will evaluate its "value" parameter to find the object to display. Suppose that it is the Customer object 1 (Paul). Then it will ask the IAutocompleteModel for the primary key and display label for Customer object 1. In this case, they're 001 and "Paul" respectively. Then it will output them as something like an HTML <select> element. The label is used for display. The primary key is used to uniquely identify each entry.
<page-specification class="com.ttdev.ajaxapp.LoadCustomer"> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="customer" type="Autocompleter"> <binding name="value" value="customer"/> <binding name="model" value="customerAutoCompleteModel"/> </component> </page-specification> 1: Evaluate its "value" parameter. Suppose it is Customer obj 1.

AutoCompleter
2: What are the primary key and display label for Customer obj 1?

4: Output something like this

<select> <option value="001" selected="selected">Paul</option> </select>

3: The primary key is 001 and the display label is "Paul"

IAutoCompleteModel
id: 001 name: Paul Customer obj 1 Customer obj 2 Customer obj 3 id: 007 name: Peter id: 005 name: Mary

The interesting point of the Autocompleter is that whenever the user types something such as "P" into that HTML

312

Chapter 11 Building Interactive Forms with AJAX

element in the browser (see the diagram below), it will run some Javascript to contact the Autocompleter component on the server to ask for options matching "P". The Autocompleter in turn will ask the IAutocompleteModel for Customer objects that match "P". What is concerned a match is completely determined by the IAutocompleteModel. For example, it could return Customer objects whose names start with "P" or whose names contain "P" or whatever. Suppose that it is the former case. So it will return Customer object 1 (Paul) and Customer object 3 (Peter). The Autocompleter will ask the IAutocompleteModel to translate each such Customer object into its primary key and display label. Finally, it will transfer this information back to the Javascript in the HTML element in the browser. For each match (a pair of primary key and display label), it will create something an <option>-like element and add it to the <select>-like element. All this is happening without refreshing the HTML page. Only the <select>-like element is refreshed.
1: The user types so far is "P". Give me the <select>...</select> matches. 6: They are (001, "Paul") and (007, "Peter") respectively 2: Give me the a list of Customer objects who match "P". 7: Set two <option> elements into itself: 3: Here you are: { Customer obj 1, Customer obj 2} 4: What are the primary keys and display labels for Customer obj 1 and obj 2? 5: They are (001, "Paul") and (007, "Peter") respectively

AutoCompleter

<option value="001">Paul</option> <option value="007">Peter</option>

IAutoCompleteModel
id: 001 name: Paul Customer obj 1 Customer obj 2 Customer obj 3 id: 007 name: Peter id: 005 name: Mary

On rewind, the Autocompleter component will receive the primary key. Then it will ask the IAutocompleteModel to translate it back into a Customer object, before storing it into its "value" parameter. Now, let's create LoadCustomer.java to provide the IAutocompleteModel:

Building Interactive Forms with AJAX

313

package com.ttdev.ajaxapp; public abstract class LoadCustomer extends BasePage { public abstract Customer getCustomer(); Given the primary key, find out the object. @InjectState("customerDB") public abstract CustomerDB getCustomerDB(); public IAutocompleteModel getCustomerAutoCompleteModel() { return new IAutocompleteModel() { public Object getValue(Object primaryKey) { return getCustomerDB().selectById(((Integer) primaryKey).intValue()); } Given the object, find public Object getPrimaryKey(Object value) { out its primary key. return ((Customer) value).getId(); Find out the objects whose labels } match the "filter". The filter is the public List getValues(String filter) { user input so far. String nameKeyword = filter; return getCustomerDB().selectByKeyword(nameKeyword); } public String getLabelFor(Object value) { return ((Customer) value).getName(); } Given the object, find out its label. }; } public void onOk() { System.out.println("Loading customer " + getCustomer().toString()); } }

The Customer class is:


public class Customer { private int id; private String name; private String country; private String city; public Customer(int id, String name, String country, String city) { this.id = id; this.name = name; this.country = country; this.city = city; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } @Override public String toString() { return "Customer[" + name + ", " + country + ", " + city + "]"; } }

314

Chapter 11 Building Interactive Forms with AJAX

The CustomerDB class emulates a database:


public class CustomerDB { public List customers; public CustomerDB() { customers = new ArrayList(); customers.add(new Customer(0, "Paul", "US", "New York")); customers.add(new Customer(1, "Peter", "China", "Shanghai")); customers.add(new Customer(2, "Mary", "US", "Boston")); customers.add(new Customer(3, "Mike", "US", "Chicago")); customers.add(new Customer(4, "Mitchell", "China", "Beijing")); } public Customer selectById(int customerId) { for (Iterator iter = customers.iterator(); iter.hasNext();) { Customer customer = (Customer) iter.next(); if (customer.getId() == customerId) { return customer; } } return null; } public List selectByKeyword(String nameKeyword) { List matches = new ArrayList(); for (Iterator iter = customers.iterator(); iter.hasNext();) { Customer customer = (Customer) iter.next(); if (customer.getName().toLowerCase().startsWith( nameKeyword.toLowerCase())) { matches.add(customer); } } return matches; } }

You'd like to create a single instance of CustomerDB to present the database. To do that, create METAINF/hivemodule.xml in the "src" folder:
Application scope. It means only one for the whole application. <?xml version="1.0"?> <module id="com.ttdev.ajaxapp" version="1.0.0"> <contribution configuration-id="tapestry.state.ApplicationObjects"> <state-object name="customerDB" scope="application"> <create-instance class="com.ttdev.ajaxapp.CustomerDB" /> </state-object> </contribution> </module> Create an instance of the com.ttdev.ajaxapp.CustomerDB as the state object

Reload the application to make sure the hivemodule.xml file is read. If you run the application now, you will see the Autocomplete component, but it won't do anything. This is because it uses Javascript and thus needs a Shell component and a Body component:
<html jwcid="@Shell" title="Load Customer"> <body jwcid="@Body"> <form jwcid="form"> <input type="text" jwcid="customer"/> <input type="submit" value="Load"/> </form> </body> </html>

Now run the application. Enter something like "p" and you'll see that it is working:

Building Interactive Forms with AJAX

315

BUG ALERT: If you enter something like "x" so that no match is found, you'll some debug output:

This is a bug in the dojo library. As this is harmless, you may want to disable this kind of Javascript debug messages by setting a parameter of the Shell component:
<html jwcid="@Shell" title="Load Customer" debugEnabled="false"> <body jwcid="@Body"> <form jwcid="form"> <input type="text" jwcid="customer"/> <input type="submit" value="Load"/> </form> </body> </html>

Run it again and you won't see those messages again. You may wonder what happens if the user enters something not on the list such as "abc" and then submit the form? In that case, the Autocompleter will set the "value" parameter to null. If you'd like to insist that the input is valid (non-null), you can use the regular validators. In LoadCustomer.html:
<html jwcid="@Shell" title="Load Customer" debugEnabled="false"> <body jwcid="@Body"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> <form jwcid="form"> <input type="text" jwcid="customer"/> <input type="submit" value="Load"/> </form> </body> </html>

In LoadCustomer.page:
<page-specification class="com.ttdev.ajaxapp.LoadCustomer"> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> <binding name="delegate" value="bean:delegate"/> </component> <component id="customer" type="Autocompleter"> <binding name="value" value="customer"/> <binding name="model" value="customerAutoCompleteModel"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="literal:Customer"/> </component> </page-specification>

In LoadCustomer.java:
public abstract class LoadCustomer extends BasePage {

316

Chapter 11 Building Interactive Forms with AJAX

public abstract Customer getCustomer(); ... @Bean public abstract ValidationDelegate getDelegate(); public void onOk() { if (getDelegate().getHasErrors()) { return; } System.out.println("Loading customer " + getCustomer().toString()); } }

Run it, enter "abc" and click "Load". It will work:

Using a DataSet to store the Customer objects


Next, let's work on the CustomersCRUD page to allow the editing of all the customers. You'll load all the Customer objects from the database into memory . This will serve as a local copy of the data. After you finish making changes to it, you can write it back the database ("commit"). The idea is that it is like using Word to edit a file: You load the whole file into memory for editing, make changes to the copy in memory and finally saves it back to disk. You'll load the Customer objects into a table as shown below. Let's call it a DataSet. Each entry in the DataSet has four attributes: value (a Customer object), a temp value (Initially it is null. On edit, make a copy of the value and store it here to be modified by the user. On post, it will be stored into the value. On Cancel, it will be thrown away), action (how to save the value to the database) and state (is the value being edited, being viewed or hidden). Initially the action for all the entries is "no op" (meaning that on commit you don't need to do anything to the database). The state for them is "viewing" (meaning that they're just being viewed, not being edited): id: 001 name: Paul
Value

id: 005 name: Mary

Temp null null null

Action No Op No Op No Op

State View ing View ing View ing

id: 007 name: Peter If the user clicks "Edit" to edit a particular entry (say, the entry for Mary), then the state will change from "viewing" to "editing" and the value is clone: id: 005 name: Mary id: 001 name: Paul
Value

id: 005 name: Mary

Temp null null

Action No Op No Op No Op

State View ing Editing View ing

id: 007 name: Peter During user interaction, the user may change the name from "Mary" to "Judy". The change should be made to the clone, not the value:

Building Interactive Forms with AJAX

317

id: 005 name: Judy id: 001 name: Paul


Value

id: 005 name: Mary

Temp null null

Action No Op No Op No Op

State View ing Editing View ing

id: 007 name: Peter When the user clicks "Post", the clone will be stored into the value, the temp will be set to null, the state will change from "editing" to "viewing" and the action will change from "no op" to "update" (meaning that commit, you'll need to issue an update statement to the database): id: 005 name: Judy id: 001 name: Paul
Value

id: 005 name: Mary

Temp null null null

Action No Op Update No Op

State View ing View ing View ing

id: 007 name: Peter If the user chooses "Cancel" instead of "Post", the clone will be thrown away, the temp will be set to null, the state will still change from "editing" to "viewing", but the value and the action will remain unchanged ("no op"): id: 005 name: Judy id: 001 name: Paul
Value

id: 005 name: Mary

Temp null null null

Action No Op No Op No Op

State View ing View ing View ing

id: 007 name: Peter If the user clicks "Add", a new Customer object with default data and a new entry will be added. The temp is not used and will remain as null. The action will be set to "no op". The state will be set to "creating": id: 001 name: Paul id: 005 name: Mary Lou id: 007 name: Peter
Value Temp null null null null Action No Op Update No Op No Op State View ing View ing View ing Creating

id: ??? name: <some default value> The state of "creating" is similar to "editing" in that they both mean that the value is being edited by the UI. The first difference is that, the former will display the value to the UI, while the latter will display the temp value instead. The second difference is, they will set the action differently on posting or canceling. When an entry is in the "creating" state, on posting, the action will be set to "insert" (meaning that on commit, you'll need to issue an insert statement to the database) and the state will be set to "viewing". If later it is edited and posted again, the action will NOT be set to "update". Instead, it will remain as "insert":

318

Chapter 11 Building Interactive Forms with AJAX

id: 001 name: Paul id: 005 name: Mary Lou id: 007 name: Peter
Value Temp null null null null Action No Op Update No Op Insert State View ing View ing View ing View ing

id: ??? name: <some value> On canceling, the action will be set to "no op", the state will be set to "hidden" and the value will be set to null: id: 001 name: Paul id: 005 null No Op View ing name: Mary null Update View ing Lou null No Op View ing null null No Op Hidden id: 007 name: Peter Note that for the case of canceling, having such an entry in the table has absolutely no effect. It won't be shown in the UI ("hidden") and it won't cause any change to the database ("no op"). Why not delete it from the table? Whenever you're changing the number of entries in the DataSet, you're changing the number of rows in the HTML table. As a result, refreshing the current row is not enough; you must refresh the whole page. To avoid that, it's better to keep the entry around. If the user clicks "Delete" on a row, if the current action is "no op" or "update" (meaning that there is a corresponding record in the database), the action will be changed to "delete" (meaning that on commit, you'll need to issue an delete statement to the database). If the current action is "insert" (meaning that the data is new and there is no corresponding record in the database), the action will be changed to "no op". For the latter case, as the value is now useless, it will be set to null. In both cases the state will be set to "hidden" (meaning that it is not to be shown in the UI): Before delete Current action is "no op" or "update"
id: 001 name: Paul
Value

Value

Temp

Action

State

After delete
id: 001 name: Paul

id: 005 name: Mary id: 007 name: Peter

Temp null null null

Action No Op No Op No Op

State View ing View ing View ing

Value

id: 005 name: Mary id: 007 name: Peter id: 001 name: Paul

Temp null null null

Action No Op No Op Delete

State View ing View ing Hidden

Current id: 001 action is name: Paul "insert"

id: 005 name: Mary Lou id: 007 name: Peter id: ??? name: <some value>

Value

Temp null null null null

Action No Op Update No Op Insert

State View ing View ing View ing View ing

id: 005 name: Mary Lou id: 007 name: Peter

Value

null

Temp null null null null

Action No Op Update No Op No Op

State View ing View ing View ing Hidden

To implement such a DataSet, create DataSet.java:


public class DataSet { private List entries; public DataSet(List initialValues) {

Building Interactive Forms with AJAX

319

entries = new ArrayList(); for (Iterator iter = initialValues.iterator(); iter.hasNext();) { Object value = (Object) iter.next(); entries.add(new DataEntry(value, false)); } } public List getEntries() { return entries; } public void edit(int idx, ObjectCloner cloner) { DataEntry entry = getEntryAt(idx); entry.edit(cloner); } public void post(int idx) { DataEntry entry = getEntryAt(idx); entry.post(); } public void cancel(int idx) { DataEntry entry = getEntryAt(idx); entry.cancel(); } public void delete(int idx) { DataEntry entry = getEntryAt(idx); entry.delete(); } public DataEntry getEntryAt(int idx) { return (DataEntry) entries.get(idx); } public int countEntries() { return entries.size(); } public Object getValueAt(int idx) { return getEntryAt(idx).getValue(); } public void add(Object newValue) { DataEntry entry = new DataEntry(newValue, true); entries.add(entry); } }

Create DataEntry.java to represent each entry in the DataSet:


public class DataEntry public static final public static final public static final public static final public public public public static static static static final final final final { int int int int int int int int ACTION_NOOP = ACTION_UPDATE ACTION_INSERT ACTION_DELETE 0; = 1; = 2; = 3;

STATE_VIEWING = 0; STATE_EDITING = 1; STATE_CREATING = 2; STATE_HIDDEN = 3;

private private private private

Object value; Object tempValue; int action; int state;

public DataEntry(Object value, boolean isNew) { this.value = value; action = ACTION_NOOP; if (isNew) { state = STATE_CREATING; } else { state = STATE_VIEWING; } } public Object getValue() { if (state == STATE_EDITING) { return tempValue; } else { return value; } } public void setValue(Object value) {

320

Chapter 11 Building Interactive Forms with AJAX

if (state == STATE_EDITING) { this.tempValue = value; } else { this.value = value; } } public boolean isInteracting() { return isEditing() || isCreating(); } public boolean isEditing() { return state == STATE_EDITING; } public boolean isCreating() { return state == STATE_CREATING; } public boolean isViewing() { return state == STATE_VIEWING; } public boolean isHidden() { return state == STATE_HIDDEN; } public void edit(ObjectCloner cloner) { if (state == STATE_VIEWING) { state = STATE_EDITING; tempValue = cloner.clone(value); } else { throw new IllegalStateException(); } } public void post() { switch (state) { case STATE_EDITING: switch (action) { case ACTION_NOOP: action = ACTION_UPDATE; break; case ACTION_UPDATE: break; case ACTION_INSERT: break; default: throw new IllegalStateException(); } value = tempValue; tempValue = null; state = STATE_VIEWING; break; case STATE_CREATING: action = ACTION_INSERT; state = STATE_VIEWING; break; default: throw new IllegalStateException(); } } public void cancel() { switch (state) { case STATE_EDITING: tempValue = null; state = STATE_VIEWING; break; case STATE_CREATING: state = STATE_HIDDEN; value = null; break; default: throw new IllegalStateException(); } } public void delete() { state = STATE_HIDDEN; switch (action) { case ACTION_NOOP: case ACTION_UPDATE:

Building Interactive Forms with AJAX

321

action = ACTION_DELETE; break; case ACTION_INSERT: action = ACTION_NOOP; value = null; break; default: throw new IllegalStateException(); } } public int getAction() { return action; } }

Note that getValue() and setValue() will access the temp value in editing state and access the value in other states. This way, you can just call them without worrying about which one is access. ObjectCloner is used to clone the value. The source code is:
public interface ObjectCloner { Object clone(Object value); }

To implement the commit functionality:


You'll need to provide a CommitResolver to actually perform each insert, delete or update for each changed entry. public void void void } interface CommitResolver { insert(Object value); delete(Object value); update(Object value);

public class DataSet { private List entries; ... public void commit(CommitResolver resolver) { performActionOnEntries(resolver, DataEntry.ACTION_DELETE); performActionOnEntries(resolver, DataEntry.ACTION_UPDATE); performActionOnEntries(resolver, DataEntry.ACTION_INSERT);

Delete those entries that should be deleted. Then update those that should be updated. Finally, insert those that should be inserted.

} private void performActionOnEntries(CommitResolver resolver, int action) { for (Iterator iter = entries.iterator(); iter.hasNext();) { DataEntry entry = (DataEntry) iter.next(); if (entry.getAction() == action) { entry.perform(resolver); } } } }

public class DataEntry { ... public void perform(CommitResolver resolver) { switch (action) { case ACTION_INSERT: resolver.insert(value); break; case ACTION_DELETE: resolver.delete(value); break; case ACTION_UPDATE: resolver.update(value); break; After being committed, } set the action to "no action = ACTION_NOOP; op". } }

Listing all the customers


Now, let's list all the customers. Create CustomersCRUD.html:
<html> <table border="1">

322

Chapter 11 Building Interactive Forms with AJAX

<tr jwcid="customersLoop"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </tr> </table> </html>

CustomersCRUD.page is:
<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> This will be a list of DataEntry <component id="customersLoop" type="For"> objects <binding name="source" value="customerEntries"/> <binding name="value" value="customerEntry"/> Set the current entry and the <binding name="index" value="idx"/> current index for easy access </component> <component id="showCustomerName" type="Insert"> <binding name="value" value="customer.name"/> Need to provide a getCustomer() method in the <binding name="renderTag" value="true"/> page class. </component> <component id="showCountry" type="Insert"> <binding name="value" value="customer.country"/> <binding name="renderTag" value="true"/> </component> <component id="showCity" type="Insert"> <binding name="value" value="customer.city"/> By default, the Insert <binding name="renderTag" value="true"/> component will not render </component> itself as an HTML element. <component id="edit" type="DirectLink"> But here you'd like it to render <binding name="listener" value="listener:onEdit"/> as a <td>: <binding name="parameters" value="idx"/> </component> <component id="delete" type="DirectLink"> <binding name="listener" value="listener:onDelete"/> <binding name="parameters" value="idx"/> </component> </page-specification> Provide the row index to the listener methods so that they know which entry to edit/delete <html> <table border="1"> <tr jwcid="customersLoop"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> ... </tr> </table> </html>

CustomersCRUD.java is:

Building Interactive Forms with AJAX

323

public abstract class CustomersCRUD extends BasePage implements PageBeginRenderListener { Store the DataSet into the session. As it may contain @Persist("session") quite a lot of data, it is no good to store it on the client public abstract DataSet getDataSet(); (which is also bad for security as a hacker may modify public abstract void setDataSet(DataSet ds); the DataSet to modify records in the database otherwise he has no access to). @InjectState("customerDB") public abstract CustomerDB getCustomerDB(); public void pageBeginRender(PageEvent event) { if (getDataSet() == null) { If no DataSet yet, create one and setDataSet(new DataSet(getCustomerDB().selectAll())); load all the customers from the } database. } public abstract DataEntry getCustomerEntry(); public abstract int getIdx(); Provide getCustomer() so that components can write OGNL public List getCustomerEntries() { expressions like "customer.name" return getDataSet().getEntries(); } public Customer getCustomer() { return (Customer) getCustomerEntry().getValue(); } public void onEdit(int idx) { System.out.println("Editing customer at " + idx); } public void onDelete(int idx) { System.out.println("Deleting customer at " + idx); } }

Provide the selectAll() method in CustomerDB.java:


public class CustomerDB { public List customers; public CustomerDB() { customers = new ArrayList(); customers.add(new Customer(0, "Paul", "US", "New York")); customers.add(new Customer(1, "Peter", "China", "Shanghai")); customers.add(new Customer(2, "Mary", "US", "Boston")); customers.add(new Customer(3, "Mike", "US", "Chicago")); customers.add(new Customer(4, "Mitchell", "China", "Beijing")); } public Customer selectById(int customerId) { for (Iterator iter = customers.iterator(); iter.hasNext();) { Customer customer = (Customer) iter.next(); if (customer.getId() == customerId) { return customer; } } return null; } public List selectByKeyword(String nameKeyword) { List matches = new ArrayList(); for (Iterator iter = customers.iterator(); iter.hasNext();) { Customer customer = (Customer) iter.next(); if (customer.getName().toLowerCase().startsWith( nameKeyword.toLowerCase())) { matches.add(customer); } } return matches; } public List selectAll() { return customers; } }

As you're storing the DataSet into the session, DataSet, DataEntry and Customer need to implement Serializable:
public class DataSet implements Serializable { private List entries; ... } public class DataEntry implements Serializable {

324

Chapter 11 Building Interactive Forms with AJAX

private Object value; //It will hold a Customer object private int action; private int state; ... } public class Customer implements Serializable { private int id; private String name; private String country; private String city; ... }

Now run the application and it should work:

Click "Edit" or "Delete" and you'll see the output in the console:

Implementing the Edit function


To implement the Edit function, in the onEdit() method (see the diagram below), you need to put the DataEntry into "editing" state. Then when the page is rendered, for that DataEntry, you will note that it is in "editing" state and thus display a form using a Form, a TextField and other form components. For the other entries, they are in the "viewing" state, so you will output the data using Insert components.

Building Interactive Forms with AJAX

325

id: 005 name: Mary id: 001 name: Paul


Value

id: 005 name: Mary id: 007 name: Peter

Temp null null

Action No Op No Op No Op

State View ing Editing View ing

2: On render, note that it is in "viewing" state, output this entry using some Insert components. 3: Note that it is in "editing" state, output it using a Form component, a TextField component and etc.

1: In onEdit(), put it into "editing" state and a clone is made and stored into the temp.

<tr> <td>Paul</td> <td>US</td> <td>New York</td> <td>...</td> </tr>

<tr> <td colspan="4"> <form> <input type="text" value="Mary"/> <select name="country"> <option value="US" selected="selected"/> <option value="China"/> </select> <select name="city"> <option value="New York"/> <option value="Boston"/> </select> <input type="Submit" value="Post"/> <input type="Submit" value="Cancel"/> <form> </td> </tr>

Note that the whole page will be refreshed. This is OK for the moment. You'll enhance it later to just refresh the changed row. Now, let's modify CustomersCRUD.html to implement this idea:
<html> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="text" jwcid="customerName"/> <select jwcid="country""></select> <select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span> </tr> </table> </html>

Define the components in CustomersCRUD.page:


<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> <component id="customersLoop" type="For"> <binding name="source" value="customerEntries"/> <binding name="value" value="customerEntry"/> <binding name="index" value="idx"/> </component> <component id="ifInteracting" type="If"> <binding name="condition" value="customerEntry.interacting"/> <binding name="renderTag" value="false"/> </component> <component id="form" type="Form"> </component> <component id="customerName" type="TextField"> <binding name="value" value="customer.name"/> </component>

326

Chapter 11 Building Interactive Forms with AJAX

<component id="country" type="PropertySelection"> <binding name="model" value="countries"/> <binding name="value" value="customer.country"/> </component> <component id="city" type="PropertySelection"> <binding name="model" value="cities"/> <binding name="value" value="customer.city"/> </component> <component id="post" type="Submit"> <binding name="listener" value="listener:onPost"/> </component> <component id="cancel" type="Submit"> <binding name="listener" value="listener:onCancel"/> </component> <component id="ifViewing" type="If"> <binding name="condition" value="customerEntry.viewing"/> <binding name="renderTag" value="false"/> </component> <component id="showCustomerName" type="Insert"> <binding name="value" value="customer.name"/> <binding name="renderTag" value="true"/> </component> <component id="showCountry" type="Insert"> <binding name="value" value="customer.country"/> <binding name="renderTag" value="true"/> </component> <component id="showCity" type="Insert"> <binding name="value" value="customer.city"/> <binding name="renderTag" value="true"/> </component> <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="idx"/> </component> <component id="delete" type="DirectLink"> <binding name="listener" value="listener:onDelete"/> <binding name="parameters" value="idx"/> </component> </page-specification>

Note that fragment:


<component id="ifInteracting" type="If"> <binding name="condition" value="customerEntry.interacting"/> <binding name="renderTag" value="false"/> </component>

It will render its body (show the Form in its body) if DataEntry.isInteracting() returns true. That method will return true if the entry is in "editing" or "creating":
public class DataEntry implements Serializable { ... public boolean isInteracting() { return isEditing() || isCreating(); } public boolean isEditing() { return state == STATE_EDITING; } public boolean isCreating() { return state == STATE_CREATING; } }

In addition, you must tell set the "renderTag" parameter to false. Otherwise, it will generate an extra <span> tag between the <tr> tag and <td> tag:

Building Interactive Forms with AJAX

327

<html> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> ... </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td>...</td> </span> </tr> </table> </html>

If renderTag is true (the default), it will generate an extra <span> between <tr> and <td>. <html> <table border="1"> <tr> <span> <td colspan="4"> <form> ... </form> </td> </span> </tr> <tr> <span> <td>Paul</td> <td>US</td> <td>New York</td> <td>...</td> </span> </tr> </table> </html>

Finally, modify CustomersCRUD.java:


public abstract class CustomersCRUD ... { ... @Persist("session") public abstract DataSet getDataSet(); public abstract DataEntry getCustomerEntry(); public abstract int getIdx(); They're set by the For component ("customersLoop")

public Customer getCustomer() { return (Customer) getCustomerEntry().getValue(); } public void onEdit(int idx) { System.out.println("Editing customer at " + idx); Put the entry into "editing" getDataSet().edit(idx, new ObjectCloner() { state. Provide a cloner so public Object clone(Object value) { that it can clone the return ((Customer)value).makeClone(); Customer object. } }); } The user just clicked "Post". So post the public void onPost() { entry at the index. But, has the "idx" getDataSet().post(getIdx()); property been set? } The user just clicked "Cancel". So cancel the public void onCancel() { changes for the entry at the index. But, has getDataSet().cancel(getIdx()); the "idx" property been set? } public IPropertySelectionModel getCountries() { return new StringPropertySelectionModel(new String[] { "US", "China" }); } public IPropertySelectionModel getCities() { if (getCustomer().getCountry().equals("US")) { return new StringPropertySelectionModel(new String[] { "New York", "Boston", "Chicago" }); } else { return new StringPropertySelectionModel(new String[] { "Beijing", "Shanghai" }); } } } 1.1: Point to it

Provide the makeClone() method in Customer.java:


public class Customer implements Serializable { private int id; private String name; private String country;

328

Chapter 11 Building Interactive Forms with AJAX

private String city; ... public Customer makeClone() { return new Customer(id, name, country, city); } }

Now run it and try to edit a customer:

It will seem to be working. However, when you click "Post" or "Cancel", it will crash with a NullPointerException:

The problem is that on rewind, only the Form and the components in it are rewound. Everything outside the Form is not. It means the "customersLoop" is not rewound. Therefore, nobody is setting the "idx" and "customerEntry" properties. When your form components tries to store the values:
<component id="customerName" type="TextField"> <binding name="value" value="customer.name"/> </component> <component id="country" type="PropertySelection"> <binding name="model" value="countries"/> <binding name="value" value="customer.country"/> </component> <component id="city" type="PropertySelection">

Building Interactive Forms with AJAX

329

<binding name="model" value="cities"/> <binding name="value" value="customer.city"/> </component>

They will call getCustomer() on your page which relies on the "customerEntry" property being set:
public abstract class CustomersCRUD extends BasePage ... { public abstract DataEntry getCustomerEntry(); //Not set! null public abstract int getIdx(); //Not set! zero public Customer getCustomer() { return (Customer) getCustomerEntry().getValue(); //NullPointerException! } ... }

That's why it will trigger a NullPointerException. The root cause of the problem is that you shouldn't rely on the "customersLoop" component to provide a Customer object for editing. You need to have your own Customer object variable (see the diagram below). On render, for each execution in the "customersLoop", you'll update this variable to point to the Customer object in the the current row. In addition, when rendering the Form, you will store its row index into a hidden form field. On rewind, you will get back the row index from the hidden form field and use it to set the variable to the object in that row. This way, no matter it is rendering or rewinding, the variable will always point to the right object.

id: 005 name: Mary 1.1: Point to it 1: On render, point to each object in 1.2: Point to it each row in turn. 1.3: Point to it id: 001 name: Paul
Value

id: 005 name: Mary id: 007 name: Peter

Temp null null

Action No Op No Op No Op

State V iew ing Editing V iew ing

3: On rewind, after reading the hidden field, set the variable to point to the object in row 1.

customer variable

2: On render the Form, put an hidden field to store the index of the row being edited.

<html> <table border="1"> <tr> <td>Paul</td> ... </tr> <tr> <td colspan="4"> <form> <hidden name="idx" value="1"/> ... </form> </td> </tr> </table> </html>

To implement this idea, modify Modify CustomersCRUD.html, CustomersCRUD.page and CustomersCRUD.java:

330

Chapter 11 Building Interactive Forms with AJAX

<html> As this component is placed <table border="1"> inside the For, it will render for <tr jwcid="customersLoop"> each row. As it is outside the <span jwcid="loadCustomerOnRender"/> Form, it will never rewind. <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="hidden" jwcid="loadCustomerOnRewind"/> <input type="text" jwcid="customerName"/> <select jwcid="country""></select> <select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span> </tr> </table> </html> <page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="loadCustomerOnRender" type="InvokeListener"> <binding name="listener" value="listener:loadCustomerOnRender"/> </component> <component id="loadCustomerOnRewind" type="Hidden"> <binding name="value" value="idx"/> <binding name="listener" value="listener:loadCustomerOnRewind"/> </component> When this component is rendered (or </page-specification> rewound), it will NOT output anything. It will just call the listener. In this case, it will call the The Hidden component listener for each row. As it is outside the will call the listener on Form, it will not be rewound at all and thus rewind, after setting the won't call the listener during the form value into "idx". submission. public abstract class CustomersCRUD extends BasePage ... { public abstract DataEntry getCustomerEntry(); public abstract int getIdx(); public Customer getCustomer() { return (Customer) getCustomerEntry().getValue(); Get the Customer object } from the current entry as public abstract Customer getCustomer(); public abstract void setCustomer(Customer customer);set by "customersLoop" public void loadCustomerOnRender() { setCustomer((Customer) getCustomerEntry().getValue()); } public void loadCustomerOnRewind() { setCustomer((Customer) getDataSet().getValueAt(getIdx())); } } Get the Customer object by looking up the DataSet using the value in "idx" as set by the Hidden component

Now run it and it should work:

Building Interactive Forms with AJAX

331

Edit

Post

Note that as the "customersLoop" component will the "idx" property on render, and the Hidden component will do so on rewind, the "idx" property will always contain a correct value. So, you can always load the Customer object using the "idx" and combine the two methods into one:
public abstract class CustomersCRUD extends BasePage ... { public void loadCustomerOnRender() { setCustomer((Customer) getCustomerEntry().getValue()); } public void loadCustomerOnRewind() { setCustomer((Customer) getDataSet().getValueAt(getIdx())); } }

Modify CustomersCRUD.page accordingly:


<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="loadCustomerOnRender" type="InvokeListener"> <binding name="listener" value="listener:loadCustomerOnRender"/> </component> <component id="loadCustomerOnRewind" type="Hidden"> <binding name="value" value="idx"/> <binding name="listener" value="listener:loadCustomerOnRewind"/> </component> </page-specification>

Run it and it will continue to work.

Skipping validation for form cancellation


Suppose that you'd like to make sure the customer's name is non-empty. To do that, modify CustomersCRUD.html:
<html> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="loadCustomerOnRender"/> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="hidden" jwcid="loadCustomerOnRewind"/> <input type="text" jwcid="customerName"/> <select jwcid="country""></select> <select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span>

332

Chapter 11 Building Interactive Forms with AJAX

</tr> </table> </html>

Modify CustomersCRUD.page:
<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="form" type="Form"> <binding name="delegate" value="bean:delegate"/> </component> <component id="customerName" type="TextField"> <binding name="value" value="customer.name"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="literal:Name"/> </component> </page-specification>

Modify CustomersCRUD.java:
public abstract class CustomersCRUD extends BasePage ... { ... @Bean public abstract ValidationDelegate getDelegate(); public void onPost() { if (getDelegate().getHasErrors()) { return; } getDataSet().setValueAt(getIdx(), getCustomer()); getDataSet().post(getIdx()); } public void onCancel() { getDelegate().clearErrors(); getDataSet().cancel(getIdx()); } }

Now run it and it will work: Post Cancel

What if client side validation is enabled? To see that, modify CustomersCRUD.html:


<html jwcid="@Shell" title="Customers"> <body jwcid="@Body"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="loadCustomerOnRender"/> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="hidden" jwcid="loadCustomerOnRewind"/> <input type="text" jwcid="customerName"/> <select jwcid="country""></select>

Building Interactive Forms with AJAX

333

<select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span> </tr> </table> </body> </html>

Modify CustomersCRUD.page:
<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="form" type="Form"> <binding name="delegate" value="bean:delegate"/> <binding name="clientValidationEnabled" value="true"/> </component> <component id="customerName" type="TextField"> <binding name="value" value="customer.name"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="literal:Name"/> </component> </page-specification>

Then if you enter an empty name, no matter you're clicking "Post" or "Cancel", you'll get an error message:

The truth is, you really don't want to validate the input if the user is canceling. To do that, you need to understand the "submit type" of a form. There are three submit types supported: Submit type Client side validation Enabled Disabled Disabled Server side validation Enabled Disabled Enabled Form components are rewound Yes No Yes When the processed. Typical use

submit (the default) cancel refresh

form

data

should

be

When the form is canceled and the data should be thrown away.

When a part of the form should be refreshed. That is, the form will be displayed again with the existing input. At first, it seems that you should set the submit type to "cancel" for the Cancel button. However, you will need to change the state of the DataEntry back to "viewing". To do that you need to get the row index from the hidden form field. If you set the submit type to "cancel", the Hidden component won't be rewound and won't set the "idx" property. Therefore, in this case you should set the submit type to "refresh", just to disable the client side validation:

334

Chapter 11 Building Interactive Forms with AJAX

<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="form" type="Form"> <binding name="delegate" value="bean:delegate"/> <binding name="clientValidationEnabled" value="true"/> </component> <component id="cancel" type="Submit"> <binding name="listener" value="listener:onCancel"/> <binding name="submitType" value="literal:refresh"/> </component> </page-specification>

Note that in order to set the submit type, it will use Javascript so make sure you have a Shell component and a Body component (which is indeed the case here). Now run it, enter an empty name and click "Cancel". It will work.

Refreshing the current row only


As the editing function is working, now it is time that you implemented the AJAX effect: When the user clicks "Edit", you'd like to refresh the current row only. To do that, modify CustomersCRUD.page:

Building Interactive Forms with AJAX

335

The most critical setting! It will change <page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> the way the response page is sent and received. Read the steps to understand. <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="idx"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId}"/> </component> 1: On render, it will call getRowClientId() on </page-specification> the page object. You will create this method to return the HTML id for the current <tr>. So, it is an array like {"customerLoops_0"}. 2: The user clicks this link. As usual, your listener will be called and a response page is generated on the server. Existing page (before clicking "Edit") <html> <table border="1"> <tr id="customerLoops"> <td>Paul</td> ... </tr> <tr id="customerLoops_0"> <td>Peter</td> ... </tr> <tr id="customerLoops_1"> <td>Mary</td> ... </tr> ... </table> </html> Result page (after clicking "Edit") <html> <table border="1"> <tr id="customerLoops"> <td>Paul</td> 3: Only those parts specified in the "updateComponents" Now it contains a ... parameter will be returned to the browser. The Javascript form </tr> on the browser will update the <tr> with the id of <tr id="customerLoops_0"> "customerLoops_0", without changing the rest of the page. <td colspan="4"> <form> <hidden name="idx" value="1"/> ... </form> </td> </tr> <tr id="customerLoops_1"> <td>Paul</td> ... </tr> ... </table> </html>

Create the getRowClientId() method:


public abstract class CustomersCRUD extends BasePage ... { ... @InjectComponent("customersLoop") public abstract ForBean getRow(); public String getRowClientId() { return getRow().getClientId();

336

Chapter 11 Building Interactive Forms with AJAX

} }

BUG ALERT: In Tapestry 4.1.1, dojo has difficulty refreshing a <tr> element in Internet Explorer. To work around the issue, use a list instead of a table (<table> => <ul>, <tr> => <li>, delete <td>). Now run it and make sure the "Edit" link still works. To verify that it is not refreshing the whole page, make the browser window smaller and scroll to the end of the table:

Click "Edit" and observe that the page is not scrolled to the beginning:

If you click "Cancel" or "Post", as you haven't implemented the AJAX effect for them, you will see the resulting page scrolled to the beginning:

Next, implement the AJAX effect for "Post" and "Cancel". After clicking "Post", you'd like to refresh the current row. In addition, if there are validation errors, you'd also like to refresh the Delegator component (shown below). In fact, even if there is no validation error, you should also refresh it in order to clear any previous validation errors. For "Cancel", it is similar. It should also clear any previous validation errors.
<html jwcid="@Shell" title="Customers"> <body jwcid="@Body"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> <table border="1"> ... </table> </body> </html>

Building Interactive Forms with AJAX

337

It is difficult to refresh the Delegator though because it won't generate any HTML element. To solve this problem, you can add a surrounding component:
<html jwcid="@Shell" title="Customers"> <body jwcid="@Body"> <span jwcid="errorPanel@Any" style="color:red"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> </span> <table border="1"> ... </table> </body> </html>

By default the Any component will render the HTML element it is associated with (<span> here). Now, to achieve the AJAX effect, modify the two Submit components:
Update the current row and the error panel. Here you can just hard code the <page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> HTML id for the "errorPanel" because it is not in a loop, so it will be rendered ... exactly once. <component id="post" type="Submit"> <binding name="listener" value="listener:onPost"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId,'errorPanel'}"/> </component> <component id="cancel" type="Submit"> <binding name="listener" value="listener:onCancel"/> <binding name="submitType" value="literal:refresh"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId,'errorPanel'}"/> </component> </page-specification> BUG ALERT: In Tapestry 4.1.1 there is a bug: If you set "async" to true and "submitType" to "refresh", the whole page will be refreshed and the listener for the Submit component will not be called. So, to work around this, let's delete this line (so client script will be in effect). Just link a DirectLink, a Submit component accepts exactly the same parameters for AJAX effect.

Now run it to verify that on clicking "Post" or "Cancel", only the current row is refreshed: Editing After clicking "Post" or "Cancel"

Refreshing the city list when the country is changed


Next, let's implement the third AJAX effect: when the user changes the country say from US from China, then the list of cities will be refreshed automatically to show those in China. This is not a DirectLink nor a Submit button, so you can't use the previous approaches. To do that, modify CustomersCRUD.java:

338

Chapter 11 Building Interactive Forms with AJAX

1: Please call this Java method when a certain event occurs in the browser

2: This is the HTML element id generated by the "country" component. Here you're saying you'd like to monitor for events generated by this HTML element. As it accepts an array, you can specify more than one elements.

3: Which events to monitor for? It's the onchange event. This event will be triggered when the user changes the selected item. Again, you can specify multiple events.

4: When the event occurs, first, submit the form with the HTML element id of "form". Here it is generated by your component named "form".

public abstract class CustomersCRUD extends BasePage ... { ... @EventListener(targets = { "country" }, events = { "onchange" }, submitForm = "form") public void onCountryChanged(IRequestCycle cycle) { getDelegate().clearErrors(); cycle.getResponseBuilder().updateComponent("city"); } } Existing page (before changing the country) Some input may be invalid, but the user is just <html> Tell Tapestry to refreshing the form. Don't <table border="1"> send back the show any error. <tr id="customerLoops"> HTML element <td>Paul</td> with the id "city". ... </tr> 5: Use AJAX to call this <tr id="customerLoops_0"> method on the server <td colspan="4"> and wait for HTML <form id="form"> elements to be <hidden id="idx" value="1"/> refreshed. <input id="name" .../> <select id="country"> <option>US</option> <option>China</option> Result page (after changing the country) 6: Result </select> page is <select id="city"> ... generated <option>New York</option> <form id="form"> <option>Boston</option> <hidden id="idx" value="1"/> <option>Chicago</option> <input id="name" .../> </select> <select id="country"> </form> <option>US</option> 7: Only this </td> <option>China</option> element is sent to </tr> </select> the browser for <tr id="customerLoops_1"> <select id="city"> refreshing. <td>Paul</td> <option>Beijing</option> ... <option>Shanghai</option> </tr> </select> ... </form> </table> </html>

How does the list of cities get changed? Check the existing code:
On rewind, the "country" component will set the country to the value selected by the user ("China"). So, on render, this value is still China, so the cities in China will be listed. public abstract class CustomersCRUD extends BasePage ... { ... public IPropertySelectionModel getCities() { if (getCustomer().getCountry().equals("US")) { return new StringPropertySelectionModel(new String[] { "New York", "Boston", "Chicago" }); } else { return new StringPropertySelectionModel(new String[] { "Beijing", "Shanghai" }); } } }

Now run it and it should work:

Building Interactive Forms with AJAX

339

Changing from US to China

The list of cities is refreshed

Will client side validation get in the way? For example, if you set the name to empty and then change the country, will it work? Yes, it will. This is a feature of the @EventListener annotation: it will skip client side validation. Note that in the example above, the city is changed from Boston to Shanghai. Why? Because Boston is the 1st city (0based) in the list (see the diagram below), when the form is submitted, it is the index (1 in this case) that is submitted. The "city" component will lookup the 1st entry in the city list, which depends on the country value in the clone. Because the "country" component has been rewound first, it has set that value to "China". So, the city list is that for China. By looking up the 1st entry in that list, the "city" component will get "Shanghai" and store it into the city value in the clone.
... <form id="form"> <hidden id="idx" value="1"/> <input id="name" .../> <select id="country"> <option>US</option> <option>China</option> </select> <select id="city"> <option value="0">New York</option> <option value="1">Boston</option> <option value="2">Chicago</option> </select> </form>

On rewind, this has changed to "China" because the "country" component has been rewound first.

public IPropertySelectionModel getCities() { if (getCustomer().getCountry().equals("US")) { return new StringPropertySelectionModel(new String[] { "New York", "Boston", "Chicago" }); } else { return new StringPropertySelectionModel(new String[] { "Beijing", "Shanghai" }); } }

This is no good. If the original city were Chicago, then it would try to look up the 2nd entry (0-based) on the list which contains only two entries. In that case it will set the city value to null. A better behavior is to reset the city the first entry in the list whenever the country is changed. To do that:
public abstract class CustomersCRUD extends BasePage ... { ... public IPropertySelectionModel getCities() { if (getCustomer().getCountry().equals("US")) { return new StringPropertySelectionModel(new String[] { "New York", "Boston", "Chicago" }); } else { return new StringPropertySelectionModel(new String[] { "Beijing", "Shanghai" }); }

340

Chapter 11 Building Interactive Forms with AJAX

} @EventListener(targets = { "country" }, events = { "onchange" }, submitForm = "form") public void onCountryChanged(IRequestCycle cycle) { getDelegate().clearErrors(); getCustomer().setCity((String) getCities().getOption(0)); cycle.getResponseBuilder().updateComponent("city"); } }

Now run the application and try to change the country. It should work.

Preventing multiple forms


For the moment, there is nothing preventing the user from having two forms in a page:

But the @EventListener annotation in Tapestry is not handling this case well: the target id and the form to be submitted must be constants and thus won't work if the components are in a loop:
public abstract class CustomersCRUD extends BasePage ... { ... @EventListener(targets = { "country" }, events = { "onchange" }, submitForm = "form") public void onCountryChanged(IRequestCycle cycle) { getDelegate().clearErrors(); getCustomer().setCity((String) getCities().getOption(0)); cycle.getResponseBuilder().updateComponent("city"); } }

Therefore, you'd like to prevent the user from opening multiple forms. To do that, modify CustomersCRUD.java:
public abstract class CustomersCRUD extends BasePage ... { ... public void onEdit(int idx) { if (getDataSet().isInteracting()) { getDelegate().record("You must finish editing the current entry first!", null); return; } getDataSet().edit(idx, new ObjectCloner() { public Object clone(Object value) { return ((Customer)value).makeClone(); } }); } }

Define the IsInteracting() method in DataSet:


public class DataSet implements Serializable { ... public boolean isInteracting() { for (Iterator iter = entries.iterator(); iter.hasNext();) { DataEntry entry = (DataEntry) iter.next(); if (entry.isCreating() || entry.isEditing()) { return true; } } return false; }

Building Interactive Forms with AJAX

341

Now run it and try to open two forms. It will not open the second form, but there is no error displayed:

Why? Because the Edit link is only refreshing the current row:
<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="idx"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId}"/> </component> </page-specification>

To fix the problem, change it to:


<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="idx"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId,'errorPanel'}"/> </component> </page-specification>

Run it again and it will work:

Implementing the Delete function


To implement the delete function, modify CustomersCRUD.java:
public abstract class CustomersCRUD extends BasePage ... { ... public void onDelete(int idx) { System.out.println("Deleting customer at " + idx); getDataSet().delete(idx); } }

This will put the DataEntry into the "hidden" state. When rendering, you need to make the <tr> invisible for such an

342

Chapter 11 Building Interactive Forms with AJAX

entry. This can be done using an HTML style. So, modify CustomersCRUD.html:
<html jwcid="@Shell" title="Customers"> <body jwcid="@Body"> <span jwcid="errorPanel@Any" style="color:red"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> </span> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="loadCustomerOnRender"/> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="hidden" jwcid="loadCustomerOnRewind"/> <input type="text" jwcid="customerName"/> <select jwcid="country""></select> <select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span> <span jwcid="ifHidden"> <td colspan="4" style="display:none"/> </span> </tr> </table> </body> </html>

Define the component in CustomersCRUD.page:


<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="ifInteracting" type="If"> <binding name="condition" value="customerEntry.interacting"/> <binding name="renderTag" value="false"/> </component> <component id="ifViewing" type="If"> <binding name="condition" value="customerEntry.viewing"/> <binding name="renderTag" value="false"/> </component> <component id="ifHidden" type="If"> <binding name="condition" value="customerEntry.hidden"/> <binding name="renderTag" value="false"/> </component> </page-specification>

Run it and it will work. But it is not using AJAX yet and is refreshing the whole page. To fix it, make the Delete link async just like the Edit link:
<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="idx"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId,'errorPanel'}"/> </component> <component id="delete" type="DirectLink"> <binding name="listener" value="listener:onDelete"/> <binding name="parameters" value="idx"/> <binding name="async" value="true"/> <binding name="updateComponents" value="{rowClientId,'errorPanel'}"/> </component> </page-specification>

Run it and it will work.

Implementing the Add and the Commit function


Finally, you can implement the Add and the Commit functions. For the former, it will refresh the whole page because a

Building Interactive Forms with AJAX

343

new row is added. For the latter, it will probably return to the main page of the application. Therefore, there is no AJAX effect involved. They are here just to show how to use the DataSet. To implement these two functions, modify CustomersCRUD.html:
<html jwcid="@Shell" title="Customers"> <body jwcid="@Body"> <span jwcid="errorPanel@Any" style="color:red"> <span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError"/> </span> <table border="1"> <tr jwcid="customersLoop"> <span jwcid="loadCustomerOnRender"/> <span jwcid="ifInteracting"> <td colspan="4"> <form jwcid="form"> <input type="hidden" jwcid="loadCustomerOnRewind"/> <input type="text" jwcid="customerName"/> <select jwcid="country""></select> <select jwcid="city"></select> <input type="submit" value="Post" jwcid="post"/> <input type="submit" value="Cancel" jwcid="cancel"/> </form> </td> </span> <span jwcid="ifViewing"> <td jwcid="showCustomerName">Paul</td> <td jwcid="showCountry">US</td> <td jwcid="showCity">New York</td> <td><a href="" jwcid="edit">Edit</a> <a href="" jwcid="delete">Delete</a></td> </span> <span jwcid="ifHidden"> <td colspan="4" style="display:none"/> </span> </tr> </table> <a href="" jwcid="add">Add</a> <a href="" jwcid="commit">Commit</a> </body> </html>

Define the components in CustomersCRUD.page:


<page-specification class="com.ttdev.ajaxapp.CustomersCRUD"> ... <component id="add" type="DirectLink"> <binding name="listener" value="listener:onAdd"/> </component> <component id="commit" type="DirectLink"> <binding name="listener" value="listener:onCommit"/> </component> </page-specification>

Define the listeners in CustomersCRUD.java:

344

Chapter 11 Building Interactive Forms with AJAX

Prevent opening multiple forms public abstract class CustomersCRUD extends BasePage ... { ... public void onAdd() { if (getDataSet().isInteracting()) { getDelegate().record("You must finish editing the current entry first!", null); return; } getDataSet().add(new Customer(0, "John Doe", "US", "New York")); } Provide a default Customer public String onCommit() { getDataSet().commit(new CustomerDBCommitResolver(getCustomerDB()));object as the new object return "Home"; Provide a CommitResolver to } make changes to the database }

public class CustomerDBCommitResolver implements CommitResolver { private CustomerDB customerDB; public CustomerDBCommitResolver(CustomerDB customerDB) { this.customerDB = customerDB; } public void update(Object value) { customerDB.update((Customer) value); } public void delete(Object value) { customerDB.delete((Customer) value); } public void insert(Object value) { customerDB.insert((Customer) value); } }

Define the update methods in CustomerDB.java. In a real application, you'll issue UPDATE, INSERT and DELETE SQL statements to the database. Here you'll just output some messages to the console:
public class CustomerDB { ... public void update(Customer customer) { System.out.println("Updating customer: " + customer.toString()); } public void delete(Customer customer) { System.out.println("Deleting customer: " + customer.toString()); } public void insert(Customer customer) { System.out.println("Inserting customer: " + customer.toString()); } }

Now, run it. Try to add, edit and delete some customers and commit. Observe the messages in the console. It will be like:

Using FireBug
AJAX operations can be difficult to debug. First, after clicking an async link or button, the HTML elements are refreshed by Javascript, but the HTML source will remain unchanged and thus you can't see the updated version. In addition, you normally don't see the HTML element fragments returned from the server. To deal with these problems, one way is to develop the pages without using AJAX first and make sure everything works. This is the strategy you adopted for this chapter. Another way is to use an excellent tool called FireBug. To use FireBug, you must use FireFox. Then go to http://www.getfirebug.com to install it. To view the HTML elements of the current page, open FireBug using View | FireBug, choose the "Inspector" tab and you'll see the structure displayed:

Building Interactive Forms with AJAX

345

Then you can drill down to find the elements concerned. Or you can click "Inspect" to switch to mouse mode. Then move the mouse over the element in the HTML page in the upper window and its HTML code will be displayed in the lower window:

346

Chapter 11 Building Interactive Forms with AJAX

To see the AJAX responses, choose the "Console" tab. There you'll see the requests sent by the current page:

In this case, they're requests to load various Javascript files. To generate an AJAX request, click the Edit link, then you'll see a new request. Before, that, switch back to keyboard mode by clicking "Inspect" again, otherwise it will be difficult to click the Edit link. After clicking the Edit link, you'll see:

Expand it to see the response where you'll find the HTML element fragments:

Building Interactive Forms with AJAX

347

You can even debug Javascript. To do that, choose the "Debugger" tab. At the bottom of the window you can choose which Javascript file to open (because a page load to multiple such files). Then you can set breakpoint by clicking on the left hand side of a line:

Then perform some action to trigger the execution (e.g., clicking a link or a button). The debugger will stop at the

348

Chapter 11 Building Interactive Forms with AJAX

breakpoint. Using the tools at bottom of the window, you can execute the script step by step. Using lower right window, you can inspect the variables and etc.:

Summary
AJAX allows you to refresh a certain HTML elements of the page. If you'd like to refresh them when the user clicks a DirectLink or a Submit button, just set "async" to true and set the "updateComponents" to indicate which HTML elements to refresh. If you'd like to refresh when some other events occur, you can use the @EventListener annotation. To make debugging AJAX easier, try implementing all the functions first and add AJAX as the last step. You can also use FireBug. In a non-AJAX form submission, you can set the submit type to "submit", "refresh" or "cancel". They allow you to skip rewinding and/or client side validation. If you're displaying a Form in a loop, keep in mind that on rewind the loop won't be rewound. Most likely you'll need to store the id or index into a Hidden component and use its listener method to restore the current object on rewind.

349

Chapter 12
Chapter 12

Test Driven Development with HtmlUnit

350

Chapter 12 Test Driven Development with HtmlUnit

What's in this chapter?


In this chapter you'll learn something called "test driven development", a development methodology that can improve the quality of your code.

Developing a calculator using test driven development


Suppose that you'd like to develop a calculator as shown below:

The user can input two integers, choose an operator and then click "OK", then the result will be displayed:

He can also click on the "History" link to see all the calculations that he has performed in this session like:

He can also click on the "Help" link. Then a new window will pop up displaying some help instructions:

Test Driven Development with HtmlUnit

351

If the user enters some garbage such as "abc" as an integer, it will pop up an error:

Setting up HtmlUnit
Now, let's do it. Before that, you need to first download a package called "HtmlUnit". It is available at http://htmlunit.sourceforge.net. You need to get v1.6 or later (I submitted a patch to make it work with Tapestry. The patch has been integrated into v1.6). Assume that you have downloaded the file htmlunit-1.6.zip and unzipped it into the folder c:\htmlunit. What does HtmlUnit do? You'll see next. Now, create a Tapestry application called TDDCalc. "TDD" means "test driven development". You'll see what it means. Then, add all the jar files in c:\htmlunit\lib to the build path:

352

Chapter 12 Test Driven Development with HtmlUnit

In addition, add c:\eclipse\plugins\org.junit_3.8.1\junit.jar too.

Setting up the web application context


Now, set your first target: The application should run as /TDDCalc/app in Tomcat. To do that, normally you'd just create a context descriptor. But now, you'll write a test first. In Eclipse, create a new class Tests in the package com.ttdev.tddcalc:
package com.ttdev.tddcalc; This method is testing to see if the web application context is available. It is called a "test case". This class is in the package junit.framework. It is provided by junit.jar (this library called "JUnit").

import java.net.*; import com.gargoylesoftware.htmlunit.*; import junit.framework.*;

public class Tests extends TestCase { public void testContextAvail() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); client.getPage(url); } } Tell the web client (browser) to go to this URL and get the result page. At the moment, you are not storing the result page at all. All you want is to see if this URL works. If something is wrong, some exception will be thrown. So you need to declare the method as throwing Exception. It is very important that this method be public Create a URL to access your application

Create a web client (WebClient). This class is coming with HtmlUnit. It is simulating a web browser.

Now, you'd like to run this test. How to run it? As it extends TestCase, you can run it directly in Eclipse by choosing "Run as JUnit Test":

Test Driven Development with HtmlUnit

353

Eclipse will find run all the public methods whose names start with "test". At the moment there is only one such method, then for each such method, it will create an instance of your class and run that method. When your method is run, it fails:

This is good! It should fail because you haven't done anything yet. You may wonder why run the test if you know that it is going to fail? You'll see later. For now, let's check the stack trace above, you will see that the connection was refused. It certainly should be refused because you haven't created the context descriptor (actually haven't started Tomcat yet). Just to be sure that it is not working, let's start Tomcat and try again, it should still fail, but this time the exception is different:

354

Chapter 12 Test Driven Development with HtmlUnit

The error code 404 means "File not found". This is good. You have made progress again. Tomcat is handling the request and it cannot find /TDDCalc/app. Now, create a web.xml and then a context descriptor in c:\tomcat\conf\Catalina\localhost\TDDCalc.xml:
<Context docBase="c:/workspace/TDDCalc/context" path="/TDDCalc" reloadable="true"/>

Tomcat should pick it up automatically. Now, run the test again:

This time it passes. You have finished your first target (make the application available as /TDDCalc/app).

Implementing the add operation


Now, you're ready to set your next target. Suppose that you'd like to implement the "Add" operation. But this target seems to big for you. Let's make it smaller: Just check if there are two text fields for the user to input two integers. So, let's write the test first:

Test Driven Development with HtmlUnit

355

Store the result page in a variable of type HtmlPage. Why need to typecast? Because it is return a Page object which could be any page such as an XML page or an HTML page. Of course, you are expecting that it should be an HTML page, so just typecast it. public class Tests extends TestCase { public void testContextAvail() throws Exception { WebConversation conv = new WebConversation(); conv.getResponse("http://localhost:8080/TDDCalc/app"); } From the HTML page, try to public void testCanInputTwoIntegers() throws Exception { get a form named WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); "calcForm". HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); assertTrue(form.getInputByName("num1") instanceof HtmlTextInput); assertTrue(form.getInputByName("num2") instanceof HtmlTextInput); } } From the form, try to get an <input> element named "num1". This method will check if its argument is true. If yes, fine. Otherwise it will throw an exception. This method is a method provided by the class TestCase. As you're extending TestCase, you can call this method. Check if that <input> element is an instance of the class HtmlTextInput. Because there are different types of input elements such as submit, button, text field and etc, you must check to make sure.

Now, run the tests:

The new test fails with an ElementNotFoundException. This is good. Obviously you don't have the form yet. If you'd like, you can double click on the offending line:

356

Chapter 12 Test Driven Development with HtmlUnit

Then Eclipse will bring you to that line that failed:

So, modify Home.html to provide the form and the two text fields:
<html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <input type="text" jwcid="num2"/> </form> </html>

Define the components in Home.page:


<page-specification> <property name="num1"/> <property name="num2"/> <component id="calcForm" type="Form"> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> </component> </page-specification>

Note that you are not defining the operator combo box or even the "OK" button. This is because you should work on your current target only (have two text fields) so that you could make the test pass as soon as possible. If you worked on other things, then it would take a lot of time before you could make the test pass. Now, run the tests again. This time they will pass:

Next, what's the next target? Mostly likely you'd like to click the "OK" button and get the result. You may wonder why you don't need to choose "Add" as the operator? Right, at the moment just assume that it will also perform addition. This way you quickly make the test pass. So, add another test:

Test Driven Development with HtmlUnit

357

public class Tests extends TestCase { public void testContextAvail() throws Exception { ... } public void testCanInputTwoIntegers() throws Exception { ... Get the form as usual } public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); Set the text input field HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); "num1" to string "3". Why num1.setValueAttribute("3"); not set it to an integer? HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); This is because you're Set "num2" to string "5" num2.setValueAttribute("5"); simulating a browser and HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); "num1" is a text field so it HtmlPage resultPage = (HtmlPage) ok.click(); only accepts a string as HtmlElement result = resultPage.getHtmlElementById("result"); input. assertEquals(result.asText(), "Result: 8"); } Try to get a submit button named "OK". Then } you call click() to simulate a click on the button. This should submit the form and get a Get the body text of AssertEquals() is provided result page. that <span> element by the TestCase class. It checks if its two arguments and check if it is equal to "Result: 8". are equal (it uses equals() Now, here comes a question, how can you to compare them). If yes, retrieve the result of the calculation for fine. Otherwise it will throw checking? It is not a text field. It is just some Get this <span> an exception. plain text. To solve this problem, you will element using its wrap the result in a <span> like: id (not its name)

<html> ... <span id="result">Result: 8</span> ... </html>

Now run the tests again. You get an ElementNotFoundException at the line highlighted below:

This is correct. You don't have the "OK" button yet. Let's define it. In Home.html:
<html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK"/> </form> </html>

Now run the tests again. This time you get an ElementNotFoundException at the line:

358

Chapter 12 Test Driven Development with HtmlUnit

This is right. You haven't generated the <span> to show the result yet. So, modify Home.html:
<html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK"/> </form> <span id="result">Result: 8</span> </html>

What? You're just hard coding the result? Right! In order to make the test pass as soon as possible, you could do that if you'd like! Now, run the tests and they should pass:

You may wonder why do that? To really calculate the result, you need to define a listener and an Insert component to show the result. That's a lot of work and will take a lot of time. As you don't want to "cool off" the test, by hard coding the result, you can quickly check if your test is working or not, in particular the following lines:
public class Tests extends TestCase { ... public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("3"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("5"); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 8"); } }

Of course, if you're very familiar with HtmlUnit and are sure that the test is correct, then you can just implement the listener to calculate and output the real result. It is up to you to decide. Next, you're about to define the listener to calculate the real thing. So, you should define a listener. But in test driven development, you must have a failing test before you write any code. So, let's modify the test case:

Test Driven Development with HtmlUnit

359

public class Tests extends TestCase { ... public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); Change the HtmlPage page = (HtmlPage) client.getPage(url); numbers HtmlForm form = page.getFormByName("calcForm"); HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 3"); } } Now run the test and it should fail:

Now you have a ComparisonFailure. JUnit is expecting a "8" but it got "3". This is because the first argument to assertEquals() is "Result: 8" while the second argument is "Result: 3":
public class Tests extends TestCase { ... public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 3"); } }

This is good. Double clicking the offending line will bring you to:

360

Chapter 12 Test Driven Development with HtmlUnit

So, let's do the work: Home.html <html> <html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span> </html>
Home.page <page-specification class="com.ttdev.tddcalc.Home"> <property name="num1"/> <property name="num2"/> <component id="calcForm" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> <binding name="translator" value="translator:number"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> <binding name="translator" value="translator:number"/> </component> <component id="resultValue" type="Insert"> <binding name="value" value="resultValue"/> </component> </page-specification> Home.java public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract void setResultValue(int result); public void onOk() { setResultValue(getNum1()+getNum2()); } } Now run the tests and they will pass.

Test Driven Development with HtmlUnit

361

Providing a list of operations


What's the next target? Let's allow the user to choose an operator from a combo box. So, add a test case first:
public class Tests extends TestCase { ... public void testOperatorList() throws Exception { Get the <select> element WebClient client = new WebClient(); (i.e., the combo box) URL url = new URL("http://localhost:8080/TDDCalc/app"); named "operator" HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlSelect operator = form.getSelectByName("operator"); Check if it contains four assertEquals(operator.getOptionSize(), 4); options assertEquals(operator.getOption(0).asText(), "Add"); assertEquals(operator.getOption(1).asText(), "Minus"); assertEquals(operator.getOption(2).asText(), "Multiply"); Get a list of selected option assertEquals(operator.getOption(3).asText(), "Divide"); elements. This is because a List selectedOptions = operator.getSelectedOptions(); <select> supports multipleassertEquals(selectedOptions.size(), 1); selection. assertEquals(((HtmlOption)selectedOptions.get(0)).asText(), "Add"); } } There should be only one option selected Get the first option. It will return an HTMLOption and it should be "Add". object representing an <option> element. Here, you check this element contains the text "Add" in its body.

Now run the test and it should fail with an ElementNotFoundException:

The offending line is:

This is fine because you don't have such a select element yet. OK, let's do the work:

362

Chapter 12 Test Driven Development with HtmlUnit

Home.html <html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span> </html> Home.page <page-specification class="com.ttdev.tddcalc.Home"> <component id="calcForm" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> <binding name="translator" value="translator:number"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> <binding name="translator" value="translator:number"/> </component> <component id="resultValue" type="Insert"> <binding name="value" value="resultValue"/> </component> <component id="operator" type="PropertySelection"> <binding name="model" value="availOperators"/> <binding name="value" value="operator"/> </component> </page-specification> Home.java public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract String getOperator(); public abstract void setResultValue(int result); public void onOk() { setResultValue(getNum1()+getNum2()); } public IPropertySelectionModel getAvailOperators() { return new StringPropertySelectionModel(new String[] { "Add", "Minus", "Multiply", "Divide" }); } }

Now run the tests and they should pass.

Using the setUp() method


Now, you're ready to set your next target. However, you note that there is some code duplication between the test cases:

Test Driven Development with HtmlUnit

363

public class Tests extends TestCase { public void testContextAvail() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); client.getPage(url); } Is part of it public void testCanInputTwoIntegers() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); assertTrue(form.getInputByName("num1") instanceof HtmlTextInput); assertTrue(form.getInputByName("num2") instanceof HtmlTextInput); } public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 3"); } public void testOperatorList() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlSelect operator = form.getSelectByName("operator"); assertEquals(operator.getOptionSize(), 4); assertEquals(operator.getOption(0).asText(), "Add"); assertEquals(operator.getOption(1).asText(), "Minus"); assertEquals(operator.getOption(2).asText(), "Multiply"); assertEquals(operator.getOption(3).asText(), "Divide"); List selectedOptions = operator.getSelectedOptions(); assertEquals(selectedOptions.size(), 1); assertEquals(((HtmlOption)selectedOptions.get(0)).asText(), "Add"); } }

Exactly the same

That is, you always connect to that URL and most of the time you will get the form. Let's extract the code to remove the duplication:

364

Chapter 12 Test Driven Development with HtmlUnit

Before each test method is run, Eclipse will create a new object of this case, call its setUp() method and then run that test method. public class Tests extends TestCase { private WebClient client; As you need to access the form in the test methods, private HtmlPage page; you need to make the "form" an instance variable. The private HtmlForm form; same is true for the web client and the result page. protected void setUp() throws Exception { client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); page = (HtmlPage) client.getPage(url); form = page.getFormByName("calcForm"); } As what this method does is already public void testContextAvail() throws Exception { included in the other tests, you don't WebClient client = new WebClient(); really need it anymore. URL url = new URL("http://localhost:8080/TDDCalc/app"); client.getPage(url); } public void testCanInputTwoIntegers() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); assertTrue(form.getInputByName("num1") instanceof HtmlTextInput); assertTrue(form.getInputByName("num2") instanceof HtmlTextInput); } public void testAdd() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 3"); } public void testOperatorList() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlForm form = page.getFormByName("calcForm"); HtmlSelect operator = form.getSelectByName("operator"); assertEquals(operator.getOptionSize(), 4); assertEquals(operator.getOption(0).asText(), "Add"); assertEquals(operator.getOption(1).asText(), "Minus"); assertEquals(operator.getOption(2).asText(), "Multiply"); assertEquals(operator.getOption(3).asText(), "Divide"); List selectedOptions = operator.getSelectedOptions(); assertEquals(selectedOptions.size(), 1); assertEquals(((HtmlOption)selectedOptions.get(0)).asText(), "Add"); } }

Now, run the tests and they should continue to pass.

Implementing minus
Next, let's pick another target. Let's check if the "Minus" operation works. So, write a test first:

Test Driven Development with HtmlUnit

365

public class Tests extends TestCase { ... public void testMinus() throws Exception { HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSelect operator = form.getSelectByName("operator"); operator.getOption(1).setSelected(true); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: -1"); } } Get the option element for "Minus" As you're using 1 to minus 2, you (whose position is 1) and call expect the value to be -1. setSelected(true) to select it. Now run the test and it should fail because you haven't done anything yet. Now, let's do the work:
public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract String getOperator(); public abstract void setResultValue(int result); public void onOk() { if (getOperator().equals("Add")) { setResultValue(getNum1()+getNum2()); return; } if (getOperator().equals("Minus")) { setResultValue(getNum1()-getNum2()); return; } } public IPropertySelectionModel getAvailOperators() { return new StringPropertySelectionModel(new String[] { "Add", "Minus", "Multiply", "Divide" }); } }

Now run the tests again and they should pass.

Implementing the History link


Now, let's pick the next target. You will not do the multiplication and division as they are very similar. Let's check the "History" link. First, write a test case. As the Tests class is quite large, and this "History" link seems to be a different function, let's rename the Tests class as TestsForCalculation:
public class TestsTestsForCalculation extends TestCase { ... }

Then create a new class TestsForHistory:

366

Chapter 12 Test Driven Development with HtmlUnit

Go to the Home page

To save some work, make sure of the TestsForCalculation

Try to find a link whose id is "history". You public class TestsForHistory extends TestCase { need to typecast it to HtmlAnchor. Then public void testTwoCalculations() throws Exception { click the link and get the result page. TestsForCalculation test = new TestsForCalculation(); test.setUp(); Perform an addition Get the result page. Need to define this test.testAdd(); method. and a subtraction test.testMinus(); HtmlPage page = test.getPage(); HtmlAnchor historyLink = (HtmlAnchor) page.getHtmlElementById("history"); page = (HtmlPage) historyLink.click(); HtmlTable table = (HtmlTable) page.getHtmlElementById("historyTable"); assertEquals(table.getRowCount(), 3); assertEquals(table.getCellAt(0, 0).asText(), "Number 1"); In the result page you look for a table assertEquals(table.getCellAt(0, 1).asText(), "Operator"); whose id is "historyTable" assertEquals(table.getCellAt(0, 2).asText(), "Number 2"); assertEquals(table.getCellAt(0, 3).asText(), "Result"); Check if the table contains three rows assertEquals(table.getCellAt(1, 0).asText(), "1"); assertEquals(table.getCellAt(1, 1).asText(), "Add"); Check the cells on the first row (row 0). assertEquals(table.getCellAt(1, 2).asText(), "2"); The cell at row 0 and column 0 should assertEquals(table.getCellAt(1, 3).asText(), "3"); be the string "Number 1". The cell at assertEquals(table.getCellAt(2, 0).asText(), "1"); row 0 and column 1 should be the string assertEquals(table.getCellAt(2, 1).asText(), "Minus"); "Number 2" and etc. assertEquals(table.getCellAt(2, 2).asText(), "2"); Check the cells on the second row (row assertEquals(table.getCellAt(2, 3).asText(), "-1"); 1). It should show "1 Add 2 = 3". } } Check the cells on the third row (row 2). It should show "1 Minus 2 = -1". public class TestsForCalculation extends TestCase { private WebClient client; private HtmlPage page; private HtmlForm form; ... public HtmlPage getPage() { return page; } }

Now run the test and it should fail because you don't have the "History" link yet. Let's do the work:

Test Driven Development with HtmlUnit

367

Home.html <html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span> <a id="history" href="" jwcid="history">History</a> </html> Home.page <page-specification class="com.ttdev.tddcalc.Home"> <component id="calcForm" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> <binding name="translator" value="translator:number"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> <binding name="translator" value="translator:number"/> </component> <component id="resultValue" type="Insert"> <binding name="value" value="resultValue"/> </component> <component id="operator" type="PropertySelection"> <binding name="model" value="availOperators"/> <binding name="value" value="operator"/> </component> <component id="history" type="PageLink"> <binding name="page" value="literal:History"/> </component> </page-specification>

Create the History page. History.html is:


<html> <table id="historyTable"> <tr><td>Number 1</td><td>Operator</td><td>Number 2</td><td>Result</td></tr> <tr jwcid="history"> <td><span jwcid="num1">10</span></td> <td><span jwcid="operator">Add</span></td> <td><span jwcid="num2">3</span></td> <td><span jwcid="result">13</span></td> </tr> </table> </html>

Define the components in History.page:


<page-specification class="com.ttdev.tddcalc.History"> <component id="history" type="For"> <binding name="source" value="calculations"/> <binding name="value" value="calculation"/> <binding name="element" value="literal:tr"/> </component> <component id="num1" type="Insert"> <binding name="value" value="calculation.num1"/> </component> <component id="num2" type="Insert"> <binding name="value" value="calculation.num2"/> </component> <component id="operator" type="Insert"> <binding name="value" value="calculation.operator"/> </component> <component id="result" type="Insert"> <binding name="value" value="calculation.result"/> </component> </page-specification>

Create Calculation.java:

368

Chapter 12 Test Driven Development with HtmlUnit

public class Calculation { private int num1; private int num2; private String operator; private int result; public Calculation(int num1, int num2, String operator, int result) { this.num1 = num1; this.num2 = num2; this.operator = operator; this.result = result; } public int getNum1() { return num1; } public int getNum2() { return num2; } public String getOperator() { return operator; } public int getResult() { return result; } }

Create History.java:
public abstract class History extends BasePage { public abstract Calculation getCalculation(); public List getCalculations() { List calculations = new ArrayList(); calculations.add(new Calculation(1, 2, "Add", 3)); calculations.add(new Calculation(1, 2, "Minus", -1)); return calculations; } }

Again, you're hard coding the result, so that you can get the test pass as soon as possible. You've done a lot of work without running the test. Now, run it and it should pass. Next, you should get rid of the hard code calculations. Let's perform one more addition:
public class TestsForHistory extends TestCase { public void testTwoThreeCalculations() throws Exception { TestsForCalculation test = new TestsForCalculation(); test.setUp(); test.testAdd(); test.testMinus(); test.testAdd(); HtmlPage page = test.getPage(); HtmlAnchor historyLink = (HtmlAnchor) page.getHtmlElementById("history"); page = (HtmlPage) historyLink.click(); HtmlTable table = (HtmlTable) page.getHtmlElementById("historyTable"); assertEquals(table.getRowCount(), 34); assertEquals(table.getCellAt(0, 0).asText(), "Number 1"); assertEquals(table.getCellAt(0, 1).asText(), "Operator"); assertEquals(table.getCellAt(0, 2).asText(), "Number 2"); assertEquals(table.getCellAt(0, 3).asText(), "Result"); assertEquals(table.getCellAt(1, 0).asText(), "1"); assertEquals(table.getCellAt(1, 1).asText(), "Add"); assertEquals(table.getCellAt(1, 2).asText(), "2"); assertEquals(table.getCellAt(1, 3).asText(), "3"); assertEquals(table.getCellAt(2, 0).asText(), "1"); assertEquals(table.getCellAt(2, 1).asText(), "Minus"); assertEquals(table.getCellAt(2, 2).asText(), "2"); assertEquals(table.getCellAt(2, 3).asText(), "-1"); assertEquals(table.getCellAt(3, 0).asText(), "1"); assertEquals(table.getCellAt(3, 1).asText(), "Add"); assertEquals(table.getCellAt(3, 2).asText(), "2"); assertEquals(table.getCellAt(3, 3).asText(), "3"); } }

Run the test and it should fail. However, It fails in TestCalculation:

Test Driven Development with HtmlUnit

369

Most likely this is the new addition operation that you just added. Why the result is not "3"? It is because you are not setting the operator to "Add". In the last calculation you set it to "Minus" so it remains to be "Minus". OK, let's fix this test:
public class TestsForCalculation extends TestCase { ... public void testAdd() throws Exception { HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("1"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSelect operator = form.getSelectByName("operator"); operator.getOption(0).setSelected(true); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); HtmlElement result = resultPage.getHtmlElementById("result"); assertEquals(result.asText(), "Result: 3"); } }

Now run it again. Now it fails at the expected location:

Now let's do the work. You should store each calculation performed into a List in the session. Create METAINF/hivemodule.xml:
<module id="com.ttdev.album" version="1.0.0"> <contribution configuration-id="tapestry.state.ApplicationObjects"> <state-object name="history" scope="session"> <create-instance class="java.util.ArrayList"/> </state-object> </contribution> </module>

As you'll store Calculation objects into the session, the Calculation class must be serializable:
public class Calculation implements Serializable { private static final long serialVersionUID = 2066677456244620659L; private int num1; private int num2; private String operator; private int result;

370

Chapter 12 Test Driven Development with HtmlUnit

... }

Modify Home.java to log each calculation:


public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract String getOperator(); public abstract void setResultValue(int result); public void onOk() { if (getOperator().equals("Add")) { setResult(getNum1()+getNum2()); return; } if (getOperator().equals("Minus")) { setResult(getNum1()-getNum2()); return; } } @InjectState("history") public abstract List getHistory(); private void setResult(int result) { setResultValue(result); getHistory().add(new Calculation(getNum1(), getNum2(), getOperator(), result)); } public IPropertySelectionModel getAvailOperators() { return new StringPropertySelectionModel(new String[] { "Add", "Minus", "Multiply", "Divide" }); } }

Restart the application so that the hivemodule.xml file is read. Then, run the test but it still fails. Why? Let's run it manually:

It is still showing the hard code data. Right, you're still using the hard code data in History.java:
public abstract class History extends BasePage { public abstract Calculation getCalculation(); public List getCalculations() { List calculations = new ArrayList(); calculations.add(new Calculation(1, 2, "Add", 3)); calculations.add(new Calculation(1, 2, "Minus", -1)); return calculations; } }

So, change it to use the calculations in Visit:


public class History extends BasePage { public abstract Calculation getCalculation(); @InjectState("history") public abstract List getCalculations();

Test Driven Development with HtmlUnit

371

Now run the test and it should pass.

Fixing the problems revealed by manual inspection


When you run the application manually, you note that something is wrong: the button is called "Submit Query", not "OK". The "History" should not appear on the same line as the result:

It means that your tests so far haven't revealed all the problems in the program. This is true. Automated testing cannot reveal all the problems. Testing it manually from time to time is helpful. So, what do you do? Fix the two problems? No! Write a failing test first:
public class TestsForCalculation extends TestCase { ... public void testOKLabel() throws Exception { assertEquals(form.getInputByName("OK").getValueAttribute(), "OK"); } }

Can you just run this test instead of all the tests in TestsForCalculation? Yes, you can. Just expand the TestsForCalculation class in the package explorer, right "testOKLabel" and choose "Run | JUnit Test":

372

Chapter 12 Test Driven Development with HtmlUnit

It fails. Good. Modify Home.html:


<html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK" value="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span> <a id="history" href="" jwcid="history">History</a> </html>

Run it again and it should pass. Now, how to test that the "History" link is on the next line? It is very difficult to test this kind of visual effect. So the best way is not to test it with code. Just modify Home.html to add a <p> element:
<html> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK" value="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span><p> <a id="history" href="" jwcid="history">History</a> </html>

and be done with it.

Test Driven Development with HtmlUnit

373

Running all the tests


Now you have implemented the "History" link. Are you sure that this hasn't broken any of the existing tests in TestsForCalculation? Therefore, you'd like to run all the tests in the program. Now they are separate in TestsForCalculation and TestsForHistory. To run them all, create a new class TestsPassed:
Instead of having individual test methods, it has a single public static method called suite(). When you run it, Eclipse will call this suite() method and expect it to return a "test suite". A test suite is just a bunch of test cases. Create a empty test suite first public class TestsPassed extends TestCase { static public TestSuite suite() { TestSuite suite = new TestSuite(); This will create a test suite containing all suite.addTestSuite(TestsForCalculation.class); the test cases in TestsForCalculation suite.addTestSuite(TestsForHistory.class); and then add that suite into the empty return suite; suite } } Do the same thing for TestsForHistory

Now, run TestsPassed as a JUnit test and it will run all the test cases in the program:

Implementing validation
Now let's pick another target. Let's check the integer validation. Add a new test in TestsForCalculation (why not create a new class TestsForValidation? You could do it. It's up to you how to organize the test cases):
public class TestsForCalculation extends TestCase { ... public void testBadInt() throws Exception { HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("abc"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSelect operator = form.getSelectByName("operator"); operator.getOption(0).setSelected(true); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); HtmlPage resultPage = (HtmlPage) ok.click(); ... } } How to test the popup behavior? Input "abc" as "num1". Then when you click "OK". It should pop up an alert window with text like "An integer is required" or something like that.

Your web client will not really pop up a window. Instead, it will call an "alert handler" and pass the alert message to it. So, you can do it this way:

374

Chapter 12 Test Driven Development with HtmlUnit

public class TestsForCalculation extends TestCase { ... public void testBadInt() throws Exception { HtmlTextInput num1 = (HtmlTextInput) form.getInputByName("num1"); num1.setValueAttribute("abc"); HtmlTextInput num2 = (HtmlTextInput) form.getInputByName("num2"); num2.setValueAttribute("2"); HtmlSelect operator = form.getSelectByName("operator"); operator.getOption(0).setSelected(true); HtmlSubmitInput ok = (HtmlSubmitInput) form.getInputByName("OK"); You'll store the alert messages List alerts = new ArrayList(); client.setAlertHandler(new CollectingAlertHandler(alerts)); onto this List ok.click(); assertEquals(alerts.size(), 1); assertTrue(((String) alerts.get(0)).indexOf("number 1") != -1); } } Set the alert handler The CollectingAlertHandler class is provided by HtmlUnit. You pass a List to it, then it will store each alert message onto the List. Get the first (and only) alert message and check if it contains the string "number 1"

After clicking "OK", check if the List contains exactly one alert.

Now run the test and it should fail. This is good. Let's do the work. Home.page is:
<page-specification class="com.ttdev.tddcalc.Home"> <component id="calcForm" type="Form"> <binding name="listener" value="listener:onOk"/> <binding name="delegate" value="bean:delegate"/> <binding name="clientValidationEnabled" value="true"/> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> <binding name="translator" value="translator:number"/> <binding name="displayName" value="literal:number 1"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> <binding name="translator" value="translator:number"/> <binding name="displayName" value="literal:number 2"/> </component> <component id="resultValue" type="Insert"> <binding name="value" value="resultValue"/> </component> <component id="operator" type="PropertySelection"> <binding name="model" value="availOperators"/> <binding name="value" value="operator"/> </component> <component id="history" type="PageLink"> <binding name="page" value="literal:History"/> </component> </page-specification>

Home.java is:
public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract String getOperator(); public abstract void setResultValue(int result); @Bean public abstract ValidationDelegate getDelegate(); public void onOk() { if (getOperator().equals("Add")) { setResult(getNum1()+getNum2()); return; } if (getOperator().equals("Minus")) { setResult(getNum1()-getNum2());

Test Driven Development with HtmlUnit

375

return; } } ... }

Run the test again but it fails. To see why, run it manually:

So, you are missing a Body component for the script. Modify Home.html:
<html> <body jwcid="@Body"> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK" value="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span><p> <a id="history" href="" jwcid="history">History</a> </body> </html>

Run the test again and it should pass.

Implementing the Help link


Next, let's pick another target. The next most interesting target is the "Help" link which should pop up a new window. To implement it, you need to write a failing test first. As this test isn't about calculation nor about history, let's create a new class TestsForHelp:
public class TestsForHelp extends TestCase { public void testPopupHelp() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlAnchor helpLink = (HtmlAnchor) page.getHtmlElementById("help"); helpLink.click(); } }

Nothing special here. You just get the "Help" link and click it. However, how to test if it really pops up a new window showing a help page? To do that, you can:

376

Chapter 12 Test Driven Development with HtmlUnit

public class TestsForHelp extends TestCase { public void testPopupHelp() throws Exception { WebClient client = new WebClient(); Create a web window listener URL url = new URL("http://localhost:8080/TDDCalc/app"); and add it to the web client HtmlPage page = (HtmlPage) client.getPage(url); HtmlAnchor helpLink = (HtmlAnchor) page.getHtmlElementById("help"); client.addWebWindowListener(new WebWindowListener() { Whenever a new window is opened, public void webWindowOpened(WebWindowEvent arg0) { this method will be called. However, } public void webWindowContentChanged(WebWindowEvent ev) { at that time, no page has been loaded in the window yet. HtmlPage page = (HtmlPage) ev.getNewPage(); assertEquals(page.getTitleText(), "Help"); Get the new page from the TopLevelWindow window = (TopLevelWindow) ev.getWebWindow(); event window.close(); Check if the page title is } "Help". You don't want to public void webWindowClosed(WebWindowEvent arg0) { check the full content of } the help page. }); helpLink.click(); } Get the window from the event. What you'd like When a page is indeed loaded, this } to do is to close the window. However, there is method will be called. no close() method in the WebWindow class. This is because either a frame or a top level window is a web window. Only the latter can be closed. In your case you know that the window must be a top level window, so you type cast it before calling its close() method.

Now run the test and it fails because it can't find the "Help" link. This is correct because you haven't added it yet. So, modify Home.html:
<html> <body jwcid="@Body"> <form jwcid="calcForm"> <input type="text" jwcid="num1"/> <select jwcid="operator"/> <input type="text" jwcid="num2"/> <input type="submit" name="OK" value="OK"/> </form> <span id="result">Result: <span jwcid="resultValue">8</span></span><p> <a id="history" href="" jwcid="history">History</a> <a id="help" href="" jwcid="help">Help</a> </body> </html>

Modify Home.page:
<page-specification class="com.ttdev.tddcalc.Home"> <component id="calcForm" type="Form"> <binding name="listener" value="listener:onOk"/> <binding name="delegate" value="bean:delegate"/> <binding name="clientValidationEnabled" value="true"/> </component> <component id="num1" type="TextField"> <binding name="value" value="num1"/> <binding name="translator" value="translator:number"/> <binding name="displayName" value="literal:number 1"/> </component> <component id="num2" type="TextField"> <binding name="value" value="num2"/> <binding name="translator" value="translator:number"/> <binding name="displayName" value="literal:number 2"/> </component> <component id="resultValue" type="Insert"> <binding name="value" value="resultValue"/> </component> <component id="operator" type="PropertySelection"> <binding name="model" value="availOperators"/> <binding name="value" value="operator"/> </component> <component id="history" type="PageLink"> <binding name="page" value="literal:History"/> </component> <component id="help" type="PageLink"> <binding name="page" value="literal:Help"/> </component>

Test Driven Development with HtmlUnit

377

</page-specification>

Create a Help page. Put an empty page in Help.html:


<html/>

Help.page is also an empty element:


<page-specification/>

Now run the test and it fails at the following line:

This is OK. Just set the title in Help.html:


<html> <head><title>Help</title></head> </html>

Now run the test and it passes! This is no good! You're not popping up any new window yet! You're just displaying the Help page in the current window. It means something is wrong with your test. That's why you'd like to run a test and see it fails before implementing the real functionality. If the test mysteriously passes, then there must be something wrong with it. In your case, you should check if the Help page is displayed in a new window:

378

Chapter 12 Test Driven Development with HtmlUnit

public class TestsForHelp extends TestCase { private TopLevelWindow helpWindow; public void testPopupHelp() throws Exception { WebClient client = new WebClient(); URL url = new URL("http://localhost:8080/TDDCalc/app"); HtmlPage page = (HtmlPage) client.getPage(url); HtmlAnchor helpLink = (HtmlAnchor) page.getHtmlElementById("help"); client.addWebWindowListener(new WebWindowListener() { public void webWindowOpened(WebWindowEvent ev) { Store the new window object into an helpWindow = (TopLevelWindow) ev.getWebWindow(); instance variable so that you can } public void webWindowContentChanged(WebWindowEvent ev) { compare it later. This will ensure that a new window has been opened. assertSame(ev.getWebWindow(), helpWindow); HtmlPage page = (HtmlPage) ev.getNewPage(); assertEquals(page.getTitleText(), "Help"); TopLevelWindow window = (TopLevelWindow) ev.getWebWindow(); window.close(); helpWindow.close(); } public void webWindowClosed(WebWindowEvent arg0) { } }); helpLink.click(); } } assertSame() will check if its two arguments are exactly the same object (it uses == to check)

Now run the test and it fails at the following line:

This is good. Now, let's really pop up a new window. To do that:

Test Driven Development with HtmlUnit

379

Home.page <page-specification ...> ... <component id="help" type="PageLink"> <binding name="page" value="literal:Help"/> <binding name="renderer" value="helpLinkRenderer"/> </component> </page-specification> Provide a renderer to render the <a> element Home.java public abstract class Home extends BasePage { ... public ILinkRenderer getHelpLinkRenderer() { return new PopupLinkRenderer(); } }

PopupLinkRenderer is provided by Tapestry. It will generate an <a> element which will pop up a new window and display the target document in there. Now run the test and it should pass. Finally, add TestsForHelp to TestsPassed:
public class TestsPassed extends TestCase { static public TestSuite suite() { TestSuite suite = new TestSuite(); suite.addTestSuite(TestsForCalculation.class); suite.addTestSuite(TestsForHistory.class); suite.addTestSuite(TestsForHelp.class); return suite; } }

Then run TestsPassed and all the tests should continue to pass.

Refactoring
Check your Home.java again:
public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract String getOperator(); public abstract void setResultValue(int result); @Bean public abstract ValidationDelegate getDelegate(); public void onOk() { if (getOperator().equals("Add")) { setResult(getNum1()+getNum2()); return; } if (getOperator().equals("Minus")) { setResult(getNum1()-getNum2()); return; } } @InjectState("history") public abstract List getHistory(); private void setResult(int result) { setResultValue(result); getHistory().add( new Calculation(getNum1(), getNum2(), getOperator(), result)); }

380

Chapter 12 Test Driven Development with HtmlUnit

public IPropertySelectionModel getAvailOperators() { return new StringPropertySelectionModel(new String[] { "Add", "Minus", "Multiply", "Divide" }); } public ILinkRenderer getHelpLinkRenderer() { return new PopupLinkRenderer(); } }

As you add multiply and division in the future, you'll see a long list of if-then in onOk(). In terms of OO design this is no good. You'd like to remove this repeative if-then blocks. To do that, you can think of an operator as an object, not just a string:
public abstract class Home extends BasePage { public abstract int getNum1(); public abstract int getNum2(); public abstract int getResultValue(); public abstract Operator getOperator(); public abstract void setResultValue(int result); ... public void onOk() { setResult(getOperator().apply(getNum1(), getNum2())); } private void setResult(int result) { setResultValue(result); getHistory().add( new Calculation(getNum1(), getNum2(), getOperator().getName(), result)); } public IPropertySelectionModel getAvailOperators() { return new OperatorSelectionModel(); } }

Define the Operator interface and various subclasses:


public interface Operator { String getName(); int apply(int num1, int num2); } public class Add implements Operator { public String getName() { return "Add"; } public int apply(int num1, int num2) { return num1+num2; } } public class Minus implements Operator { public String getName() { return "Minus"; } public int apply(int num1, int num2) { return num1-num2; } }

Finally, create the property selection model for the operators:


public class OperatorSelectionModel implements IPropertySelectionModel { private List operators; public OperatorSelectionModel() { operators = new ArrayList(); operators.add(new Add()); operators.add(new Minus()); } public int getOptionCount() { return operators.size(); } public Object getOption(int index) { return getOperator(index); } public String getLabel(int index) { return getOperator(index).getName(); }

Test Driven Development with HtmlUnit

381

public String getValue(int index) { return Integer.toString(index); } public Object translateValue(String value) { return getOperator(Integer.parseInt(value)); } public Operator getOperator(int index) { return (Operator) operators.get(index); } }

Quite a lot of changes. How are you sure that you haven't broken anything? Easy! Just run TestsPassed again. In this it fails at the following line:

This is OK as there are just two operators for selection. To make it pass, just create two more operators:
public class Multiply implements Operator { public String getName() { return "Multiply"; } public int apply(int num1, int num2) { return num1*num2; } } public class Divide implements Operator { public String getName() { return "Divide"; } public int apply(int num1, int num2) { return num1 / num2; } }

Add them to the property selection model:


public class OperatorSelectionModel implements IPropertySelectionModel { private List operators; public OperatorSelectionModel() { operators = new ArrayList(); operators.add(new Add()); operators.add(new Minus()); operators.add(new Multiply()); operators.add(new Divide()); } ... }

Now, run the tests again and they should pass. You have restructured your code without changing the functionality, while improving its quality. This is called "refactoring". As you're developing the application bit by bit, it is very important that you refactor it whenever you find "code smells" like a long list of if-then blocks or duplicate code. After changing the code, you can just run the tests. If they continue to pass, it means you haven't broken anything. Without the tests, you might not have dared to change your code. That is, the tests serve as your safety net to allow you to improve the quality of your code.

Summary
In TDD, you write a little test, let it fail and then write a little code to make the test pass ASAP. This way you get fast feedbacks on your test (is the test working?) and the code (is the code working?). In time the passed tests will become

382

Chapter 12 Test Driven Development with HtmlUnit

a safety net for you so that you can refactor your code without fearing that your changes may break anything. Whenever you find a bug, don't fix it yet. Create a failing test first. This will further strengthen your safety net. Even automated tests are very useful, they can't reveal all problems. So, regular manual inspection is quite useful in revealing other problems. HtmlUnit can help you create automated tests for your web application. You can use it to simulate user actions through a simulated browser. After getting a response page for a URL, you can check it by getting various elements such as <span>, <input>, <select>, <table> using their id's or names and then check their contents. To check a Javascript alert dialog, use an alert handler to collect the alert message. To check the contents of a popup window, use a web window listener. To open a link in a popup window, use a PopupLinkRenderer with the link component.

383

Chapter 13
Chapter 13

Database and Concurrency Issues

384

Chapter 13 Database and Concurrency Issues

What's in this chapter?


In this chapter you'll learn how to access a database in a Tapestry application and how to allow users to access your application concurrently without interfering with one another.

Developing a banking application


Suppose that you'd like to develop an online banking application. It should allow the user to transfer some money from one account to another::

Here you're trying to transfer $50 from bank account 001 to bank account 003. You will ignore security issues and allow any user to perform this operation. Let's create a Tapestry application called Bank and set it up as usual (use /Bank as the context path).

Setting up PostgreSQL
You need to store the accounts in your application. In the previous chapter you'd have used a Java List or something similar to simulate a database. But now, let's a real database. As an example, you'll setup a PostgreSQL database server. Go to http://www.postgresql.org to download the binary for Windows. Suppose that it is postgresql-8.0.1.zip. Unzip it and you'll get a few files:

Make sure you are logged in as the Administrator. Then double click on postgresql-8.0.msi. This will launch the installer. Basically you'll accept the defaults. During the installation, you'll be asked to enter the password for a Windows user account "postgres" to run the PostgreSQL service:

Database and Concurrency Issues

385

You can leave the password empty so that the installer will generate a password for it. This is OK. You will not login to Windows using account at all. Only the PostgreSQL service will. Later, you'll be prompted for the password for the database user also named "postgres". This account is not the Windows account, but a database user account. You'll use it to login to the PostgreSQL service. So, pick a password that you can remember. In this example, suppose that the password is "pgsql":

By default, the PostgreSQL service will accept connections from the local computer only. This is for security. If you need to run Tomcat on one computer but PostgreSQL on another, then you need to check "Accept connections on all

386

Chapter 13 Database and Concurrency Issues

addresses, not just localhost". Follow the instructions to finish the installation. The installer will start the PostgreSQL service for you (It will also start automatically every time the computer boots). Now logout and then login using your regular Windows user account. Run "Programs | PostgreSQL 8.0 | pgAdmin III". You'll see:

There is a connection listed that is crossed out. Right click on it and choose "Connect", then enter the password "pgsql" as you're connecting as the database user "postgres":

Then the cross along the connection should disappear:

Database and Concurrency Issues

387

Expand the connection:

A single user "postgres" is listed there. Right click "Users" and choose "New User". Then create a new user as shown below:

388

Chapter 13 Database and Concurrency Issues

Next, right click "Databases" and choose "New Database" and enter:

This will create a database named "bank" and let "bankuser" be the owner. You have been doing should be done by the database administrator. Later you will connect as "bankuser" to create the tables and access the database from your banking application. Right click the connection and choose "Disconnect". Right click it again and choose "Properties". Change to connect as "bankuser" instead of "postgres":

Database and Concurrency Issues

389

Right click it and choose "Connect". Enter "123456" as the password:

Then, expand "bank" like:

390

Chapter 13 Database and Concurrency Issues

Right click "Tables" and choose "New Table". You will create a table to store the bank accounts. Enter "accounts" as the name of the table:

Choose "Columns" and add the following columns:

You're advised to use lower case for the column names. If you used names like "accNo", then PostgreSQL will think that it is case sensitive and will require an exact match of column names. Next, choose "Constraints":

Click "Add" to add a primary key. Leave the name of the primary key as empty:

Database and Concurrency Issues

391

Choose "Columns" and add the "accno" to the primary key:

That's it. If you click on the "accounts" table, you'll see the corresponding SQL:

392

Chapter 13 Database and Concurrency Issues

Hard coding some bank accounts


To allow the user to make transfers, you must have some bank accounts. Let's hard code some. In PgAdmin, click the Pencil icon:

This will open the query tool. Enter the following statements:

Click the icon highlighted above to execute the query. This will create three bank accounts. Close the query tool. To verify that the bank accounts are indeed there, right click the "accounts" table and choose "View Data":

Database and Concurrency Issues

393

Transferring some money


Now let's work on the money transfer. Create a Transfer page. Then modify Transfer.html:
<html> <form jwcid="form"> From: <input type="text" jwcid="fromAccNo"/><br> To: <input type="text" jwcid="toAccNo"/><br> Amount: <input type="text" jwcid="amount"/><br> <input type="submit" value="OK"/> </form> </html>

Transfer.page is like:
<page-specification class="com.ttdev.bank.Transfer"> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="fromAccNo" type="TextField"> <binding name="value" value="fromAccNo"/> </component> <component id="toAccNo" type="TextField"> <binding name="value" value="toAccNo"/> </component> <component id="amount" type="TextField"> <binding name="value" value="amount"/> <binding name="translator" value="translator:number"/> <binding name="displayName" value="literal:amount"/> </component> </page-specification>

Nothing special here. Create Transfer.java:

394

Chapter 13 Database and Concurrency Issues

Use the JDBC protocol

Use the postgresql protocol

Access a database on localhost whose name is "bank"

public abstract class Transfer extends BasePage { public abstract String getFromAccNo(); public abstract String getToAccNo(); public abstract int getAmount();

Load the class org.postgresql.Driver. This is public void onOk() { the JDBC driver for try { PostgreSQL. It can be found Class.forName("org.postgresql.Driver"); in a jar file in c:\Program Connection conn = DriverManager.getConnection( Files\PostgreSQL\8.0\jdbc. "jdbc:postgresql://localhost/bank", "bankuser", Connect as database user "bankuser" "123456"); with password "123456". try { //update the bank accounts here... } finally { conn.close(); Must close the connection } For convenience, catch any } catch (Exception e) { exception and wrap it inside a throw new RuntimeException(e); RuntimeException. } }

You need to use the jar file containing the PostgreSQL JDBC driver in c:\Program Files\PostgreSQL\8.0\jdbc:

There are several drivers there. You need the JDBC3 driver as shown above. To make it available to your application at runtime, create a "lib" folder under WEB-INF and copy the jar file there. To update the bank accounts, you can do it this way:

Database and Concurrency Issues

395

public abstract class Transfer extends BasePage { public abstract String getFromAccNo(); public abstract String getToAccNo(); public abstract int getAmount(); Create an update statement to reduce the amount from public void onOk() { try { the source account Class.forName("org.postgresql.Driver"); Connection conn = DriverManager.getConnection( "jdbc:postgresql://localhost/bank", "bankuser", "123456"); try { PreparedStatement st = conn .prepareStatement("update accounts set balance=balance-? where accno=?"); try { Set the amount to reduce st.setInt(1, getAmount()); st.setString(2, getFromAccNo()); Set the account st.executeUpdate(); number } finally { Execute the update st.close(); statement } st = conn .prepareStatement("update accounts set balance=balance+? where accno=?"); try { st.setInt(1, getAmount()); st.setString(2, getToAccNo()); st.executeUpdate(); } finally { st.close(); } } finally { conn.close(); Increase the balance of } the target account } catch (Exception e) { throw new RuntimeException(e); } } }

Now go localhost:8080/Bank/app?service=page&page=Transfer and try to transfer $50 from account 001 to 003:

Then click "OK". It may take some time and then the same page is displayed. This is because you're not showing any result page. To check the transfer has indeed occurred, view the data of the "accounts" table in PgAdmin:

396

Chapter 13 Database and Concurrency Issues

Their balances were 200 and 100 respectively. Now they're both 150. It means it is working.

Using a transaction
Even though it is working, it has a problem. What if after reducing the balance of the source account, when it is about to increase the balance of the target account, Tomcat or the underlying server computer crashes? Then the money will be gone! To solve this problem, you can use a transaction: public void onOk() { Set auto commit to false. By default try { auto commit is true. When it is true, Class.forName("org.postgresql.Driver"); each SQL statement is a transaction. Connection conn = DriverManager.getConnection( When it is false, the first statement will "jdbc:postgresql://localhost/bank", start a transaction. All subsequent "bankuser", statements are also in the same "123456"); transaction until it is committed or rolled try { back. conn.setAutoCommit(false); PreparedStatement st = conn.prepareStatement( "update accounts set balance=balance-? where accno=?"); try { st.setInt(1, getAmount()); st.setString(2, getFromAccNo()); st.executeUpdate(); This statement starts a } finally { transaction st.close(); } st = conn.prepareStatement( "update accounts set balance=balance+? where accno=?"); try { The whole transaction span st.setInt(1, getAmount()); st.setString(2, getToAccNo()); st.executeUpdate(); } finally { st.close(); } conn.commit(); If there is something wrong before you call } catch (Exception e) { commit(), catch it and call rollback(). This conn.rollback(); will rollback the transaction, i.e., cancel throw e; everything done in this transaction. } finally { conn.close(); } } catch (Exception e) { throw new RuntimeException(e); } } No matter a transaction is committed or rolled back, the next statement executed will start a new transaction. What if Tomcat crashes before you commit? You will never call commit() or rollback(). In that case the database server will note that the connection is broken (through timeout or some other mechanism) and will rollback the transaction automatically.

Database and Concurrency Issues

397

Now run the application and try to transfer $20 from account 003 to 002. Then check the data:

It is working. Now, simulate a failure by throwing an exception if the amount is $1:


public void onOk() { ... try { conn.setAutoCommit(false); PreparedStatement st = conn.prepareStatement( "update accounts set balance=balance-? where accno=?"); try { st.setInt(1, getAmount()); st.setString(2, getFromAccNo()); st.executeUpdate(); } finally { st.close(); } if (getAmount() == 1) { throw new Exception(); } st = conn.prepareStatement( "update accounts set balance=balance+? where accno=?"); try { st.setInt(1, getAmount()); st.setString(2, getToAccNo()); st.executeUpdate(); } finally { st.close(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } ... }

Run it again and try to transfer $1 from 001 to 002. You should receive an exception page:

398

Chapter 13 Database and Concurrency Issues

This is fine. Check the data to make sure that the balance of account 01 has not been reduced:

So this is working. Now, remove the code simulating the failure.

Connection pooling
Even though the application is working, there is a problem. It is quite slow to get a connection. The database has to authenticate the user and allocate quite some resources to establish a connection. At the moment you're getting a new connection every time you need to perform a money transfer. If there are many users performing transfers, it will be very slow. To solve this problem, can you just use a global connection? Unfortunately, no:

Database and Concurrency Issues

399

Bank application
2: Set auto commit to false and issue a statement which will start a transaction by page 2 is also committed!

1: Request 1 arrives

Transfer page object 1 5: Commit. The work done

Connection object

3: Request 2 arrives

Transfer page object 2

4: Set auto commit to false again (no effect) and issue another statement which belongs to the existing transaction 6: If it commits or rolls back, it will have no effect! If it issues the second update statement, it will start its own transaction including the second update statement only!

Therefore, for two concurrent operations, you must not use the same connection. So, using a global connection doesn't work. To really solve this problem, you can keep a pool of connections (just like a pool of pages):

Bank application Connection pool


1: Request 1 arrives 2: Give me a connection object

Transfer page object 1

... Connection object 1


3: Moved out of the pool 10: Returned to the pool

4: Start a transaction, issue statements, commit or rollback.

...
7: Moved out of the pool

5: Request 2 arrives

Transfer page object 2


8: Use it

6: Give me a connection object

9: Close

Connection object 1 ...

To implement this idea, it is quite easy because Tomcat can provide such a connection pool to you. To use it, modify web.xml:

400

Chapter 13 Database and Concurrency Issues

<web-app> <display-name>Bank</display-name> <servlet> <servlet-name>Bank</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> You are saying: My application <servlet-mapping> needs such a "resource". <servlet-name>Bank</servlet-name> Please give it to me. <url-pattern>/app</url-pattern> </servlet-mapping> <resource-ref> <res-ref-name>jdbc/bankDataSource</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </web-app> The container, i.e., Tomcat, should perform the authentication required. The name of the resource is "jdbc/bankDataSource". You'll use this name to refer to it.

This resource is a Java object of the class javax.sql.DataSource. You can ask this object to give you a connection. That's why it is called a data source You're just saying you need such a resource. This will not create the resource for you. The person deploying your application needs to create it in the context descriptor. The way to do it is a bit different in Tomcat 5.0 and 5.5:

Database and Concurrency Issues

401

Basic info of a resource. Tomcat 5.5 Must exactly match the info in web.xml <Context <Context docBase="c:/workspace/Bank/context" docBase="c:/workspace/Bank/context" path="/Bank" path="/Bank" reloadable="true"> reloadable="true"> <Resource <Resource name="jdbc/bankDataSource" name="jdbc/bankDataSource" auth="Container" auth="Container" type="javax.sql.DataSource"/> type="javax.sql.DataSource" <ResourceParams name="jdbc/bankDataSource"> driverClassName="org.postgresql.Driver" <parameter> url="jdbc:postgresql://localhost/bank" <name>driverClassName</name> username="bankuser" <value>org.postgresql.Driver</value> password="123456" </parameter> maxActive="20" <parameter> defaultAutoCommit="false" <name>url</name> timeBetweenEvictionRunsMillis="60000"/> </Context> <value> jdbc:postgresql://localhost/bank </value> </parameter> Additional parameters <parameter> for the resource if it is <name>username</name> a data source <value>bankuser</value> </parameter> <parameter> The data source object will <name>password</name> use these info to create <value>123456</value> JDBC connections </parameter> <parameter> <name>maxActive</name> There will be at most <value>20</value> 20 connections in the </parameter> pool <parameter> It will set auto commit <name>defaultAutoCommit</name> to false for each <value>false</value> connection it creates </parameter> <parameter> Every 60000 milliseconds (i.e., <name>timeBetweenEvictionRunsMillis</name> 60 seconds) it will check if there <value>60000</value> are any idle connections in the </parameter> pool and close some of them. </ResourceParams> When a connection is considered </Context> idle? By default it has stayed in the pool for 30 minutes, then it is considered idle.

Tomcat 5.0

As Tomcat (the data source object) will need to load the driver, you must copy the JDBC jar file into c:\tomcat\common\lib. You have put the Tapestry jar files into c:\tomcat\shared\lib. Why not put the JDBC jar file into there too? The "shared" folder should contain jar files for the web applications, but Tomcat itself will not use the jar files there. In contrast, if you put a jar file into the "common" folder, then Tomcat and all the web applications can use it. Now restart Tomcat so that it takes effect. As it is now in the "common" folder, you can delete it from your WEB-INF/lib folder. Now, in Transfer.java:

402

Chapter 13 Database and Concurrency Issues

Create a so called "initial context". You public void onOk() { can consider it something like a current try { directory when you are looking up a Context context = new InitialContext(); resource. DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); Connection conn = ds.getConnection(); try { Lookup your resource using its name. As mentioned ... before you'll refer to it as "jdbc/bankDataSource", but Ask the data conn.commit(); why add "java:comp/env" before it? This is because } catch (Exception e) { source for a you could lookup things like IP address using a name connection conn.rollback(); like "dns:www.yahoo.com" or lookup an LDAP entry throw e; This will not really using a name like "ldap:c=US,o=Foo". When you use a } finally { close the connection. name like "java:comp/env/jdbc/bankDataSource", it conn.close(); Instead, it will be means you're trying to lookup a resource for your web } returned to the pool. application. "comp" stands for "component" including } catch (Exception e) { web components (e.g., servlets). "env" stands for throw new RuntimeException(e); "environment". } }

Now run the application and try to transfer $40 from 002 to 001:

Then check the data:

It is working.

Concurrency issues
Suppose that you'd like to allow the user to withdraw from an account:

Database and Concurrency Issues

403

To do that, create a Withdraw page. This page is very similar to the Transfer page, so you can copy most of the code from there. Withdraw.html should be like:
<html> <span jwcid="error"/> <form jwcid="form"> Account: <input type="text" jwcid="accNo"/><br> Amount: <input type="text" jwcid="amount"/><br> <input type="submit" value="OK"/> </form> </html>

Withdraw.page is like:
<page-specification class="com.ttdev.bank.Withdraw"> <component id="error" type="Delegator"> <binding name="delegate" value="beans.delegate.firstError"/> </component> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> <binding name="delegate" value="beans.delegate"/> </component> <component id="accNo" type="TextField"> <binding name="value" value="accNo"/> </component> <component id="amount" type="TextField"> <binding name="value" value="amount"/> <binding name="translator" value="translator:number"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="literal:amount"/> </component> </page-specification>

Withdraw.java is like:
public abstract class Withdraw extends BasePage { public abstract String getAccNo(); public abstract int getAmount(); @Bean public abstract ValidationDelegate getDelegate(); public void onOk() { try { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); Connection conn = ds.getConnection(); try { int balance; PreparedStatement st = conn.prepareStatement( "select * from accounts where accno=?"); try { st.setString(1, getAccNo()); ResultSet rs = st.executeQuery(); rs.next(); balance = rs.getInt("balance"); } finally { st.close(); } if (balance < getAmount()) { ValidationDelegate delegate = (ValidationDelegate) getBeans().getBean( "delegate");

404

Chapter 13 Database and Concurrency Issues

delegate.setFormComponent((IFormComponent) getComponent("amount")); delegate.record("Insufficient balance", null); return; } st = conn.prepareStatement("update accounts set balance=? where accno=?"); try { st.setInt(1, balance-getAmount()); st.setString(2, getAccNo()); st.executeUpdate(); } finally { st.close(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); } } catch (Exception e) { throw new RuntimeException(e); } } }

The most important thing here is that you check the current balance first to make sure it has enough money for the withdrawal. If it is not enough, you will record an error in the validation delegate. Otherwise, you go ahead to set the balance. The new balance is the existing balance minus the amount of withdrawal. Now show the page using http://localhost:8080/Bank/app?service=page&page=Withdraw and try to withdraw $20 from account 001:

Currently the balances of the accounts are:

Now click "OK" and check the balances again:

Database and Concurrency Issues

405

So it is working. Try to withdraw $180, it should show an error:

The balance should remain unchanged:

So, it seems to be working fine. However, what if two users try to withdraw from the same account? Let's say user 1 is trying withdraw $100 from 001 and user 2 is trying to withdraw $50 from the same account. This is perfectly fine as there is $170 in the account. Let's consider what may happen. When user 1 clicks "OK", an HTTP request is sent to the server. If you check the HTML code:

406

Chapter 13 Database and Concurrency Issues

service=direct/0/Withdraw/form
Invoke the "Direct" service in Tapestry Let's ignore the meaning of the "0". It is unimportant for you.

Additional info for the Direct service

The path to a component which has a trigger() method. In this case, this is a component with id "form" in a page named "Withdraw", i.e., your Form component. The Direct service will load the Withdraw page, locate your Form component and call its trigger() method. In that method your Form component will rewind itself (and the components in it) including call its onOk() listener.

Then you can see that when the form is submitted, it calls the direct service. But what happens when two requests arrive almost at the same time? The first stop is Tomcat (see the diagram below). Tomcat will create a thread and let the thread execute the doPost() method of your application servlet object. If the HTTP request from user 2 also arrives at the moment, Tomcat will create another thread and let it execute the doPost() method of your application servlet object. As you have only one application servlet object, two or more threads may be executing on it simultaneously. Then, for thread 1, the application servlet invokes the Direct service (Java) object. In your application there is only one object for each type of service. So, you have only one Direct service object. Therefore, two or more threads may be executing on it simultaneously. Then, for thread 1, the Direct service object will try to get the Withdraw page object from the pool (create one if there is none). Suppose that there is one in the pool, so it is taken out of the pool and thread 1 will execute the trigger() method of the Form component, which will store the account number and amount into the properties of the page object and execute the onOk() listener. For thread 2, the Direct service object will also try to get the Withdraw page object from the pool. As the page object has already been taken out of the pool, it will have to create a new Withdraw page object. Then thread 2 will execute the trigger() method of the Form component in that page, which will store the account number and amount into the instance variables of that page object and execute the onOk() listener of that page object. Therefore, only one thread can be executing in a page object. This is different from the application servlet and the service objects. Finally, after the form is rewound in both threads, the page objects will be returned to the pool, so eventually the pool will contain two Withdraw page objects: As each thread will have its own page object, basically they will not interfere with each other. However, if the page objects need to access some shared resources such as a database, an state object (application scope or session scope), then you still need to coordinate their accesses.

Database and Concurrency Issues

407

1: Request 1 arrives 4: Request 2 arrives

Bank application Tomcat


2: Create it and let it handle the request class ApplicationServlet { public void doPost( HttpServletRequest request, 3: Execute each HttpServletResponse response) { step there ... ... 6: The two //call the DirectService object } threads are executing on } the same servlet object

A single application servlet object

Thread 1
5: Create another thread and let it handle the request

Thread 2

class DirectService { 7: Execute each public void service(...) { ... step there //load the page //find the component 8: Execute each //call its trigger() method step there. The two } threads are } executing on the same service object.

A single Direct service object

9: Execute the onOk()

Withdraw page 1 Withdraw page 2

Page pool

10: Execute the onOk(). Note that they are executing on two different page objects.

Now let's consider the current case in which two threads are accessing the database. What may happen when thread 1 and thread 2 are executing in the onOk() listener of the two different page objects? A possible scenario is:

408

Chapter 13 Database and Concurrency Issues

3: Set the balance to $70

Thread 1
1: Execute this part and find that the balance is $170 and >= $100

4: Set the balance to $120

Thread 2
2: Execute this part and find that the balance is $170 and >= $50

int balance; PreparedStatement st = conn.prepareStatement( "select * from accounts where accno=?"); try { st.setString(1, getAccNo()); ResultSet rs = st.executeQuery(); rs.next(); balance = rs.getInt("balance"); } finally { st.close(); } if (balance < getAmount()) { ValidationDelegate delegate = (ValidationDelegate) getBeans().getBean( "delegate"); delegate.setFormComponent((IFormComponent) getComponent("amount")); delegate.record("Insufficient balance", null); return; } st = conn.prepareStatement("update accounts set balance=? where accno=?"); try { st.setInt(1, balance-getAmount()); st.setString(2, getAccNo()); st.executeUpdate(); } finally { st.close(); } conn.commit();

That is: Steps 1 2 3 Writes the balance ($70) Withdrawal 1 Reads the balance ($170) and finds that it is OK, i.e., >= $100 Reads the balance ($170) and finds that it is OK, i.e., >= $50 Withdrawal 2

4 Writes the balance ($120) So, the end result is that $150 has been withdrawn from $170, but account still has $120! This is a serious problem. However, the sequence doesn't have to be this way. It could have been: Steps 1 2 3 Withdrawal 1 Reads the balance ($170) and finds that it is OK Writes the balance ($70) Reads the balance ($70) and finds that it is OK Withdrawal 2

4 Writes the balance ($20) This sequence is a fine. The final balance of $20 is correct. Another possible sequence is: Steps 1 2 Withdrawal 1 Withdrawal 2 Reads the balance ($170) and finds that it is OK Writes the balance ($120)

Database and Concurrency Issues

409

Steps 3

Withdrawal 1 Reads the balance ($120) and finds that it is OK

Withdrawal 2

4 Writes the balance ($20) The final balance is also $20. So this sequence is also fine. Let's call them sequence 1, 2 and 3 respectively. Why sequence 1 doesn't work but sequence 2 and 3 work? In sequence 2, withdrawal 1 is actually completed before withdrawal 2 is started. In sequence 3, withdrawal 2 is completed before withdrawal 1 is started. Sequences like these are called serialized execution. As the execution is serialized, the result is always correct. Obviously, sequence 1 is not a serialized execution. However, you cannot conclude that a non-serialized execution must produce an incorrect result. It may or may not produce an incorrect result. For example, assume everything is the same in sequence except that user 2 is trying withdraw from account 002 instead of 001: Steps 1 2 3 Writes the balance ($70) Withdrawal 1 Reads the balance ($170) and finds that it is OK Reads the balance ($480) and finds that it is OK Withdrawal 2

4 Writes the balance ($430) The result is correct, even though the sequence is not a serialized execution. Let's refer to this as sequence 4. Why sequence 1 doesn't work but sequence 4 works, even though both are non-serialized execution? The intuitive explanation is that in sequence 4, the two withdrawals are working on completely different data (two different accounts), so they will not interfere with each other. More technically, observe that in sequence 4, step 1 and step 2 are reading two different records, so if you switch their ordering, the end result will not be affected: Steps 1 2 3 Reads the balance ($170) and finds that it is OK Writes the balance ($70) Withdrawal 1 Withdrawal 2 Reads the balance ($480) and finds that it is OK

4 Writes the balance ($430) Similarly, step 3 and step 4 are writing to two different records, so if you switch their ordering, the end result will not be affected: Steps 1 2 3 Reads the balance ($170) and finds that it is OK Writes the balance ($430) Withdrawal 1 Withdrawal 2 Reads the balance ($480) and finds that it is OK

4 Writes the balance ($70) Similarly, step 2 is reading one record and step 3 is writing to another record, so if you switch their ordering, the end result will not be affected: Steps 1 2 3 Reads the balance ($170) and finds that it is OK Withdrawal 1 Withdrawal 2 Reads the balance ($480) and finds that it is OK Writes the balance ($430)

4 Writes the balance ($70) Now you have a serialized execution whose end result is the same as sequence 4. Therefore, you say that sequence 4 is a serializable execution even though it is not a serialized execution. As long as a sequence is serializable, it will produce a correct result. Now let's consider sequence 1 again: Steps 1 2 Withdrawal 1 Reads the balance ($170) and finds that it is OK Reads the balance ($170) and finds that it is OK Withdrawal 2

410

Chapter 13 Database and Concurrency Issues

Steps 3

Withdrawal 1 Writes the balance ($70)

Withdrawal 2

4 Writes the balance ($120) Can you switch step 1 and step 2? As they are reading the same record, which one reads first doesn't matter at all. They will still read the same balance. So they can be switched: Steps 1 2 3 Reads the balance ($170) and finds that it is OK Writes the balance ($70) Withdrawal 1 Withdrawal 2 Reads the balance ($170) and finds that it is OK

4 Writes the balance ($120) Can you switch step 3 and step 4? As they are trying to write to the same record, switching them will change the final balance. So you can't switch them. So, it is impossible to "move" withdrawal 2 before withdrawal 1. Can you "move" withdrawal 1 before withdrawal 2? Consider sequence 1 again: Steps 1 2 3 Writes the balance ($70) Withdrawal 1 Reads the balance ($170) and finds that it is OK Reads the balance ($170) and finds that it is OK Withdrawal 2

4 Writes the balance ($120) If you could switch step 2 and 3, then the result would be serialized. However, step 2 is reading account 001 while step 3 is writing to account 001, switching them will cause step 2 to read a different balance and therefore the end result will be changed. So you can't switch them. Therefore, it's impossible to "move" withdrawal 1 before withdrawal 2. Therefore, sequence 1 is non-serializable and will produce an incorrect result. In summary, when can you switch two steps? You can't switch if both are writing to the same records or one is writing but the other is reading. In this sense, you can consider writing a "bad" operation because it prevents you from serializing the sequence. This kind of problem is called "race condition" and is very difficult to spot. You can use the withdrawal code above and it may work 9999 out of 10000 withdrawals because most withdrawals are operated on different accounts. Even if they operate on the same account, as the withdrawal operation is quick to execute, one may have finished before the other is started. How to prevent this problem? The idea is to prevent the write-write combination and read-write combination. If a transaction writes something to the database, no other transactions can change it. If it reads something from the database, no other transactions can change it. While the former is OK, the latter turns out to be extremely inefficient. Therefore, many database servers provide the former entirely but provide only something weaker than the latter. To enable this feature, you can set the "transaction isolation level". For example, in your code, you can write:
public abstract class Withdraw extends BasePage { ... public void onOk() { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); Connection conn = ds.getConnection(); try { conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); int balance; PreparedStatement st = conn.prepareStatement( "select * from accounts where accno=?"); ... conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); } } }

Database and Concurrency Issues

411

You're setting the transaction isolation level to TRANSACTION_SERIALIZABLE. The term of TRANSACTION_SERIALIZABLE seems to be suggesting that this will ensure the transactions are serializable and solve all your problems. However, this is not the case! The exact effect depends on the database server. For PostgreSQL (and Oracle), it has two effects. First, it forbids the write-write combination as shown below: Steps 1 2 3 Writes X Reads X Reads X Transaction 1 Transaction 2

4 Writes X (This step will be blocked!) PostgreSQL will not allow step 4 to execute. Instead, it will block transaction 2 and wait until the transaction 1 commits or rolls back. If it commits, the server will return an error to transaction 2. Typically transaction 2 should rollback, show the error to the user and let him try again. If transaction 1 rolls back for some reason, then PostgreSQL will complete step 4 and transaction 2 will commit. This is the first effect of setting the transaction isolation to serializable. What is the second effect? Ideally it would forbid writing after something is read. But it only provides something weaker. Consider step 2 above, transaction 2 will always read the value of X as when the transaction was started. Conceptually it is as if a snapshot is taken on the database when each transaction is started and then that transaction can only see that snapshot (until it commits or rolls back). Even if transaction 1 had written to X, transaction 2 would still read the old value of X. It means that you can switch step 2 and step 3 without changing the end result: Steps 1 2 3 Reads X Writes X Reads X Transaction 1 Transaction 2

4 Writes X Of course, if transaction 2 itself has written to X, then it will read the value it wrote to it. In summary, if you set the transaction isolation to serializable, in PostgreSQL the first effect is that a write-write combination is impossible; the second effect is that even if you have a read-write combination, you can still switch them without changing the end result. So it seems that if you set the isolation to serializable, given any sequence, they can't have write-write combinations. If they have read-write combinations, you can switch their steps so that they become serialized. For many scenarios, this is true, but it is not always true. Consider: Steps 1 2 3 Writes Y to X Reads Y Reads X Transaction 1 Transaction 2

4 Writes X to Y This is like there are two people in a building. Person 1 is on story X (e.g., 10) while person 2 is on story Y (e.g., 20). Both are trying to take a lift to go to the other story meet the other person. Suppose that you have set the isolation level to TRANSACTION_SERIALIZABLE. If person 1 is completed before person 2 is started, they will meet at story 20. Steps 1 2 3 Transaction 1 Reads Y (20, the value at the start of the transaction) Writes Y to X (20) Reads X (20, the value at the start of the transaction) Transaction 2

4 Writes X to Y (20) If person 2 is completed before person 1 is started, they will meet at 10:

412

Chapter 13 Database and Concurrency Issues

Steps 1 2 3

Transaction 1

Transaction 2 Reads X (10, the value at the start of the transaction) Writes X to Y (10)

Reads Y (10, the value at the start of the transaction)

4 Writes Y to X (10) No matter which case it is, finally they will meet. However, for the sequence: Steps 1 2 3 Writes Y to X (10) Transaction 1 Reads Y (10, the value at the start of the transaction) Reads X (20, the value at the start of the transaction) Transaction 2

4 Writes X to Y (20) Then person 1 will go to story 10 while person 2 will go to story 20 and they will miss each other. While one is going down the other is going up. Obviously a serialized execution will never produce this result and therefore it is an incorrect result. How could it be? This is because as transaction 2 reads 20 as the value of X but transaction 1 is setting X to 10, transaction 2 must be conceptually before transaction 1 (otherwise it would read a value of 10). However, the same is true for transaction 1. As it is reading 10 as the value of Y but transaction 2 is setting Y to 20, conceptually it must be before transaction 2 (otherwise it would read a value of 20). Obviously they must be mutually before each other and this is impossible. This is happening because they are destroying what the other is reading (person 1 thinks person 2 is on story 10 but it is no longer true; person 2 thinks person 1 is on story 20 but it is no longer true), but they don't write to the same thing (if they wrote to the same thing, the write-write combination would be denied by the database server):

Transaction 1
Premise
dest roy

Transaction 2
Premise
dest roy

Changes
no common changes

Changes

This is the price that you pay by using the weaker protection. That is, with this weaker protection, for most cases it should work fine. But for those transactions that are destroying each other's premises without writing to the same thing, this weaker protection will fail to protect you. To handle this kind of situation, there are two solutions. One is to make sure your premise doesn't change once it is read. For example, many database server allows you to issue something like "select * from table where recordId=X for update". This will not only read X, but also treat it like it has been written so that other transactions can't write to it: Steps 1 2 3 Blocked: Writes Y to X (10) Transaction 1 Reads Y for update (10, the value at the start of the transaction) Reads X for update (20, the value at the start of the transaction) Transaction 2

4 Blocked: Writes X to Y (20) In this case, at step 3 PostgreSQL will block transaction 1 until transaction 2 commits or rolls back. But at step 4

Database and Concurrency Issues

413

transaction 2 is blocked too waiting transaction 1 to commit or rollback. This creates a deadlock. PostgreSQL will notice that a deadlock has occurred sooner or later and will rollback one of them. Another solution is to make them change something in common. For example, you could force anyone to use a single lift. Before anyone can move, he must obtain the lift. Let Z=0 represent that the lift is free. Z=1 means it is in use. Then you can: Steps 1 2 3 4 5 6 7 Writes 1 to Z if it is 0 (assume that it was 0 at the start of the transaction) Writes Y to X (10) Writes 0 to Z Blocked: Writes 1 to Z if it is 0 (assume that it was 0 at the start of the transaction) Writes X to Y (20) Transaction 1 Reads Y (10, the value at the start of the transaction) Reads X (20, the value at the start of the transaction) Transaction 2

8 Writes 0 to Z As transaction 2 is trying to write to Z at step 5, but it has been set by transaction 1, it will be blocked. When transaction 1 commits, transaction 2 will receive an error and rollback. Of course, in this example, this solution is not as good as the first one. It is forcing everyone to use the same lift and is reducing the concurrency unnecessarily. Let's consider some scenarios with your banking application. Suppose that you can close (delete) an account if its balance is $0. Otherwise you can't delete it. On the other hand, you can deposit into an account only if it exists. Suppose the current balance of account 001 is $0. One transaction is trying to delete it, while another transaction is trying to deposit $100 into it. A possible sequence may be: Steps 1 2 3 4 5 Transaction 1 select * from accounts where accNo='001' Check if balance is 0 select * from accounts where accNo='001' Check if a record is found update accounts where accNo='001' set balance=balance+100 Transaction 2

6 delete from accounts where accNo='001' Obviously this sequence will delete the account even if it has $100 in it. Fortunately, this sequence is forbidden if you set the transaction isolation level to TRANSACTION_SERIALIZABLE because step 5 and step 6 are trying to change the same record (Yes, delete is considered a write). Let's consider another scenario. Suppose that you need to record each withdrawal and deposit for audit purpose. Therefore, you need to an extra table to store these records, which may be like: accNo 001 001 003 $190 -$20 $20 amount dateTime January 10, 2005 January 11, 2005 January 9, 2005

... ... ... For example, the first record says that on someone deposited $190 into account 001 on January 10, 2005. The second record has a negative amount. It means it is a withdrawal. So if there are no more deposits and withdrawals for account 001, the current balance is $170. Let's call this table the deposits table.

414

Chapter 13 Database and Concurrency Issues

As you are recording each deposit/withdrawal, there is no need to keep track of the balance in the accounts table: accNo 001 002 003 ... You can just delete this field from the table. $200 $400 $100 ... balance

Now, let's consider the withdrawal operation again. Suppose you're trying to withdraw $100 from account 001. In order to make sure the balance is enough, you need to calculate the current balance from the deposits: Steps 1 2 3 Transaction 1 select sum(amount) as balance from deposits where accNo='001' Check if balance is >= $100

insert into deposits values('001', -$100, current time) What if you have another withdrawal transaction trying to withdraw $80? A sequence could be: Steps 1 2 3 4 5 6 insert into deposits values('001', -$100, current time) Transaction 1 select sum(amount) as balance from deposits where accNo='001' Check if balance is >= $100 select sum(amount) as balance from deposits where accNo='001' Check if balance is >= $80 Transaction 2

insert into deposits values('001', -$80, current time) Here the two transactions are destroying each other's premise. For transaction 1, the premise is that the balance calculated is >= $100, but transaction 2 is destroying it by adding a new record to the deposits table. For transaction 2, the premise is that the balance calculated is >= $80, but transaction 1 is destroying it by adding a new record to the deposits table. The question is, do they write something in common? They will both insert a new record into the deposits table. So they are not writing to the same thing. So, this is a situation that can't be prevented by setting the isolation level to serializable. To solve this problem, remember in general you have two solutions: One is to lock what you read. In this case, you may try to add "for update" to the select statement: Steps 1 2 3 4 5 6 insert into deposits values('001', -$100, current time) insert into deposits values('001', -$80, current time) Transaction 1 select sum(amount) as balance from deposits where accNo='001' for update Check if balance is >= $100 select sum(amount) as balance from deposits where accNo='001' for update Check if balance is >= $80 Transaction 2

Database and Concurrency Issues

415

But this will not work because you can't use "select XXX for update" when XXX is the "sum" SQL function. You may instead select all the record for account 001 for update: Steps 1 2 3 4 5 6 insert into deposits values('001', -$100, current time) Transaction 1 select * from deposits where accNo='001' for update Loop through the records to get balance and check if it is >= $100 select * from deposits where accNo='001' for update Loop through the records to get balance and check if it is >= $80 Transaction 2

insert into deposits values('001', -$80, current time) This will lock the two existing records for account 001, but it will not prevent another transaction from adding a new record for account 001. So this still doesn't work. So you have to use another solution: Make the two transactions write to something in common (typically a lock flag). You can imagine that each account has a boolean lock flag. When you need to withdraw, you set the flag to true. Once the flag is true, no other transaction can withdraw from it. After you are done, you set the flag to false. The implement this idea, add a field to the accounts table: accNo 001 002 003 ... Then the transactions will be like: Steps 1 2 3 4 5 6 7 8 9 10 insert into deposits values('001', -$100, current time) update accounts accNo='001' set locked=false where Transaction 1 select locked from accounts where accNo='001' and check if locked is false update accounts accNo='001' set locked=true deposits where where false false false ... Transaction 2 locked

select sum(balance) accNo='001'

from

Check if the balance is >= $100 select locked from accounts where accNo='001' and check if locked is false update accounts accNo='001' set locked=true deposits where where

select sum(balance) accNo='001'

from

Check if the balance is >= $80

416

Chapter 13 Database and Concurrency Issues

Steps 11 12

Transaction 1

Transaction 2 insert into deposits values('001', -$80, current time)

update accounts set locked=false where accNo='001' Note that at step 5 transaction will find that the flag is false because it will see the value when it started. So it will proceed to step 6. However, when it tries to set the flag to true at step 6, the database server will block it and wait until transaction 1 commits. Then transaction 2 will receive an error and should rollback. Having to lock and unlock is quite troublesome and error-prone. Fortunately, in practice this may not be necessary. If you have an id for each record in the deposits table (a bank transaction id): id 0 1 2 001 001 003 accNo $190 -$20 $20 amount dateTime January 10, 2005 January 11, 2005 January 9, 2005

3 ... ... ... Then when you need to add a new record, you must obtain an id. How to obtain an id? You could use another table to use the largest id used: usage bank-transaction-id 3 maxId

... ... Then you read the id (3), increment it to get the next id (4) and write it back. So, the two transactions will have to write to the same thing and the artificial lock flag will become unnecessary. Note that some database servers provides builtin support for generating ids. For example, in PostgreSQL, you can: SQL create sequence bank-transaction-id insert into deposits values(nextval('bank-transaction-id'), '001', ...) Explanation This creates a "sequence" named "banktransaction-id".

The "nextval" function will return the next value of the bank-transaction-id sequence. However, the increment happening inside the sequence is not considered part of the transaction. It means two transactions can happily call nextval() without blocking. Therefore, this will not help you address the concurrency problem.

Long transaction
Suppose that you need to keep the data of your bank customers such as their names and addresses. Assume that each customer is assigned a customer id. So, you need a table like: id 001 002 Bill Gates Linus Torvalds name xxx yyy ... address

003 ... Let's call it the customers table.

Suppose that you already have quite some customers in the table. You'd like to allow the user to enter the id of a customer and edit his data:

Database and Concurrency Issues

417

The whole process can be divided into few steps: 1 2 3 4 5 6

Render the first User Submit the first page. Read the customer User Submit the second page. No need to input Get the id and activate data using the id. input page. Write the read the database at the second page. Render the second customer data into the all. page. database. You need to ensure that the whole process is not affected by concurrency problems. You could start a transaction at the beginning and commit it at the end. However, as the first three steps don't really use the database at all, you only need to start the transaction in step 4 and commit it in step 6: 1 ... 2 3 4 5 6

User ... Start a transaction User blah blah blah and input and blah blah blah. input commit the transaction. This way, you can prevent two users trying to edit the same customer. If they do, only one will succeed and the other will receive an error because they're trying to change the same thing (the same customer record). However, this is not a good way to do it. Why? Because the transaction includes a session of a user input (step 5). This may take quite long. If a database transaction lasts long, the database server will have to do a lot of work to make sure the transactions don't see the changes made by the others. This will badly affect the performance of the database server. To solve this problem, you should use a separate in each step: 1 ... 2 3 4 5 6

Start a transaction User Start a transaction and and blah blah blah input blah blah blah and commit the transaction. and commit the transaction. Of course, to make sure the whole process look like a single transaction, you need to ensure that the customer data hasn't been changed in step 6: User ... input

418

Chapter 13 Database and Concurrency Issues

1 ...

Start a transaction User Start a transaction, and blah blah blah input make sure the and commit the customer data hasn't transaction. changed, then update it and commit the transaction. You call the whole process a "business transaction". As shown here, a single business transaction may contain two or more database transactions (steps 4 and 6). In this context a database transaction is also called a "system transaction". Typically you can write to the database only in the last database transaction in a business transaction (step 6 in this case). Why? If you wrote to the database in say step 4, then later you could not rollback the change even if you wanted to. In addition, after step 4, other transaction would see the change made and this would cause concurrency problems. User ... input OK, let's do it. Create an GetCustomerId page and an EditCustomer page. GetCustomerId.html should be like:
<html> <span jwcid="error"/> <form jwcid="form"> Customer id: <input type="text" jwcid="customerId"/> <input type="submit" value="OK"/> </form> </html>

GetCustomerId.page is:
<page-specification class="com.ttdev.bank.GetCustomerId"> <component id="error" type="Delegator"> <binding name="delegate" value="beans.delegate.firstError"/> </component> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> <binding name="delegate" value="beans.delegate"/> </component> <component id="customerId" type="TextField"> <binding name="value" value="customerId"/> </component> </page-specification>

GetCustomerId.java is:
public abstract class GetCustomerId extends BasePage { public abstract String getCustomerId(); @Bean public abstract ValidationDelegate getDelegate(); @InjectPage("EditCustomer") public abstract EditCustomer getEditCustomerPage(); public IPage onOk() { getEditCustomerPage().setCustomerId(getCustomerId()); return getEditCustomerPage(); } }

This is straightforward. EditCustomer.html is:


<html> <form jwcid="form"> <input type="Hidden" jwcid="oldCustomer"/> Name: <input type="text" jwcid="name"/><p> Address: <input type="text" jwcid="address"/><p> <input type="submit" value="OK"/> </form> </html>

EditCustomer.page is:

Database and Concurrency Issues

419

<page-specification class="com.ttdev.bank.EditCustomer"> <property name="currentCustomer" initial-value="new com.ttdev.bank.Customer()"/> <component id="form" type="Form"> Store the Customer object (to <binding name="listener" value="listener:onOk"/> be defined later) that is being </component> edited <component id="oldCustomer" type="Hidden"> This is most important thing here. <binding name="value" value="oldCustomer"/> Store the Customer object into a </component> hidden field (Yes, you can store any <component id="name" type="TextField"> object into a hidden field as long as it <binding name="value" value="currentCustomer.name"/> implements Serializable). When the </component> form is rendered, both <component id="address" type="TextField"> <binding name="value" value="currentCustomer.address"/> "currentCustomer" and "oldCustomer" will be equal. When the form is </component> submitted, "currentCustomer" will be </page-specification> Show the name from that Customer modified by the two TextFields but object. On submission store the name "oldCustomer" will be set to the back to that Customer object. original customer thanks to the Hidden component. Then you compare it to the data in the database Similarly, let the user edit the address and see if the data has been changed of that Customer object by another user.

EditCustomer.java is:
public abstract class EditCustomer extends BasePage implements PageBeginRenderListener { public abstract String getCustomerId(); public abstract Customer getOldCustomer(); public abstract Customer getCurrentCustomer(); public abstract void setCustomerId(String customerId); Load the Customer object for public abstract void setCurrentCustomer(Customer customer); rendering. Do it in system public abstract void setOldCustomer(Customer customer); transaction. public void pageBeginRender(PageEvent event) { if (!event.getRequestCycle().isRewinding()) { startTransaction(); try { Customer customer = loadCustomer(getCustomerId()); setCurrentCustomer(customer); setOldCustomer(customer); commit(); } catch (Exception e) { To be edited rollback(); The customer id is a throw new RuntimeException(e); property. It should be } setup by the caller who } invoked this page. To be stored in a hidden field } private void startTransaction() { ... } private Customer loadCustomer(String customerId) { ... } private void commit() { ... } private void rollback() { ... } }

Do it only when rendering Load the Customer object from the database

Customer.java is:

420

Chapter 13 Database and Concurrency Issues

public class Customer implements Serializable { private static final long serialVersionUID = 469072691908794223L; private String id; private String name; Implements Serializable so private String address; that it can be stored in a hidden field public Customer() { } public Customer(String id, String name, String address) { this.id = id; To implement Serializable, you need a this.name = name; default constructor (i.e., a constructor this.address = address; that takes no argument) } public boolean equals(Object obj) { As you'll be comparing the return equals((Customer) obj); original Customer object to } the data in the database, you public boolean equals(Customer customer) { need an equals() method. return id.equals(customer.id) && name.equals(customer.name) && address.equals(customer.address); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } } Now, let's define the method bodies in EditCustomer.java:

Database and Concurrency Issues

421

public abstract class EditCustomer extends BasePage implements PageBeginRenderListener { private Connection conn; As you need to use the connection in several ... methods, make it an instance variable. private void startTransaction() { try { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup( "java:comp/env/jdbc/bankDataSource"); conn = ds.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); } catch (Exception e) { throw new RuntimeException(e); Get a database connection and set } the transaction isolation level to } TRANSACTION_SERIALIZABLE private void commit() { try { conn.commit(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } private void rollback() { try { conn.rollback(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } private Customer loadCustomer(String customerId) { try { Issue a select statement and PreparedStatement st = conn.prepareStatement( specify the customer id. "select * from customers where id=?"); try { st.setString(1, customerId); ResultSet rs = st.executeQuery(); if (!rs.next()) { throw new RuntimeException("Customer has been deleted"); } return new Customer( Create a new Customer object customerId, and set its id, name and address rs.getString("name"), rs.getString("address")); } finally { st.close(); } } catch (SQLException e) { throw new RuntimeException(e); } } }

Let's add a dummy onOk() listener in EditCustomer.java:


public abstract class EditCustomer ... { ... public void onOk() { } }

Now, you're about to test run the application. If you haven't created the customers table, do it now. Then add a couple of customers to it:

422

Chapter 13 Database and Concurrency Issues

Now, run the application by going to http://localhost:8080/Bank/app?service=page&page=GetCustomerId, you should see:

Enter 001 as the id and click "OK", you should see:

So, this part is working. Now, let's work on the submission of the form. Add some code to the onOk() listener in EditCustomer.java:

Database and Concurrency Issues

423

public abstract class EditCustomer ... { public abstract Customer getCurrentCustomer(); public abstract Customer getOldCustomer(); ... @InjectPage("GetCustomerId") public abstract GetCustomerId getQueryIdPage();

Save the Customer into the database. Do it in a system @InjectPage("EditCustomerSuccess") transaction. public abstract IPage getSuccessPage(); Load the Customer object from the database and public IPage onOk() { compare it with the old startTransaction(); Customer object. If they try { are not equal, another String customerId = getOldCustomer().getId(); user has changed the Customer customerFromDB = loadCustomer(customerId); data so you throw an if (!getOldCustomer().equals(customerFromDB)) { exception. throw new RuntimeException("Data has changed"); } Customer currentCustomer = getCurrentCustomer(); The current Customer is currentCustomer.setId(customerId); created and then updated saveCustomer(currentCustomer); by the two TextFields. As commit(); only the name and return getSuccessPage(); address are updated, the } catch (Exception e) { id is not set. So, you need rollback(); to set it id using the id of getQueryIdPage().getDelegate().setFormComponent(null); the original Customer getQueryIdPage().getDelegate().record(e.getMessage(), null); object. Then save it to the return getQueryIdPage(); database }

If in the process there is anything wrong (e.g., the data has been changed by another user), rollback } the transaction, record the error message in validation delegate and show the GetCustomerId page again. <page-specification class="com.ttdev.bank.EditCustomer"> <property name="currentCustomer" initial-value="new com.ttdev.bank.Customer()"/> <component id="form" type="Form"> <binding name="listener" value="listener:onOk"/> </component> <component id="oldCustomer" type="Hidden"> <binding name="value" value="oldCustomer"/> </component> <component id="name" type="TextField"> <binding name="value" value="currentCustomer.name"/> </component> <component id="address" type="TextField"> <binding name="value" value="currentCustomer.address"/> </component> </page-specification>

} private void saveCustomer(Customer customer) { ... }

Now, define the saveCustomer() method:


public abstract class EditCustomer ... { ... private void saveCustomer(Customer customer) { try { PreparedStatement st = conn.prepareStatement("update customers set name=?, address=? where id=?"); try { st.setString(1, customer.getName()); st.setString(2, customer.getAddress()); st.setString(3, customer.getId()); st.executeUpdate(); } finally { st.close(); } } catch (SQLException e) { throw new RuntimeException(e); } }

424

Chapter 13 Database and Concurrency Issues

This is straightforward. You just issue an update statement to update the record. Next, create the EditCustomerSuccess page. EditCustomerSuccess.html is:
<html> Customer is saved! </html>

EditCustomerSuccess.page contains just an empty <page-specification> element. Now, run the application again and try to edit customer 001. Change the name and address:

Click "OK" and you should see:

Check if the database has been modified:

So it is working. Now, edit customer 001 again:

Database and Concurrency Issues

425

Do not click "OK" yet. Open another browser to edit customer 001:

Click "OK":

Now return to the first browser and click "OK", you should see:

This is good. You've detected the change successfully. Check the database to make sure you have not overwritten the record:

426

Chapter 13 Database and Concurrency Issues

Right, you didn't overwrite the record. So it is working.

Dividing the application into layers


Even though it is working, however, there is something to be improved. Consider the whole business process of editing a customer, the logic involved can classified into:

Domain logic: That a customer has an id, a name and address, how two customers are considered equal and etc. Business transaction: Starting a system transaction, committing a system transaction, checking if the data has been modified by another user and etc. Persistence: Obtaining a database connection, issuing SQL statement to load a customer object and etc. User interface (UI): Storing the customer object into the "currentCustomer" property for display, getting the updated customer object from "currentCustomer" property and etc.

However, at the most of the logic is packed into the EditCustomer class. The only thing outside EditCustomer is the domain logic in the Customer. This is not good. A single class should contain exactly one of the above type of logic. For example, let's extract the persistence logic from EditCustomer first. You can consider the database as a set of customer objects:
public interface Customers { Customer loadCustomer(String customerId); void saveCustomer(Customer customer); }

Then create a CustomersInDB class to implement this interface:


public class CustomersInDB implements Customers { private Connection conn; public CustomersInDB(Connection conn) { this.conn = conn; } public Customer loadCustomer(String customerId) { try { PreparedStatement st = conn.prepareStatement("select * from customers where id=?"); try { st.setString(1, customerId); ResultSet rs = st.executeQuery(); if (!rs.next()) { throw new RuntimeException("Customer has been deleted"); } return new Customer( customerId, rs.getString("name"), rs.getString("address")); } finally { st.close(); } } catch (SQLException e) { throw new RuntimeException(e); } } public void saveCustomer(Customer customer) { try {

Database and Concurrency Issues

427

PreparedStatement st = conn.prepareStatement("update customers set name=?, address=? where id=?"); try { st.setString(1, customer.getName()); st.setString(2, customer.getAddress()); st.setString(3, customer.getId()); st.executeUpdate(); } finally { st.close(); } } catch (SQLException e) { throw new RuntimeException(e); } } }

Then EditCustomer.java can be simplified as:


public abstract class EditCustomer ... { ... public void pageBeginRender(PageEvent event) { ... } public IPage onOk() { ... } private void startTransaction() { ... } private void commit() { ... } private void rollback() { ... } private Customer loadCustomer(String customerId) { Customers customers = new CustomersInDB(conn); return customers.loadCustomer(customerId); } private void saveCustomer(Customer customer) { Customers customers = new CustomersInDB(conn); customers.saveCustomer(customer); } }

Are you free from database now? Not yet:


public abstract class EditCustomer ... { ... private void startTransaction() { try { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); conn = ds.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); } catch (Exception e) { throw new RuntimeException(e); } } private void commit() { try { conn.commit(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } private void rollback() { try { conn.rollback(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } ... }

You're using the database's transaction support. To hide the database, you consider that you don't really need a database transaction, any system transaction will do fine:

428

Chapter 13 Database and Concurrency Issues

public interface SystemTransactionFactory { SystemTransaction start(); } public interface SystemTransaction { void commit(); void rollback(); }

Then create a DBTransactionFactory and a DBTransaction class implementing these two interfaces:
public class DBTransactionFactory implements SystemTransactionFactory { public SystemTransaction start() { try { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); Connection conn = ds.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); return new DBTransaction(conn); } catch (Exception e) { throw new RuntimeException(e); } } } public class DBTransaction implements SystemTransaction { private Connection conn; public DBTransaction(Connection conn) { this.conn = conn; } public void commit() { try { conn.commit(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } public void rollback() { try { conn.rollback(); conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } }

Then EditCustomer.java can be simplified as:


public abstract class EditCustomer ... { private Connection conn; private SystemTransaction transaction; private SystemTransactionFactory transactionFactory = new DBTransactionFactory(); ... public void pageBeginRender(PageEvent event) { ... } public IPage onOk() { ... } private void startTransaction() { transaction = transactionFactory.start(); } private void commit() { transaction.commit(); } private void rollback() { transaction.rollback(); } private Customer loadCustomer(String customerId) { Customers customers = new CustomersInDB(conn); return customers.loadCustomer(customerId); } private void saveCustomer(Customer customer) { Customers customers = new CustomersInDB(conn); customers.saveCustomer(customer); } }

Database and Concurrency Issues

429

Now it is much better. The database is basically hidden. However, you still have an instance variable containing a database connection and you are still creating CustomerInDB. You need that instance variable because you need to pass a connection to the constructor of CustomersInDB. So these two issues are related. In order to get rid of them, you can ask the system transaction to give you a snapshot on the customers. Therefore, modify the SystemTransaction interface:
public interface SystemTransaction { void commit(); void rollback(); Customers getCustomersSnapshot(); }

Then implement this method in DBTransaction:


public class DBTransaction implements SystemTransaction { private Connection conn; ... public Customers getCustomersSnapshot() { return new CustomersInDB(conn); } }

Then modify EditCustomer.java:


public abstract class EditCustomer ... { private Connection conn; private SystemTransaction transaction; private SystemTransactionFactory transactionFactory = new DBTransactionFactory(); ... public void pageBeginRender(PageEvent event) { ... } public void onOk(IRequestCycle cycle) { ... } private void startTransaction() { transaction = transactionFactory.start(); } private void commit() { transaction.commit(); } private void rollback() { transaction.rollback(); } private Customer loadCustomer(String customerId) { Customers customers = new CustomersInDB(conn); Customers customers = transaction.getCustomersSnapshot(); return customers.loadCustomer(customerId); } private void saveCustomer(Customer customer) { Customers customers = new CustomersInDB(conn); Customers customers = transaction.getCustomersSnapshot(); customers.saveCustomer(customer); } }

However, there is a problem. In the future you may need to access say the accounts snapshot in a transaction, then you'll have to add a new method to the SystemTransaction interface:
public interface SystemTransaction { void start(); void commit(); void rollback(); Customers getCustomersSnapshot(); Accounts getAccountsSnapshot(); }

It means this interface will keep growing forever. This is no good. To solve this problem, let's extract the responsibility of taking a snapshot into a separate interface:
public interface CustomersSnapshotTaker { Customers getCustomersSnapshot(SystemTransaction transaction); }

You give a system transaction to this snapshot taker and it will take a snapshot of the customers for that system transaction (remember, each system transaction has its own snapshot) and then return the snapshot to you. Next, create a class CustomersInDBSnapshotTaker to implement this interface:
public class CustomersInDBSnapshotTaker implements CustomersSnapshotTaker { public Customers getCustomersSnapshot(SystemTransaction transaction) {

430

Chapter 13 Database and Concurrency Issues

return new CustomersInDB(((DBTransaction)transaction).getConn()); } }

This snapshot taker works with a DBTransaction only. It will take a snapshot on the database for that database transaction. As you need to get the connection from the DBTransaction, you need to define such a getter:
public class DBTransaction implements SystemTransaction { private Connection conn; ... public Customers getCustomersSnapshot() { return new CustomersInDB(conn); } public Connection getConn() { return conn; } }

In addition, shown above, you no longer need to return a snapshot from a transaction. This is now done by the database snapshot taker. Modify EditCustomer.java to use a snapshot taker:
public abstract class EditCustomer ... { private SystemTransaction transaction; private SystemTransactionFactory transactionFactory = new DBTransactionFactory(); private CustomersSnapshotTaker snapshotTaker = new CustomersInDBSnapshotTaker(); ... public void pageBeginRender(PageEvent event) { ... } public IPage onOk() { ... } private void startTransaction() { ... } private void commit() { ... } private void rollback() { ... } private Customer loadCustomer(String customerId) { Customers customers = transaction.getCustomersSnapshot(); Customers customers = snapshotTaker.getCustomersSnapshot(transaction); return customers.loadCustomer(customerId); } private void saveCustomer(Customer customer) { Customers customers = transaction.getCustomersSnapshot(); Customers customers = snapshotTaker.getCustomersSnapshot(transaction); customers.saveCustomer(customer); } }

Now, EditCustomer.java basically knows little about database at all. The only exception is:
public abstract class EditCustomer ... { private SystemTransaction transaction; private SystemTransactionFactory transactionFactory = new DBTransactionFactory(); private CustomersSnapshotTaker snapshotTaker = new CustomersInDBSnapshotTaker(); ... }

But let's ignore it for now. You'll take care of it later. Now let's extract the business transaction logic:
public abstract class EditCustomer ... { ... public void pageBeginRender(PageEvent event) { if (!event.getRequestCycle().isRewinding()) { startTransaction(); try { Customer customer = loadCustomer(getCustomerId()); setCurrentCustomer(customer); setOldCustomer(customer); commit(); } catch (Exception e) { rollback(); throw new RuntimeException(e); } }

Database and Concurrency Issues

431

} public IPage onOk() { startTransaction(); try { String customerId = getOldCustomer().getId(); Customer customerFromDB = loadCustomer(customerId); if (!getOldCustomer().equals(customerFromDB)) { throw new RuntimeException("Data has changed"); } Customer currentCustomer = getCurrentCustomer(); currentCustomer.setId(customerId); saveCustomer(currentCustomer); commit(); return getSuccessPage(); } catch (Exception e) { rollback(); getQueryIdPage().getDelegate().setFormComponent(null); getQueryIdPage().getDelegate().record(e.getMessage(), null); return getQueryIdPage(); } } }

Let's extract the logic into a new interface named EditCustomerService:


public interface EditCustomerService { Customer getCustomerForEdit(String customerId); void saveCustomer(Customer updatedCustomer, Customer oldCustomer); }

This interface is modeling the business transaction API for editing a customer. Create a default implementation class for it:
public class DefaultEditCustomerService implements EditCustomerService { private SystemTransaction transaction; private SystemTransactionFactory transactionFactory; private CustomersSnapshotTaker snapshotTaker; public Customer getCustomerForEdit(String customerId) { startTransaction(); try { Customer customer = loadCustomer(customerId); commit(); return customer; } catch (Exception e) { rollback(); throw new RuntimeException(e); } } public void saveCustomer(Customer updatedCustomer, Customer oldCustomer) { startTransaction(); try { String customerId = oldCustomer.getId(); Customer customerFromDB = loadCustomer(customerId); if (!oldCustomer.equals(customerFromDB)) { throw new RuntimeException("Data has changed"); } saveCustomer(updatedCustomer); commit(); } catch (RuntimeException e) { rollback(); throw e; } } private Customer loadCustomer(String customerId) { Customers customers = snapshotTaker.getCustomersSnapshot(transaction); return customers.loadCustomer(customerId); } private void startTransaction() { transaction = transactionFactory.start(); } private void commit() { transaction.commit(); } private void rollback() { transaction.rollback(); }

432

Chapter 13 Database and Concurrency Issues

private void saveCustomer(Customer customer) { Customers customers = snapshotTaker.getCustomersSnapshot(transaction); customers.saveCustomer(customer); } public void setTransactionFactory(SystemTransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; } public void setSnapshotTaker(CustomersSnapshotTaker snapshotTaker) { this.snapshotTaker = snapshotTaker; } }

Before using it, one needs to set a SystemTransactionFactory and a CustomersSnapshotTaker to it. Whenever you need to set some global helper objects into another global object, you may consider making them Hivemind services to make use of auto-wiring for this purpose. So, create META-INF/hivemodule.xml:
By default, Hivemind will use the module id (com.ttdev.bank) as the package for it. Actually, you can skip specifying the interface at all. In that case, it will use the service id as the interface name. <module id="com.ttdev.bank" version="1.0.0"> <service-point id="EditCustomerService" interface="EditCustomerService"> <invoke-factory> Use the default implementation. Use <construct class="DefaultEditCustomerService"/> <invoke-factory> to construct the instance </invoke-factory> so that auto-wiring will be performed. This </service-point> will call its two setters: <service-point id="SystemTransactionFactory"> <create-instance class="DBTransactionFactory"/> </service-point> Use the database implementation <service-point id="CustomersSnapshotTaker"> <create-instance class="CustomersInDBSnapshotTaker"/> </service-point> Use the database implementation </module>

public class DefaultEditCustomerService implements EditCustomerService { private SystemTransaction transaction; private SystemTransactionFactory transactionFactory; private CustomersSnapshotTaker snapshotTaker; ... public void setTransactionFactory(SystemTransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; } public void setSnapshotTaker(CustomersSnapshotTaker snapshotTaker) { this.snapshotTaker = snapshotTaker; } }

Now, EditCustomer.java can just use the EditCustomerService to perform the business transaction:
public abstract class EditCustomer ... { @InjectObject("service:com.ttdev.bank.EditCustomerService") public abstract EditCustomerService getEditService(); private SystemTransaction transaction; private SystemTransactionFactory transactionFactory = new DBTransactionFactory(); private CustomersSnapshotTaker snapshotTaker = new CustomersInDBSnapshotTaker(); public abstract String getCustomerId(); public abstract Customer getOldCustomer(); public abstract Customer getCurrentCustomer(); public abstract void setCustomerId(String customerId); public abstract void setCurrentCustomer(Customer customer); public abstract void setOldCustomer(Customer customer); public void pageBeginRender(PageEvent event) { if (!event.getRequestCycle().isRewinding()) { Customer customer = getEditService().getCustomerForEdit(getCustomerId()); setCurrentCustomer(customer); setOldCustomer(customer); } }

Database and Concurrency Issues

433

@InjectPage("GetCustomerId") public abstract GetCustomerId getQueryIdPage(); @InjectPage("EditCustomerSuccess") public abstract IPage getSuccessPage(); public IPage onOk() { try { String customerId = getOldCustomer().getId(); Customer currentCustomer = getCurrentCustomer(); currentCustomer.setId(customerId); getEditService().saveCustomer(currentCustomer, getOldCustomer()); cycle.activate("EditCustomerSuccess"); } catch (RuntimeException e) { getQueryIdPage().getDelegate().setFormComponent(null); getQueryIdPage().getDelegate().record(e.getMessage(), null); return getQueryIdPage(); } } private Customer loadCustomer(String customerId) { ... } private void commit() { ... } private void rollback() { ... } private void startTransaction() { ... } private void saveCustomer(Customer customer) { ... } }

Now it is a lot simpler. It no longer contains persistence logic and business transaction logic. Instead, it is only concerned with showing the customer to the user, taking the updated customer from the user, showing error messages and etc. This is exactly what a UI class should do. Now, for each Java class in the application, you can conceptually put it into one of four layers: Layer Domain Business transaction Persistence Customer, Customers EditCustomerService, EditCustomerServiceFactory, SystemTransaction, SystemTransactionFactory, CustomersSnapshotTaker CustomersInDB, DBTransaction, CustomersInDBSnapshotTaker Classes

UI EditCustomer, GetCustomerId The classes on the domain layer represents the logic in the business domain (banking). They should know nothing about the classes in the other layers. For example, the Customer class in the domain layer shouldn't know about how it is represented to the user, how is stored into a persistent storage, how transactions are used for concurrency control. The business transaction layer, also called the service layer (that's why the class is called EditCustomerService) or application layer, models business transactions. It makes use of system transactions to ensure correct concurrent accesses. The business transaction should only know about the domain layer (because it needs to manipulate Customer objects). The persistence layer stores objects into a persistent storage (e.g., database, file or EJB) and retrieves them. It also provides system transaction support for access to the persistence storage (e.g., rollback, transaction isolation). It should only know about the domain layer (because it needs to load or save the Customer objects) and the business transaction layer (because it needs to provide implementations for the snapshot takers). The UI layer should only know about the domain layer (because it needs to show the Customer objects to the user or accept updated Customer objects from the user) and the business transaction layer (because it gets the Customer objects from the business transaction layer or provides the Customers object to it for processing). In summary:

434

Chapter 13 Database and Concurrency Issues

Layer Domain Nothing

Depends on

Reuse scenarios Different business transaction (e.g., showing all customers in a grid for editing instead of editing just one). Different persistence (e.g., flat file, XML file or EJB). Different UI (e.g., Swing, Struts, JSP, JSF). Different persistence. Different UI. Different UI. Different persistence.

Business transaction Persistence UI

Domain

Domain, business transaction Domain, business transaction

Summary
To use a database, most usually you will use a transaction. If something is wrong, you just roll it back so that you won't end up with a half-baked operation. If it is completed successfully, just call commit. To access a database, you need to get a connection. However, it is expensive to get a new one every time. You should use a connection pool such as the one coming with Tomcat. Then you will get a data source from Tomcat as a resource, then ask the data source to give you a connection. As Tomcat now needs to create connections for you, you need to put the JDBC driver into its shared lib folder. When two or more users are accessing your application, it is guaranteed that your pages will be used for one request at the time. So it is safe to access the page instance variables without fear of race conditions. This can't be said for a servlet or the engine or a service. However, when your pages are accessing some shared resource such as a database, you still need to avoid race conditions. To do that, you should set the transaction isolation level of each transaction to serializable. However, this won't give you true serializability. For PostgreSQL, this will block write-write and make the read operations read the values at the start of the transaction without seeing the changes made by other transactions. This is enough to ensure true serializability unless you have transactions that don't change the same thing but change each other's premise. So you need to watch out for this. One solution is to make sure what they have read can't be changed. This is usually done with a "select ... for update". However, this won't prevent others from adding records. Another solution is to force them to change the same thing (e.g., a lock). Usually a business transaction involves several steps of user interaction and takes quite a long time. If you use a single database transaction (system transaction) for it the performance will be severely affected. Therefore, usually you will use a system transaction for each HTTP request. To ensure the whole business transaction acts like a single system transaction (all or nothing and transaction isolation), you will read the database in all requests until the last one in the business transaction, then you can write to the database. Before that you must also check to ensure that the data you have read hasn't been changed in the database. Otherwise, tell the user that something is wrong. To decouple your code to increase its re-usability and testability, you should structure your application into four layers: UI layer, business transaction layer, domain layer and persistence layer. In particular you may use a generic SystemTransaction interface and various snapshot taker interfaces to hide the database from the rest of the application. Whenever you need to set some global helper objects into another global object, consider making them Hivemind services and make use of auto-wiring.

435

Chapter 14
Chapter 14

Using Hibernate

436

Chapter 14 Using Hibernate

What's in this chapter?


In this chapter you'll learn how to use Hibernate to access the database in a Tapestry application.

Setting up Hibernate
In the previous chapter you were using JDBC to access the database. In fact, it is easier to use Hibernate to do that. Hibernate is a library that allows you to access a database easily. For example, let's create an application to allow the user to list the customers:

The user can click "Edit" to edit a customer:

To delete a customer, he can click "Delete". To add a new customer, he can click "Add":

Let's do it. First, create a Tapestry application called CustomerCRUD and set it up as usual (use /CustomerCRUD the context path). What does CRUD mean? C means Create; R means Retrieve; U means Update; D means Delete. Next, go to http://www.hibernate.org to download Hibernate. Suppose that it is hibernate-3.0.zip. Unzip it into say c:\hibernate. Copy c:\hibernate\hibernate-3.0.jar and some selected jar files in c:\hibernate\lib into c:\workspace\CustomerCRUD\context\WEB-INF\lib as shown below:

Using Hibernate

437

However, do not copy the commons-logging and commons-collection jar files as they are already included in Tapestry. Now these jar files are available to your application at runtime. As you need to use the classes in Hibernate in your source code, refresh your project in Eclipse and then add hibernate-3.0.jar to your build path using the "Add Jars" button. Next, locate the file c:\hibernate\etc\hibernate.properties. This file contains some properties for Hibernate. For example, it tells Hibernate what database server that you're using. At runtime, Hibernate will read this file during initialization. Hibernate expects to find it in the root folder of the class path, so copy it to the root of your project source folder in Eclipse (c:\workspace\CustomerCRUD\src):

Then it will be automatically copied to c:\workspace\Bank\context\WEB-INF\classes. That's the root folder of the class path. Next, edit it in Eclipse. Currently it should look like:
## JNDI Datasource #hibernate.connection.datasource jdbc/test #hibernate.connection.username db2 #hibernate.connection.password db2 ## HypersonicSQL hibernate.dialect org.hibernate.dialect.HSQLDialect hibernate.connection.driver_class org.hsqldb.jdbcDriver hibernate.connection.username sa hibernate.connection.password ... ## PostgreSQL #hibernate.dialect org.hibernate.dialect.PostgreSQLDialect #hibernate.connection.driver_class org.postgresql.Driver #hibernate.connection.url jdbc:postgresql:template1 #hibernate.connection.username pg #hibernate.connection.password ...

438

Chapter 14 Using Hibernate

What is JNDI data source? Remember that you looked up your data source from Tomcat:
public class DBTransaction implements SystemTransaction { private Connection conn; public void start() { try { Context context = new InitialContext(); DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/bankDataSource"); conn = ds.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); } catch (Exception e) { throw new RuntimeException(e); } } ... }

This kind of lookup is called "JNDI lookup". JNDI stands for "Java Naming and Directory Interface". So the properties file is saying that Hibernate will not be using JNDI to lookup a data source from Tomcat. Instead, it will be creating JDBC connections by itself. It is also assuming that the database is HypersonicSQL (note that the section for HypersonicSQL is not commented out). As you're indeed using JNDI to look up a data source from Tomcat and are using PostgreSQL, modify the file as: Resource name for the data source object or called the "JNDI ## JNDI Datasource name" hibernate.connection.datasource java:comp/env/jdbc/bankDataSource hibernate.dialect org.hibernate.dialect.PostgreSQLDialect #hibernate.connection.datasource jdbc/test #hibernate.connection.username db2 #hibernate.connection.password db2 ## HypersonicSQL Tell Hibernate that you're using PostgreSQL Comment out this section as you're not using Hypersonic SQL

#hibernate.dialect org.hibernate.dialect.HSQLDialect #hibernate.connection.driver_class org.hsqldb.jdbcDriver #hibernate.connection.username sa #hibernate.connection.password ... ## PostgreSQL Just copy this line #hibernate.dialect org.hibernate.dialect.PostgreSQLDialect #hibernate.connection.driver_class org.postgresql.Driver #hibernate.connection.url jdbc:postgresql:template1 #hibernate.connection.username pg #hibernate.connection.password For this to work you need to define the data source in web.xml. Just copy the related lines from the web.xml file for Bank project:
<web-app ...> <servlet> <servlet-name>CustomerCRUD</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>CustomerCRUD</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> <resource-ref> <res-ref-name>jdbc/bankDataSource</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </web-app>

You must also provide such a data source in the context descriptor (CustomerCRUD.xml). Again, you can copy the related lines from the context descriptor for the Bank project:
<Context

Using Hibernate

439

docBase="c:/workspace/CustomerCRUD/context" path="/CustomerCRUD" reloadable="true"> <Resource name="jdbc/bankDataSource" auth="Container" type="javax.sql.DataSource" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://localhost/bank" username="bankuser" password="123456" maxActive="20" defaultAutoCommit="false" timeBetweenEvictionRunsMillis="60000"/> </Context>

Finally, copy c:\hibernate\etc\log4j.properties into the root of your project source folder in Eclipse, along with hibernate.properties. This file will control the logging of Hibernate. For your purpose, there is no need to modify it.

Adding an id not exposed to the user


Next, copy the Customer class from the Bank project into package com.ttdev.customerCRUD. At the moment it is like this:
public class Customer implements Serializable { private static final long serialVersionUID = 469072691908794223L; private String id; private String name; private String address; ... }

When storing objects into a database (using Hibernate or not), it is a good idea to have an id for each object and that the id is never exposed to users and has no business meaning. As your existing id is exposed to the user and probably has business meaning (e.g., printed on bank statements), it is not suitable for your use. So you added an internal id. This is not strictly required by Hibernate, but it is a recommended practice:
public class Customer implements Serializable { private static final long serialVersionUID = 469072691908794223L; private Long internalId; private String id; private String name; private String address; public Long getInternalId() { return internalId; } public void setInternalId(Long internalId) { this.internalId = internalId; } ... }

Using a Long object as the id is the easiest way in Hibernate (You'll see why later). However, there is still a problem: When you have a property named "id" but it is not the internal id, Hibernate may get confused. So, let's use the rename refactoring in Eclipse to rename "id" to "customerId". Make sure to include the getter and setter in the refactoring:

Now you can rename "internalId" as just "id":


public class Customer implements Serializable { private static final long serialVersionUID = 469072691908794223L;

440

Chapter 14 Using Hibernate

private private private private

Long id; String customerId; String name; String address;

public Long getId() { return id; } public void setId(Long id) { this.id = id; } ... }

Specifying the mapping


Next, create a file Customer.hbm.xml in the same folder of Customer.java. It tells Hibernate how to map a Customer object to a record in the database: Store the Customer objects into <?xml version="1.0"?> the customers table in the <!DOCTYPE hibernate-mapping PUBLIC database "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.ttdev.customerCRUD.Customer" table="customers"> <id name="id"> The "id" property should be saved <generator class="sequence"/> into the primary key of the table. </id> Which column is it? You're not <property name="customerId"/> saying so Hibernate will assume it <property name="name"/> is the same ("id"). <property name="address"/> </class> When you save a new Customer to the </hibernate-mapping> database, Hibernate will generate a value as the id and store it into the Customer object. Here Save these properties to the table. you're telling Hibernate to use a sequence in the As the columns are not specified, database to generate the id. That's why using a for example, the "customerId" long is the easiest way. Not all database property will be saved into the supports sequences though (although "customerId" column. The "name" PostgreSQL does support it). In other cases, you property will be saved to the can use another type of generator. Check the "name" column and etc. Hibernate documentation about this. If you'd like, you could specify the column names explicitly:
<hibernate-mapping> <class name="com.ttdev.customerCRUD.Customer" table="customers"> <id name="id" column="id"> <generator class="sequence"/> </id> <property name="customerId" column="customerId"/> <property name="name" column="name"/> <property name="address" column="address"/> </class> </hibernate-mapping>

But as they're exactly the same, so there is no need to do that.

Accessing objects with Hibernate


Next, you can start to use Hibernate to list Customer objects. To do that, first, you need to understand two concepts in Hibernate: Therefore, you need to create a session factory just once for the whole application and create a session for as needed for each request. For the session factory, you can use a Hivemind service to wrap around it that can create session objects. As each Hivemind service must implement have an interface, define the interface first:

Using Hibernate

441

JDBC JDBC Connection


Create Equivalent

Hibernate Session
Create Equivalent

Data source
import org.hibernate.*; public interface SessionCreator { Session createSession(); }

Session factory

Then create the Hivemind service that wraps around a session factory and implements that interface: Add the mapping information for the Customer class to the configuration. It will determine the full package name of the class (com.ttdev.customerCRUD.Customer) and then load the corresponding mapping file (Customer.hbm.xml in the / com/ttdev/customerCRUD folder of the class path). Now Hibernate knows how to map Customer objects to the database.
public class DefaultSessionCreator implements SessionCreator { private SessionFactory sessionFactory; public DefaultSessionCreator() { Configuration cfg = new Configuration(); cfg.addClass(Customer.class); sessionFactory = cfg.buildSessionFactory(); } public Session createSession() { return sessionFactory.openSession(); } }

Create a configuration object (for Hibernate). It will configure itself by loading the hibernate.properties file (from the root of the class path) and reading the options.

Use the session factory to create a session and return it Your whole application will use a single instance of this Hivemind service to create sessions. As the createSession() will be accessed by multiple threads, do you need to declare the method synchronized? The answer is no. The session factory in Hibernate can be used by multiple threads concurrently (i.e., it is thread safe). Next, in JDBC you can set the transaction isolation level after creating a connection. In Hibernate how to set the transaction isolation level after creating a session? You can do it in the hibernate.properties:
##################### ### JDBC Settings ### ##################### ## specify a JDBC isolation level hibernate.connection.isolation 8 #hibernate.connection.isolation 4 ...

Build a session factory and store it for later use for the whole application.

Here, 8 means TRANSACTION_SERIALIZABLE. The default is 4, which means something less powerful than TRANSACTION_SERIALIZABLE. To register the Hivemind service, create META-INF/hivemodule.xml in the root of the src folder:
<?xml version="1.0"?> <module id="com.ttdev.customerCRUD" version="1.0.0"> <service-point id="SessionCreator"> <create-instance class="DefaultSessionCreator"/> </service-point>

442

Chapter 14 Using Hibernate

</module>

Next, create a Home page. Home.html is:


<html> <table border="1"> <tr jwcid="customerList"> <td><span jwcid="customerId">001</span></td> <td><span jwcid="name">Bill Gates</span></td> <td><span jwcid="address">xxx</span></td> </tr> </table> </html>

Home.page is:
<page-specification> <component id="customerList" type="For"> <binding name="source" value="customerList"/> <binding name="value" value="currentCustomer"/> <binding name="element" value="literal:tr"/> </component> <component id="customerId" type="Insert"> <binding name="value" value="currentCustomer.customerId"/> </component> <component id="name" type="Insert"> <binding name="value" value="currentCustomer.name"/> </component> <component id="address" type="Insert"> <binding name="value" value="currentCustomer.address"/> </component> </page-specification>

Modify CustomerCRUD.application so that the pages can find their Java classes:
<application> <meta key="org.apache.tapestry.page-class-packages" value="com.ttdev.customerCRUD"/> </application>

Home.java is:
Access the SessionCreator service
public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); Create a session @InjectObject("service:com.ttdev.customerCRUD.SessionCreator") public abstract SessionCreator getSessionCreator();

Start a new transaction. This is public void pageBeginRender(PageEvent event) { not needed in the JDBC version Session session = getSessionCreator().createSession(); because a transaction is started try { automatically in JDBC when a Transaction tx = session.beginTransaction(); SQL statement is executed. try { Query query = session.createQuery("select c from Customer as c"); customerList = query.list(); Commit the tx.commit(); transaction } catch (RuntimeException e) { It looks like an SQL query but it is not tx.rollback(); really an SQL query. It will select all the Create a Hibernate query objects from your Customer class, not throw e; } the customers table (note that there is } finally { no "s" at the end). Load all the Customer session.close(); objects in a Java List } } If anything is wrong, rollback public List getCustomerList() { the transaction and rethrow the return customerList; exception. } } Close the session. This is very important, otherwise you will run out of database connections. A Hibernate session corresponds to a JDBC connection and thus the number of active sessions are limited.

Using Hibernate

443

Updating the database schema


Before you can run the application, you must change the type of the "id" field of the customers table from varchar to int4. You also need to add the "customerId" field to the customers table. Not only that, you also need to create a sequence to generate the id's. That is, you need to update the structure of the database (i.e., the "schema" of the database). This is a lot of work. The good news is, Hibernate can do all these for you. That is, it can update the schema automatically. To do that, modify hibernate.properties:
## auto schema export hibernate.hbm2ddl.auto update #hibernate.hbm2ddl.auto create-drop #hibernate.hbm2ddl.auto create #hibernate.hbm2ddl.auto update

This way, when Hibernate reads the properties file and initializes itself, it will create every table or sequence it needs. If there is already one, it will check if it is correct, if not, it will update it. However, while it works in theory, in practice sometimes it may fail to see the need to say add a column or change some constraints. To be safe than sorry, delete the tables in PgAdmin first. Then Hibernate will create them automatically. Now, run the application. You should see an empty page:

This is because there is no data in the newly created Customers table.

Hard coding some customers programmatically


To see some customers, you can hard code some programmatically. To do that, create a HardCodeCustomers page. HardCodeCustomers.html can be very simple:
<html> OK! </html>

HardCodeCustomers.page is:
<page-specification/>

HardCodeCustomers.java is:

444

Chapter 14 Using Hibernate

public abstract class HardCodeCustomers extends BasePage implements PageBeginRenderListener { @InjectObject("service:com.ttdev.customerCRUD.SessionCreator") public abstract SessionCreator getSessionCreator(); public void pageBeginRender(PageEvent event) { Session session = getSessionCreator().createSession(); try { Transaction tx = session.beginTransaction(); Load each Customer object one by try { one Query query = session.createQuery("select c from Customer as c"); for (Iterator iter = query.iterate(); iter.hasNext(); ) { session.delete(iter.next()); } Delete it from the database session.save(new Customer("001", "Bill Gates", "xxx")); session.save(new Customer("002", "Linus Torvalds", "yyy")); tx.commit(); } catch (RuntimeException e) { Insert two Customer objects tx.rollback(); throw e; into the database } } finally { session.close(); } } }

Run it using http://localhost:8080/CustomerCRUD/app?service=page&page=HardCodeCustomers. You should see:

Then go to the Home page and you'll see:

Do NOT access objects loaded after its session is closed


Although everything seems to be work, something very dangerous has been going on here! Once a session is closed, you must not use the objects loaded from the session. For example, in Home.java:

Using Hibernate

445

public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); @InjectObject("service:com.ttdev.customerCRUD.SessionCreator") public abstract SessionCreator getSessionCreator(); public void pageBeginRender(PageEvent event) { Session session = getSessionCreator().createSession(); try { Transaction tx = session.beginTransaction(); try { Query query = session.createQuery("select c from Customer as c"); customerList = query.list(); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; } } finally { session.close(); } After this point of time, you must } not use the Customers object public List getCustomerList() { stored in customerList. return customerList; } }

This is because when Hibernate loads say a Customer object, it may just give you an object of a subclass of Customer which is uninitialized. When you call say getName() on that Customer object, it will read the database to load its name. This works only if the session is still open. If the session is closed, it can't read the database and will throw a LazyInitializationException. To solve this problem, the idea is to delay closing the session as long as possible. For example, do it only after the HTTP response has been generated. It would be great if there is an object that contains the session for the current thread. When a page object needs a session, it can ask that object. Finally, when a response has been generated and is about to be sent to the browser, that object will close the session automatically:
4: Hey, a response has been generated. 2: Create a session if not yet created

1: Give me the session

Some object
5: Close it

Page
3: Here you are

Session As this object is responsible for creating and closing the session, it has ownership to that session. So let's call it a SessionOwner. As each thread should have its own session, there should be a unique SessionOwner for each thread. How to do that? Hivemind not only allows you to have a service for the whole application, it also allows you to have a service for each thread. To use this feature, create the SessionOwner interface first:
public interface SessionOwner { Session getSession(); }

Note how a SessionOwner is different from a SessionCreator. The former allows you to access a session maintained (to be closed) by it, while the latter creates a session and returns it to you for you to maintain (i.e., to close). Next, create a DefaultSessionOwner class to implement this interface:

446

Chapter 14 Using Hibernate

It needs to access the SessionCreator to create the session. You can just use auto-wiring. So, give it a setter.
public class DefaultSessionOwner implements SessionOwner, Discardable { private SessionCreator creator; private Session session; public Session getSession() { if (session == null) { session = creator.createSession(); } return session; } public void threadDidDiscardService() { if (session != null) { Set it to null so that if getSession() is session.close(); called again to handle another session = null; request in the future, it will create a } } new session. public void setSessionCreator(SessionCreator creator) { this.creator = creator; } }

When Hivemind is told to clean up all the Hivemind services, it will check each service to see if it implements this interface. If so, it will call:

How to indicate that each thread should have its own SessionOwner service? Do it in hivemodule.xml:
<module id="com.ttdev.customerCRUD" version="1.0.0"> <service-point id="SessionCreator"> <create-instance class="DefaultSessionCreator"/> </service-point> <service-point id="SessionOwner"> <invoke-factory model="threaded"> <construct class="DefaultSessionOwner"/> </invoke-factory> </service-point> </module>

"threaded" means each thread will have its own service object. If you don't specify the model, by default it is "singleton" which is what you have been using and means a single object for the whole application.

Must use <invoke-factory> instead of <create-instance> in order to use auto-wiring (and threaded model). Finally, who is going to tell Hivemind to clean up its services? The Tapestry application servlet will do it automatically. When it receives an HTTP request (see the diagram below), it will forward it to the engine for handling. After receiving a response, before it forwards it back to the browser, it will tell Hivemind to clean up all its threaded services. More accurately, it will call the Hivemind registry which knows about all existing Hivemind services to do the clean up. The Hivemind registry will ask each threaded model service for the current thread to clean up:

Using Hibernate

447

Tapestry
1: Handle the request 7: Done. Here is the response

Application servlet

2: Handle the request

Engine
3: Done. Here is the response

4: Clean up all threaded services for the current thread

Hivemind Hivemind registry Threaded service

5: Clean it up if it is for the current thread 6: Clean it up if it is for the current thread

Threaded service

Now, Home.java no longer needs to close the session by itself:


public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); @InjectObject("service:com.ttdev.customerCRUD.SessionCreator") public abstract SessionCreator getSessionCreator(); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Session session = getSessionCreator().createSession(); Session session = getSessionOwner().getSession(); try { Transaction tx = session.beginTransaction(); try { Query query = session.createQuery("select c from Customer as c"); customerList = query.list(); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; } } finally { session.close(); } } public List getCustomerList() { return customerList; } }

In fact, in most cases you will only use one transaction when handling an HTTP request, so you can start and commit (or rollback) the transaction in DefaultSessionOwner:

448

Chapter 14 Using Hibernate

public class DefaultSessionOwner implements SessionOwner, Discardable { private SessionCreator creator; private Session session; private Transaction tx; private boolean isToRollback; public Session getSession() { if (session == null) { Start a transaction automatically if there is none existing. session = creator.createSession(); if (tx == null) { Set the isToRollback flag to false, meaning that the tx = session.beginTransaction(); transaction should be committed. If something is wrong, isToRollback = false; you can set it to true so that the transaction will be rolled } back. } return session; } public void threadDidDiscardService() { if (session != null) { try { End a transaction automatically endTransaction(); } finally { session.close(); Always close the session even if session = null; it failed to commit } } } public void setToRollback() { isToRollback = true; } public void endTransaction() { if (tx != null) { try { if (isToRollback) { Commit or rollback depending on tx.rollback(); the isToRollback flag } else { tx.commit(); } } catch (RuntimeException e) { tx.rollback(); If error (most likely a commit throw e; error), rollback and rethrow. } finally { tx = null; Always forget about this } transaction } } ... }

In fact, if you need to have say two separate transactions in handling one HTTP request, you can just call endTransaction() to finish the first one and then call getSession() to start the second one. Therefore, expose a these methods through SessionOwner:
public interface SessionOwner { Session getSession(); void setToRollback(); void endTransaction(); }

Anyway, now Home.java can be further simplified:


public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Session session = getSessionOwner().getSession(); Transaction tx = session.beginTransaction(); try { Query query = session.createQuery("select c from Customer as c"); customerList = query.list(); tx.commit(); } catch (RuntimeException e) {

Using Hibernate

449

tx.rollback(); throw e; } } ... }

Now run the application and it should continue to work. Now Home.java has been simplified. What about HardCodeCustomers.java? Even though it won't access the objects after the session is closed, it can still be simplified by using the SessionOwner:
public abstract class HardCodeCustomers extends BasePage implements PageBeginRenderListener { @InjectObject("service:com.ttdev.customerCRUD.SessionCreator") public abstract SessionCreator getSessionCreator(); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Session session = getSessionCreator().createSession(); Session session = getSessionOwner().getSession(); try { Transaction tx = session.beginTransaction(); try { Query query = session.createQuery("select c from Customer as c"); for (Iterator iter = query.iterate(); iter.hasNext(); ) { session.delete(iter.next()); } session.save(new Customer("001", "Bill Gates", "xxx")); session.save(new Customer("002", "Linus Torvalds", "yyy")); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; } } finally { session.close(); } } }

Run the application again and it should continue to work.

Editing a Customer object


To allow the user to edit a Customer object, modify Home.html:
<html> <table border="1"> <tr jwcid="customerList"> <td><span jwcid="customerId">001</span></td> <td><span jwcid="name">Bill Gates</span></td> <td><span jwcid="address">xxx</span></td> <td><a jwcid="edit">Edit</a></td> </tr> </table> </html>

Home.page is:
<page-specification> <component id="customerList" type="For"> <binding name="source" value="customerList"/> <binding name="value" value="currentCustomer"/> <binding name="element" value="literal:tr"/> </component> <component id="customerId" type="Insert"> <binding name="value" value="currentCustomer.customerId"/> </component> <component id="name" type="Insert"> <binding name="value" value="currentCustomer.name"/> </component> <component id="address" type="Insert"> <binding name="value" value="currentCustomer.address"/> </component> <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="currentCustomer.id"/> </component>

450

Chapter 14 Using Hibernate

</page-specification>

Home.java is:
public abstract class Home extends BasePage implements PageBeginRenderListener { ... @InjectPage("CustomerPage") public abstract CustomerPage getCustomerPage(); public IPage onEdit(Long id) { CustomerPage customerPage = getCustomerPage(); customerPage.setInternalId(id); return customerPage; } }

You still need to create CustomerPage. CustomerPage.html is:


<html> <form jwcid="edit"> <input jwcid="customerId" type="text"/><br> <input jwcid="name" type="text"/><br> <input jwcid="address" type="text"/><br> <input type="submit" value="OK"/> </form> </html>

CustomerPage.page is:
<page-specification> <component id="edit" type="Form"> <binding name="listener" value="listener:onOK"/> </component> <component id="customerId" type="TextField"> <binding name="value" value="customer.customerId"/> </component> <component id="name" type="TextField"> <binding name="value" value="customer.name"/> </component> <component id="address" type="TextField"> <binding name="value" value="customer.address"/> </component> </page-specification>

CustomerPage.java is:
Must persist it so that it is still available on rewind
public abstract class CustomerPage extends BasePage implements PageBeginRenderListener { @Persist("client") public abstract Long getInternalId(); The caller must set the internal id public abstract void setInternalId(Long internalId); public abstract Customer getCustomer(); public abstract void setCustomer(Customer customer);

to indicate which Customer to edit

@InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); Load the Customer object before public void pageBeginRender(PageEvent event) { Session session = getSessionOwner().getSession(); Customer customer = (Customer) session.load(Customer.class, getInternalId()); setCustomer(customer); } Load the Customer object public String onOK() { according to its internal getSessionOwner().endTransaction(); (Hibernate) id return "Home"; }

rendering is started

Commit the transaction. During the rewind process, the Customer object is modified by the TextField components. Now, force the changes to save to the database. Actually this step is optional because the transaction will be committed automatically when the request handling has finished. But it may be a good idea to do it now so that if there are any errors such as violation of integrity constraints, they will occur now instead of later. Now run the application and it should be work:
}

Using Hibernate

451

Adding a Customer object


To allow the user to add a Customer object, modify Home.html:
<html> <table border="1"> <tr jwcid="customerList"> <td><span jwcid="customerId">001</span></td> <td><span jwcid="name">Bill Gates</span></td> <td><span jwcid="address">xxx</span></td> <td><a jwcid="edit">Edit</a></td> </tr> </table> <a jwcid="add">Add</a> </html>

Home.page is:
<page-specification> <component id="customerList" type="For"> <binding name="source" value="customerList"/> <binding name="value" value="currentCustomer"/> <binding name="element" value="literal:tr"/> </component> <component id="customerId" type="Insert"> <binding name="value" value="currentCustomer.customerId"/> </component> <component id="name" type="Insert"> <binding name="value" value="currentCustomer.name"/> </component> <component id="address" type="Insert"> <binding name="value" value="currentCustomer.address"/> </component> <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="currentCustomer.id"/> </component> <component id="add" type="PageLink"> <binding name="page" value="literal:CustomerPage"/> </component> </page-specification>

It will call the CustomerPage without setting the internal id. In that case, it should add a customer instead of editing a

452

Chapter 14 Using Hibernate

customer. Therefore instead of loading a Customer object from the database, it should create a default Customer object:
public abstract class CustomerPage extends BasePage implements PageBeginRenderListener { @Persist("client") public abstract Long getInternalId(); public abstract void setInternalId(Long internalId); public abstract Customer getCustomer(); public abstract void setCustomer(Customer customer); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Customer customer; If no internal id specified, create if (getInternalId() == null) { a default Customer object customer = new Customer(); } else { Session session = getSessionOwner().getSession(); customer = (Customer) session.load(Customer.class, getInternalId()); } setCustomer(customer); } public String onOK() { if (getInternalId()==null) { Session session = getSessionOwner().getSession(); session.save(getCustomer()); } getSessionOwner().endTransaction(); return "Home"; Save it to the database if adding } }

(internal id is null)

Now run it and it should work:

Using Hibernate

453

Deleting a Customer object


To allow the user to add a Customer object, modify Home.html:
<html> <table border="1"> <tr jwcid="customerList"> <td><span jwcid="customerId">001</span></td> <td><span jwcid="name">Bill Gates</span></td> <td><span jwcid="address">xxx</span></td> <td><a jwcid="edit">Edit</a></td> <td><a jwcid="delete">Delete</a></td> </tr> </table> <a jwcid="add">Add</a> </html>

Home.page is:
<page-specification> <component id="customerList" type="For"> <binding name="source" value="customerList"/> <binding name="value" value="currentCustomer"/> <binding name="element" value="literal:tr"/> </component> <component id="customerId" type="Insert"> <binding name="value" value="currentCustomer.customerId"/> </component> <component id="name" type="Insert"> <binding name="value" value="currentCustomer.name"/> </component> <component id="address" type="Insert"> <binding name="value" value="currentCustomer.address"/> </component> <component id="edit" type="DirectLink"> <binding name="listener" value="listener:onEdit"/> <binding name="parameters" value="currentCustomer.id"/> </component> <component id="add" type="PageLink"> <binding name="page" value="literal:CustomerPage"/> </component> <component id="delete" type="DirectLink"> <binding name="listener" value="listener:onDelete"/> <binding name="parameters" value="currentCustomer.id"/> </component> </page-specification>

Home.java is:
public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Session session = getSessionOwner().getSession(); Query query = session.createQuery("select c from Customer as c"); customerList = query.list(); } @InjectPage("CustomerPage") public abstract CustomerPage getCustomerPage(); public IPage onEdit(Long id) { CustomerPage customerPage = getCustomerPage(); customerPage.setInternalId(id); return customerPage; } public void onDelete(Long id) { Session session = getSessionOwner().getSession(); session.delete(session.load(Customer.class, id)); getSessionOwner().endTransaction(); } public List getCustomerList() { return customerList; } }

454

Chapter 14 Using Hibernate

Again, to delete an object, you must load it first and then delete it. The call to endTransaction() is again optional. Now run it and it should work:

Handling concurrency issues


You haven't considered the concurrency issues yet. For example, if one user is editing a customer but another has changed it first, then the first user should receive an error when trying to save the changes. To solve the problem, you may store a copy of the Customer object on the HTML page (as a client persistent property). When the form is submitted, you can compare it against the copy in the database. This is the way you did it in the previous chapter. What is interesting here is how do you store the Customer object into the HTML page. First, a Hibernate object is not serializable. Second, remember that you must not access an object once the session is closed? But in this case the object must out-live the session. To solve these problems, you must somehow convert the Hibernate object to a plain old Java object (POJO). To do that, modify CustomerPage.java:

Using Hibernate

455

public abstract class CustomerPage extends BasePage implements PageBeginRenderListener { @Persist("client") public abstract Long getInternalId(); public abstract void setInternalId(Long internalId); public abstract Customer getCustomer(); public abstract void setCustomer(Customer customer); @Persist("client") public abstract Customer getOldCustomer(); public abstract void setOldCustomer(Customer customer); @InjectObject("service:com.ttdev.customerCRUD.SessionOwner") public abstract SessionOwner getSessionOwner(); public void pageBeginRender(PageEvent event) { Customer customer; Critical: Make a copy to create a if (getInternalId() == null) { POJO so that it is no longer customer = new Customer(); linked to Hibernate at all } else { Session session = getSessionOwner().getSession(); customer = (Customer) session.load(Customer.class, getInternalId()); if (!getRequestCycle().isRewinding()) { setOldCustomer(customer.makeCopy()); } else { if (!customer.equals(getOldCustomer())) { Compare to see if the copy in the getSessionOwner().setToRollback(); database has changed. If so, roll getSessionOwner().endTransaction(); back and display the listing (with throw new PageRedirectException("Home"); the latest data). } } } setCustomer(customer); } public String onOK() { Session session = getSessionOwner().getSession(); if (getInternalId()==null) { session.save(getCustomer()); } getSessionOwner().endTransaction(); return "Home"; } }

Store the existing Customer object into the HTML page

You need to define the makeCopy() method in Customer.java:


public class Customer implements Serializable { private static final long serialVersionUID = 469072691908794223L; private Long id; private String customerId; private String name; private String address; public Customer() { } public Customer(String id, String name, String address) { this.customerId = id; this.name = name; this.address = address; } ... public Customer makeCopy() { return new Customer(customerId, name, address); } }

Now run it and try to change the address of a customer to "yyy":

456

Chapter 14 Using Hibernate

Open another browser to change the address of that customer to "zzz". Then click "OK". You'll be redirected to the listing:

Note that the address has been changed to "zzz".

Separating UI code and database code


At the moment your UI code is dealing with Hibernate (database) code directly. This is no good. You should separate them. The Home page may use a HomeService:
public interface HomeService { List listAll(); void delete(Long id); }

The default HomeService is:


public class DefaultHomeService implements HomeService { private Customers customers; private ObjectPool pool; public void setCustomers(Customers customers) { this.customers = customers; } public void setPool(ObjectPool pool) { this.pool = pool; } public List listAll() { return customers.listAll(); } public void delete(Long id) { customers.delete(id); pool.end(); } }

The ObjectPool represents a transactional pool of objects. You can think of it as a SessionOwner except that conceptually it has nothing to do with Hibernate. It is like:
public interface ObjectPool { void setToRollback(); void end(); }

The Hibernate implementation is:

Using Hibernate

457

public class HibernateObjectPool implements ObjectPool { private SessionOwner sessionOwner; public void end() { sessionOwner.endTransaction(); } public void setSessionOwner(SessionOwner sessionOwner) { this.sessionOwner = sessionOwner; } public void setToRollback() { sessionOwner.setToRollback(); } }

The Customers class is:


public interface Customers { List listAll(); Customer get(Long id); void delete(Long id); void save(Customer customer); }

The Hibernate implementation is:


public class HibernateCustomers implements Customers { private SessionOwner sessionOwner; public void setSessionOwner(SessionOwner sessionOwner) { this.sessionOwner = sessionOwner; } private Session getSession() { return sessionOwner.getSession(); } public List listAll() { Session session = getSession(); Query query = session.createQuery("select c from Customer as c"); return query.list(); } public void delete(Long id) { Session session = getSession(); session.delete(session.load(Customer.class, id)); } public Customer get(Long id) { Session session = getSession(); return (Customer) session.load(Customer.class, id); } public void save(Customer customer) { Session session = getSession(); session.save(customer); } }

Home.java can be simplified as:


public abstract class Home extends BasePage implements PageBeginRenderListener { private List customerList; public abstract Customer getCurrentCustomer(); @InjectObject("service:com.ttdev.customerCRUD.HomeService") public abstract HomeService getHomeService(); public void pageBeginRender(PageEvent event) { customerList = getHomeService().listAll(); } @InjectPage("CustomerPage") public abstract CustomerPage getCustomerPage(); public IPage onEdit(Long id) { CustomerPage customerPage = getCustomerPage(); customerPage.setInternalId(id); return customerPage; } public void onDelete(Long id) { getHomeService().delete(id); } public List getCustomerList() {

458

Chapter 14 Using Hibernate

return customerList; } }

The services and their implementations need to be registered and wired together in hivemodule.xml:
<module id="com.ttdev.customerCRUD" version="1.0.0"> <service-point id="SessionCreator"> <create-instance class="DefaultSessionCreator"/> </service-point> <service-point id="SessionOwner"> <invoke-factory model="threaded"> <construct class="DefaultSessionOwner"/> </invoke-factory> </service-point> <service-point id="ObjectPool"> <invoke-factory> <construct class="HibernateObjectPool"/> </invoke-factory> </service-point> <service-point id="Customers"> <invoke-factory> <construct class="HibernateCustomers"/> </invoke-factory> </service-point> <service-point id="HomeService"> <invoke-factory> <construct class="DefaultHomeService"/> </invoke-factory> </service-point> </module>

Similarly, CustomerPage can use a CustomerPageService:


public interface CustomerPageService { Customer getDefault(); Customer load(Long id); Customer loadPersistent(Long id); void assertUnchanged(Long id, Customer oldCustomer); void save(Customer customer, boolean isNew); }

The default implementation is:


public class DefaultCustomerPageService implements CustomerPageService { private ObjectPool pool; private Customers customers; public void setPool(ObjectPool pool) { this.pool = pool; } public void setCustomers(Customers customers) { this.customers = customers; } public Customer getDefault() { return new Customer(); } public Customer load(Long id) { return customers.get(id); } public Customer loadPersistent(Long id) { return load(id).makeCopy(); } public void assertUnchanged(Long id, Customer oldCustomer) { if (!load(id).equals(oldCustomer)) { pool.setToRollback(); pool.end(); throw new DataChangedException(); } } public void save(Customer customer, boolean isNew) { if (isNew) { customers.save(customer); } pool.end(); } }

Using Hibernate

459

public class DataChangedException extends RuntimeException { private static final long serialVersionUID = -1314635187658914946L; }

CustomerPage can be simplified as:


public abstract class CustomerPage extends BasePage implements PageBeginRenderListener { @Persist("client") public abstract Long getInternalId(); public abstract void setInternalId(Long internalId); public abstract Customer getCustomer(); public abstract void setCustomer(Customer customer); @Persist("client") public abstract Customer getOldCustomer(); public abstract void setOldCustomer(Customer customer); @InjectObject("service:com.ttdev.customerCRUD.CustomerPageService") public abstract CustomerPageService getCustomerPageService(); public void pageBeginRender(PageEvent event) { Customer customer; if (getInternalId() == null) { customer = getCustomerPageService().getDefault(); } else { customer = getCustomerPageService().load(getInternalId()); if (!getRequestCycle().isRewinding()) { setOldCustomer(getCustomerPageService().loadPersistent(getInternalId())); } else { try { getCustomerPageService().assertUnchanged(getInternalId(), getOldCustomer()); } catch (DataChangedException e) { throw new PageRedirectException("Home"); } } } setCustomer(customer); } public String onOK() { getCustomerPageService().save(getCustomer(), getInternalId() == null); return "Home"; } }

hivemodule.xml is:
<module id="com.ttdev.customerCRUD" version="1.0.0"> <service-point id="SessionCreator"> <create-instance class="DefaultSessionCreator"/> </service-point> <service-point id="SessionOwner"> <invoke-factory model="threaded"> <construct class="DefaultSessionOwner"/> </invoke-factory> </service-point> <service-point id="ObjectPool"> <invoke-factory> <construct class="HibernateObjectPool"/> </invoke-factory> </service-point> <service-point id="Customers"> <invoke-factory> <construct class="HibernateCustomers"/> </invoke-factory> </service-point> <service-point id="HomeService"> <invoke-factory> <construct class="DefaultHomeService"/> </invoke-factory> </service-point> <service-point id="CustomerPageService"> <invoke-factory> <construct class="DefaultCustomerPageService"/> </invoke-factory> </service-point> </module>

Now run the application and it should continue to work.

460

Chapter 14 Using Hibernate

Summary
To use Hibernate, you no longer use a JDBC data source from Tomcat directly. Instead, you create a Hibernate session factory. Conceptually they're equivalent. Then you ask the session factory to create sessions for you. A Hibernation session can be considered a wrapper around a JDBC connection. You can configure the session factory to create JDBC connections by itself or use JNDI to lookup a JDBC data source from Tomcat. In either case you need to tell it the dialect to use. There is a dialect for each supported database server. When using Hibernate, it is critical that you not access objects loaded from a session after the session is closed. Usually you should use a threaded Hivemind service to close the session for the current thread after the request handling is finished so you can use the Hibernate objects loaded freely during the request handling (both rewind and render). The Tapestry application servlet will ask all threaded Hivemind services to clean up at the end of the request handling process. If you need to store a Hibernate object into an HTML page or into a session, create and store a POJO version of it instead or just store its id.

461

Chapter 15
Chapter 15

Integrating with Struts

462

Chapter 15 Integrating with Struts

What's in this chapter?


In this chapter you'll learn how to integrate Struts with Tapestry in a single application. If you don't use Struts, this chapter may be useless to you so you may skip it.

Integrating Tapestry with Struts


Suppose that you've been working on a Struts application and now you'd like to use Tapestry. Do you have to wait for a new project? No, you can use Tapestry in a Struts project and they can integrate fairly well. Or, if you have a legacy project using Struts, you can migrate it to Tapestry in a page by page manner, while keeping the application working.

Running a sample Struts application


Let's work on a sample application. First, go to http://struts.apache.org/ and download Struts. Suppose that the file is jakarta-struts-1.2.4.zip. Unzip it into a folder such as c:\struts. Unzip c:\struts\webapps\struts-mailreader.war into a folder such as c:\mailreader. This is a sample application that you'll be working on. In Eclipse, create a new Tapestry project named MailReader. Copy all the jar files in c:\struts\lib into your context\WEBINF\lib. Add struts.jar, commons-beanutils.jar and commons-digester.jar in WEB-INF/lib to the build path. Then add servlet-api.jar and jsp-api.jar in c:\tomcat\common\lib to the build path. Import all the files in c:\mailreader\WEB-INF\src into your src folder:

Import all the files in c:\mailreader except a few folders into your context folder as shown below:

Integrating with Struts

463

In the process it will ask you whether to overwrite web.xml or not. Say yes. Then, modify web.xml as:
<web-app> <display-name>Struts Example Application</display-name> <!-- Action Servlet Configuration --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value> /WEB-INF/struts-config.xml, /WEB-INF/struts-config-registration.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet> <servlet-name>MailReader</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>MailReader</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> <!-- The Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> ... </web-app>

This sets up the Tapestry application servlet. Next, create a context descriptor as usual. Start Tomcat and try to access the application using http://localhost:8080/MailReader:

464

Chapter 15 Integrating with Struts

Let's click "Log on to the MailReader...":

The application contains a hard code user account. The user name is "user". The password is "pass". If it is not the case, check the file context\WEB-INF\database.xml which simulates the database for the application. After logging on, you'll see:

Click "Edit your user registration profile":

Integrating with Struts

465

Here you can edit the user's password, full name, from address (email address) and reply-to address. A user can also subscribe to zero or more mail boxes. For example, here the user has subscribed to two mail boxes. One on Hotmail, one on Yahoo. You can delete or edit an existing subscription or add a new one. If you click "Edit" for the Hotmail subscription, you'll see:

If you click "Delete" instead, you'll see:

466

Chapter 15 Integrating with Struts

If you click "Add" instead, you'll see:

These three pages are very similar and most likely they're implemented using the same Struts action and JSP page. If you go back to the main page:

Integrating with Struts

467

You can register to create a new user account:

You can also change the language to say Japanese:

468

Chapter 15 Integrating with Struts

That's about it.

Rewriting the Logon page in Tapestry


Suppose that you'd like to rewrite the Logon page in Tapestry. Let's check how the main page invokes the Logon page. The main page is welcome.jsp in your context folder:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html> <head> <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <h3><bean:message key="index.heading"/></h3> <ul> <li><html:link action="/EditRegistration?action=Create"><bean:message key="index.registration"/></html:link></li> <li><html:link action="/Logon"><bean:message key="index.logon"/></html:link></li> </ul> <h3>Language Options</h3> <ul> <li><html:link action="/Locale?language=en">English</html:link></li> <li><html:link action="/Locale?language=ja" useLocalEncoding="true">Japanese</html:link></li> <li><html:link action="/Locale?language=ru" useLocalEncoding="true">Russian</html:link></li> </ul> <hr /> <p><html:img bundle="alternate" pageKey="struts.logo.path" altKey="struts.logo.alt"/></p> <p><html:link action="/Tour"><bean:message key="index.tour"/></html:link></p> </body> </html>

The code highlighted above is a link invoking the Logon action. The Logon action is defined in WEB-INF/strutsconfig.xml:
<struts-config> ... <action-mappings> <action path="/Logon" forward="/logon.jsp"/> ... </action-mappings> ... </struts-config>

The logon.jsp is:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>

Integrating with Struts

469

<%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html:xhtml/> <html> <head> <title><bean:message key="logon.title"/></title> </head> <html:errors/> <html:form action="/SubmitLogon" focus="username" onsubmit="return validateLogonForm(this);"> <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <html:text property="username" size="16" maxlength="18"/> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password" bundle="alternate"/>: </th> <td align="left"> <html:password property="password" size="16" maxlength="18" redisplay="false"/> </td> </tr> <tr> <td align="right"> <html:submit property="Submit" value="Submit"/> </td> <td align="left"> <html:reset/> </td> </tr> </table> </html:form> <html:javascript formName="LogonForm" dynamicJavascript="true" staticJavascript="false"/> <script language="Javascript1.1" src="staticJavascript.jsp"></script> <jsp:include page="footer.jsp" /> </body> </html>

You'll create a Logon Tapestry page to replace login.jsp. In addition, check the action of the form:
... <html:form action="/SubmitLogon" focus="username" onsubmit="return validateLogonForm(this);"> ...

Because a Tapestry page will not only display itself, but also handle the form submission (if it does contain a form). It means that your Login page needs to perform the job of the SubmitLogon action. Check how is this /SubmitLogon action implemented:
<struts-config> ... <action-mappings> <action path="/SubmitLogon" type="org.apache.struts.webapp.example.LogonAction" name="LogonForm" scope="request" input="logon"> <exception key="expired.password" type="org.apache.struts.webapp.example.ExpiredPasswordException" path="/ExpiredPassword.do"/> </action> ... </action-mappings> ... </struts-config>

It is implemented by the LogonAction class:


public final class LogonAction extends BaseAction { static String USERNAME = "username"; static String PASSWORD = "password"; User getUser(UserDatabase database, String username, String password,

470

Chapter 15 Integrating with Struts

ActionMessages errors) throws ExpiredPasswordException { User user = null; if (database == null) { errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage( "error.database.missing")); } else { user = database.findUser(username); if ((user != null) && !user.getPassword().equals(password)) { user = null; } if (user == null) { errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage( "error.password.mismatch")); } } return user; } void SaveUser(HttpServletRequest request, User user) { HttpSession session = request.getSession(); session.setAttribute(Constants.USER_KEY, user); if (log.isDebugEnabled()) { log.debug("LogonAction: User '" + user.getUsername() + "' logged on in session " + session.getId()); } } public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Local variables UserDatabase database = getUserDatabase(request); String username = (String) PropertyUtils.getSimpleProperty(form, USERNAME); String password = (String) PropertyUtils.getSimpleProperty(form, PASSWORD); ActionMessages errors = new ActionMessages(); // Retrieve user User user = getUser(database, username, password, errors); // Save (or clear) user object SaveUser(request, user); // Report back any errors, and exit if any if (!errors.isEmpty()) { this.saveErrors(request, errors); return (mapping.getInputForward()); } // Otherwise, return "success" return (findSuccess(mapping)); } }

In particular, it is the execute() method that will be invoked. You'll implement the same logic in a listener method in your Logon page.

Invoking a Tapestry page from JSP and invoking a Struts action from Tapestry
Writing such a Logon page to replace logon.jsp and LogonAction.java is not that difficult. What is interesting is how to invoke this Logon page from Struts. That is, how to rewrite the code below in welcome.jsp to invoke the Logon page:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html> <head> <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <h3><bean:message key="index.heading"/></h3> <ul> <li><html:link action="/EditRegistration?action=Create"><bean:message key="index.registration"/></html:link></li> <li><html:link action="/Logon"><bean:message key="index.logon"/></html:link></li> </ul> <h3>Language Options</h3> <ul> <li><html:link action="/Locale?language=en">English</html:link></li> <li><html:link action="/Locale?language=ja" useLocalEncoding="true">Japanese</html:link></li> <li><html:link action="/Locale?language=ru" useLocalEncoding="true">Russian</html:link></li> </ul>

Integrating with Struts

471

<hr /> <p><html:img bundle="alternate" pageKey="struts.logo.path" altKey="struts.logo.alt"/></p> <p><html:link action="/Tour"><bean:message key="index.tour"/></html:link></p> </body> </html>

Similarly, in the SubmitLogon action, after it is done, it will invoke the "success" forward:
public final class LogonAction extends BaseAction { ... public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Local variables UserDatabase database = getUserDatabase(request); String username = (String) PropertyUtils.getSimpleProperty(form, USERNAME); String password = (String) PropertyUtils.getSimpleProperty(form, PASSWORD); ActionMessages errors = new ActionMessages(); // Retrieve user User user = getUser(database, username, password, errors); // Save (or clear) user object SaveUser(request, user); // Report back any errors, and exit if any if (!errors.isEmpty()) { this.saveErrors(request, errors); return (mapping.getInputForward()); } // Otherwise, return "success" return (findSuccess(mapping)); } }

The "success" forward is defined as:


<struts-config> ... <global-forwards> <forward name="welcome" <forward name="logoff" <forward name="logon" <forward name="success" </global-forwards> </struts-config>

path="/Welcome.do"/> path="/Logoff.do"/> path="/Logon.do"/> path="/MainMenu.do"/>

Once you use your Logon page, how to invoke this /MainMenu.do from that page? That is: MailReader application

Struts

Tapestry

welcome.jsp

Call it. But how?

Logon page

MainMenu.do action

Call it. But how?

Now, create the Logon Tapestry page that uses Logon.java. Logon.java should be like:
public class Logon extends BasePage { public void onSubmit() { }

472

Chapter 15 Integrating with Struts

How to invoke it from welcome.jsp? You can do it this way:


<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> It is a "page", no longer an action. This <html> is not a page in Tapestry. It is a page in <head> Struts which is just a URL relative to <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> the context (/MailReader). So, the full </head> link will be /MailReader/app?service... <h3><bean:message key="index.heading"/></h3> <ul> <li><html:link action="/EditRegistration?action=Create"><bean:message key="index.registration"/></html:link></li> <li><html:link action="/Logon" page="/app?service=page&page=Logon"> <bean:message key="index.logon"/></html:link></li> </ul> <h3>Language Options</h3> <ul> <li><html:link action="/Locale?language=en">English</html:link></li> <li><html:link action="/Locale?language=ja" useLocalEncoding="true">Japanese</html:link></li> <li><html:link action="/Locale?language=ru" useLocalEncoding="true">Russian</html:link></li> </ul> <hr /> <p><html:img bundle="alternate" pageKey="struts.logo.path" altKey="struts.logo.alt"/></p> <p><html:link action="/Tour"><bean:message key="index.tour"/></html:link></p> </body> </html>

Next, consider how to invoke MainMenu.do from the listener method. You could do it this way:
public class Logon extends BasePage { public void onSubmit() { throw new RedirectException("MainMenu.do"); } }

This path "MainMenu.do" is relative to the context path (/MailReader). That is, this will forward the request to / MailReader/MainMenu.do. To see if it's working, run the application:

Note that the link does contain the context path. If there was a session and URL rewriting were used, then you would see the session id too. Clicking on it will show an error because you haven't created Logon.html and Logon.page yet.

Implementing rendering part of the Logon page


You'll really implement the Logon page. Create Logon.html first. You should model it after logon.jsp. The changes are

Integrating with Struts

473

highlighted below:

Logon.html
<html> <head> <title><span key="logon.title"/></title> </head> <body jwcid="@Body"> <span jwcid="errors"/> <form jwcid="form"> <table border="0" width="100%"> <tr> <th align="right"> <span key="prompt.username"/>: </th> <td align="left"> <input type="text" jwcid="username" size="16" maxlength="18"/> </td> </tr> <tr> <th align="right"> <span key="prompt.password"/>: </th> <td align="left"> <input type="password" jwcid="password" size="16" maxlength="18"/> </td> </tr> <tr> <td align="right"> <input type="submit" value="Submit"/> </td> <td align="left"> <input type="reset"/> </td> </tr> </table> </form> <span jwcid="@Footer"/> </body> </html>

Logon.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html:xhtml/> <html> <head> <title><bean:message key="logon.title"/></title> </head> <html:errors/> <html:form action="/SubmitLogon" focus="username" onsubmit="return validateLogonForm(this);"> <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <html:text property="username" size="16" maxlength="18"/> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password" bundle="alternate"/>: </th> <td align="left"> <html:password property="password" size="16" maxlength="18" redisplay="false"/> </td> </tr> <tr> <td align="right"> <html:submit property="Submit" value="Submit"/> </td> <td align="left"> <html:reset/> </td> </tr> </table> </html:form> <html:javascript formName="LogonForm" dynamicJavascript="true" staticJavascript="false"/> <script language="Javascript1.1" src="staticJavascript.jsp"></script> <jsp:include page="footer.jsp" /> </body> </html>

To be done using Tapestry validation

Logon.page is:
<page-specification class="org.apache.struts.webapp.example.Logon"> <component id="errors" type="Delegator"> <binding name="delegate" value="beans.delegate.firstError"/> </component> <component id="form" type="Form"> <binding name="listener" value="listener:onSubmit"/> <binding name="delegate" value="beans.delegate"/> <binding name="clientValidationEnabled" value="true"/> </component> <component id="username" type="TextField"> <binding name="value" value="username"/> <binding name="validators" value="validators:required"/> <binding name="displayName" value="message:username"/> </component> <component id="password" type="TextField"> <binding name="hidden" value="true"/> <binding name="value" value="password"/> <binding name="validators" value="validators:required,minLength=3,maxLength=18"/> <binding name="displayName" value="message:password"/> </component>

474

Chapter 15 Integrating with Struts

</page-specification>

Modify Logon.java:
public abstract class Logon extends BasePage { public abstract String getUsername(); public abstract String getPassword(); @Bean public abstract ValidationDelegate getDelegate(); public void onSubmit() { throw new RedirectException("MainMenu.do"); } }

In Struts, localized strings are stored in global properties files. They are defined in struts-config.xml:
<struts-config> ... <message-resources parameter="org.apache.struts.webapp.example.ApplicationResources"/> <message-resources parameter="org.apache.struts.webapp.example.AlternateApplicationResources" key="alternate"/> ... </struts-config>

There are two message resource bundles in this case. The first bundle is the default. The second bundle is called "alternate". The first bundle contains three properties files:

There is one for English (ApplicationResources.properties), one for Japanese (ApplicationResources_ja.properties) and one for Russian (ApplicationResources_ru.properties). The English version is also shown above in the right pane. In Tapestry, each page can have its own properties files (e.g., Logon.properties, Logon_ja.properties and etc.). In addition, there can also be a global properties file named after the application. So, in this case it is MailReader.properties in the same folder as MailReader.application (if any). However, at the moment you'd like to run the application ASAP, so you'll ignore this issue for now. Tapestry will still display the message key when it can't find a localized string. Finally, to make it run, you still need to define a Footer component. Create it. Footer.html is: This is footer Footer.jwc is:
<component-specification/>

Run the application and try to logon. You'll see:

Integrating with Struts

475

As you can see, if Tapestry can't find a localized string for key XXX, it will just display [XXX] as the localized string. Anyway, enter "user" as the user name and "pass" as the password and click "Submit". Then, surprisingly the original login.jsp is displayed:

Why? It should really invoke the MainMenu.do action. Let's check how MainMenu.do is implemented by checking struts-config.xml:
<struts-config> <action-mappings> <action path="/MainMenu" forward="/mainMenu.jsp"/> ... </action-mappings> </struts-config>

Check out mainMenu.jsp:


<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/app" prefix="app" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <app:checkLogon/> <html> <head> <title><bean:message key="mainMenu.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <h3><bean:message key="mainMenu.heading"/> <bean:write name="user" property="fullName" /></h3> <ul> <li><html:link action="/EditRegistration?action=Edit"><bean:message key="mainMenu.registration"/></html:link></li> <li><html:link forward="logoff"><bean:message key="mainMenu.logoff"/></html:link></li> </ul> </body> </html>

It is using a <checkLogon> tag defined by an "app" taglib, i.e., a taglib provided by this application. Obviously it is checking whether a user has logged on. If not, it will redirect to logon.jsp. To verify, check web.xml to see how the app taglib is defined:
<web-app>

476

Chapter 15 Integrating with Struts

... <taglib> <taglib-uri>/tags/app</taglib-uri> <taglib-location>/WEB-INF/app.tld</taglib-location> </taglib> <taglib> <taglib-uri>/tags/tapestry</taglib-uri> <taglib-location>/WEB-INF/tapestry-3.0.3.tld</taglib-location> </taglib> </web-app>

Check the app.tld file:


<taglib> <tlibversion>1.0</tlibversion> <jspversion>1.1</jspversion> <shortname>Application Tag Library</shortname> <uri>http://jakarta.apache.org/taglibs/struts-example-1.0</uri> <info> This tag library contains functionality specific to the Struts Example Application. </info> <tag> <name>checkLogon</name> <tagclass>org.apache.struts.webapp.example.CheckLogonTag</tagclass> <bodycontent>empty</bodycontent> <info> Validate that there is a currently logged on user, by checking for the existence of a session-scope bean under the specified name. If there is no such bean, forward control to the specified page, which will typically be a logon form. name - Name of the session-scope bean to check for [user] page - Context-relative path to the logon page [/logon.jsp] </info> <attribute> <name>name</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>page</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> ... </taglib>

The tag is implemented by the org.apache.struts.webapp.example.CheckLogonTag class:


public final class CheckLogonTag extends TagSupport { private String name = Constants.USER_KEY; private static String LOGIN_PATH = "/Logon.do"; private String page = LOGIN_PATH; public int doStartTag() throws JspException { return (SKIP_BODY); } public int doEndTag() throws JspException { // Is there a valid user logged on? boolean valid = false; HttpSession session = pageContext.getSession(); if ((session != null) && (session.getAttribute(name) != null)) { valid = true; } // Forward control based on the results if (valid) { return (EVAL_PAGE); } else { ModuleConfig config = (ModuleConfig) pageContext .getServletContext().getAttribute( org.apache.struts.Globals.MODULE_KEY); try { pageContext.forward(config.getPrefix() + page); } catch (ServletException e) { throw new JspException(e.toString()); } catch (IOException e) {

Integrating with Struts

477

throw new JspException(e.toString()); } return (SKIP_PAGE); } } public void release() { super.release(); this.name = Constants.USER_KEY; this.page = LOGIN_PATH; } }

Here it is checking if there is bean with name Constants.USER_KEY in the session. If yes, it means the user has logged on. Otherwise, it will forward to /Logon.do. Obviously, Logon.do is implemented by logon.jsp as specified in web.xml. Anyway, it means your Logon page is indeed invoking MainMenu.do properly.

Implementing the rewinding part of the Logon page


Your Logon page is not putting the bean into the session during the form submission. Do it now by modifying Logon.java. Again, you can model it after LogonAction.java:

478

Chapter 15 Integrating with Struts

Logon.java
public abstract class Logon extends CommonPage { public abstract String getUsername(); public abstract String getPassword(); @Bean public abstract ValidationDelegate getDelegate(); @Message("error.database.missing") public abstract String getMissingMsg(); @Message("error.password.mismatch") public abstract String getMismatchMsg(); User getUser(UserDatabase database, String username, String password) throws ExpiredPasswordException { User user = null; if (database == null) { getDelegate().setFormComponent(null); getDelegate().record(getMissingMsg(), null); } else { user = database.findUser(username); if ((user != null) && !user.getPassword().equals(password)) { user = null; } if (user == null) { getDelegate().setFormComponent(null); getDelegate().record(getMismatchMsg(), null); } } return user; } @InjectObject("service:tapestry.globals.WebRequest") public abstract WebRequest getRequest(); void saveUser(User user) { WebSession session = getRequest().getSession(true); session.setAttribute(Constants.USER_KEY, user); if (log.isDebugEnabled()) { log.debug("LogonAction: User '" True means create + user.getUsername() the session if it + "' logged on in session " doesn't exist yet + session.getId()); } } public void onSubmit() { UserDatabase database = getUserDatabase(); String username = getUsername(); String password = getPassword(); User user; try { user = getUser(database, username, password); } catch (ExpiredPasswordException e) { throw new RedirectException("ExpiredPassword.do"); } saveUser(user); if (getDelegate().getHasErrors()) { return; } throw new RedirectException("MainMenu.do"); } } }

LogonAction.java
public final class LogonAction extends BaseAction { static String USERNAME = "username"; static String PASSWORD = "password"; User getUser( UserDatabase database, String username, String password, ActionMessages errors) throws ExpiredPasswordException { User user = null; if (database == null) { errors.add( ActionMessages.GLOBAL_MESSAGE, new ActionMessage( "error.database.missing")); } else { user = database.findUser(username); if ((user != null) && !user.getPassword().equals(password)) { user = null; } if (user == null) { errors.add( ActionMessages.GLOBAL_MESSAGE, new ActionMessage( "error.password.mismatch")); } } return user; } void SaveUser( HttpServletRequest request, User user) { HttpSession session = request.getSession(); session.setAttribute( Constants.USER_KEY, user); if (log.isDebugEnabled()) { log.debug("LogonAction: User '" + user.getUsername() + "' logged on in session " + session.getId()); } } public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserDatabase database = getUserDatabase(request); String username = (String) PropertyUtils.getSimpleProperty( form, USERNAME); String password = (String) PropertyUtils.getSimpleProperty( form, PASSWORD); ActionMessages errors = new ActionMessages(); User user = getUser(database, username, password, errors); SaveUser(request, user); if (!errors.isEmpty()) { this.saveErrors(request, errors); return (mapping.getInputForward()); } return (findSuccess(mapping)); }

struts-config.xml
<action path="/SubmitLogon" type="org.apache.struts.webapp.example.LogonAction" name="LogonForm" scope="request" input="logon"> <exception key="expired.password" type="org.apache.struts.webapp.example.ExpiredPasswordException" path="/ExpiredPassword.do"/> </action>

In Struts, the handler for ExpiredPasswordException is specified in struts-config.xml, while Tapestry this is done in Java code. Some methods like getUserDatabase() and the "log" instance variable are provided by the BaseAction class. Similarly,

Integrating with Struts

479

you can create a corresponding BasePage class. But since there is already a BasePage class provided by Tapestry, so you may name it CommonPage instead:

CommonPage.java
public abstract class CommonPage extends BasePage { protected Log log = LogFactory.getLog(Constants.PACKAGE); @InjectObject("service:tapestry.globals.WebContext") public abstract WebContext getContext();

BaseAction.java
public class BaseAction extends Action { protected Log log = LogFactory.getLog(Constants.PACKAGE); protected UserDatabase getUserDatabase( HttpServletRequest request) { return (UserDatabase)servlet. getServletContext(). getAttribute(Constants.DATABASE_KEY); } protected ActionForward findFailure( ActionMapping mapping) { return (mapping. findForward(Constants.FAILURE)); } protected ActionForward findSuccess( ActionMapping mapping) { return (mapping. findForward(Constants.SUCCESS)); } }

protected UserDatabase getUserDatabase() { return (UserDatabase) getContext().getAttribute(Constants.DATABASE_KEY); } }

Now run the application and try to logon:

Clicking "Submit" will bring you to the main menu:

Rewriting a JSP include file as a Tapestry component


Next, you'll implement the Footer component that is meant to replace footer.jsp that is included in most pages in the application. First, check what footer.jsp looks like:
<%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <hr /> <p><html:link action="/Welcome"><bean:message key="index.title"/></html:link></p>

It contains a link to /Welcome. This action is defined as:

480

Chapter 15 Integrating with Struts

<struts-config> <action-mappings> <action path="/Welcome" type="org.apache.struts.webapp.example.WelcomeAction"> <forward name="failure" path="/Error.jsp" /> <forward name="success" path="/welcome.jsp" /> </action> ... </action-mappings> </struts-config>

It can be activated as /Welcome.do. So, modify Footer.html to simulate footer.jsp:

Footer.html
<html jwcid="$content$"> <hr /> <p><a jwcid="welcomeLink"><span key="index.title">Title</span></a></p> </html>

footer.jsp
<%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <hr /> <p><html:link action="/Welcome"><bean:message key="index.title"/></html:link></p>

Define the component in Footer.jwc:


<component-specification class="org.apache.struts.webapp.example.Footer" allow-body="no" allow-informal-parameters="no"> <component id="welcomeLink" type="DirectLink"> <binding name="listener" value="listener:onClick"/> </component> </component-specification>

Create Footer.java:
public class Footer extends BaseComponent { public void onClick() { throw new RedirectException("Welcome.do"); } }

Now run the application again and you should see the footer on the Logon page:

Clicking the link in the footer will bring you to the welcome page:

Integrating with Struts

481

Using localized messages


Next, you'll fix the problem with the localized messages. Create MailReader.properties in WEB-INF and copy the required messages from ApplicationResources.properties:
error.database.missing=User database is missing, cannot validate logon credentials error.password.mismatch=Invalid username and/or password, please try again index.title=MailReader Demonstration Application (Struts 1.2.1-dev) logon.title=MailReader Demonstration Application - Logon prompt.password=Password prompt.username=Username username=Username password=Password

Run the application again and it should use the messages:

To support Japanese, create ApplicationResources_ja.properties:

MailReader_ja.properties

and

copy

the

relevant

strings

from

error.database.missing=\u30E6\u30FC\u30B6\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u898B\u3064\u304B\u 308A\u307E\u305B\u3093\u3002\u30ED\u30B0\u30AA\u30F3\u306E\u8A8D\u8A3C\u304C\u51FA\u6765\u307E\u305B\u3 093 error.password.mismatch=\u30E6\u30FC\u30B6\u540D\u307E\u305F\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\ u4E0D\u6B63\u3067\u3059\u3002\u518D\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044 index.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3(Struts 1.1-dev) logon.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 \u30ed\u30b0\u30aa\u30f3 prompt.password=\u30d1\u30b9\u30ef\u30fc\u30c9 prompt.username=\u30e6\u30fc\u30b6\u540d username=\u30e6\u30fc\u30b6\u540d password=\u30d1\u30b9\u30ef\u30fc\u30c9

This file is in a special Unicode encoding. For example, \u30E6 means a Unicode character whose code is hexadecimal 30E6. If this file was in JIS encoding, then you would need to specify this fact in MailReader.application. However, as this file is a special Unicode for which Java has built-in support, so you don't need to do that.

482

Chapter 15 Integrating with Struts

If you run the application now and change to Japanese and then go to the Logon page, it will still appear in English. This is because Struts stores the selected locale in the session, while Tapestry stores it in a cookie. When the user changes the locale, the Struts locale is set but the Tapestry one isn't. To solve this problem, you may try to set the Tapestry locale when you set the Struts locale. To do that, let's check how the Struts locale is set. In welcome.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html> <head> <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <h3><bean:message key="index.heading"/></h3> <ul> <li><html:link action="/EditRegistration?action=Create"><bean:message key="index.registration"/></html:link></li> <li><html:link page="/app?service=page&page=Logon"><bean:message key="index.logon"/></html:link></li> </ul> <h3>Language Options</h3> <ul> <li><html:link action="/Locale?language=en">English</html:link></li> <li><html:link action="/Locale?language=ja" useLocalEncoding="true">Japanese</html:link></li> <li><html:link action="/Locale?language=ru" useLocalEncoding="true">Russian</html:link></li> </ul> <hr /> <p><html:img bundle="alternate" pageKey="struts.logo.path" altKey="struts.logo.alt"/></p> <p><html:link action="/Tour"><bean:message key="index.tour"/></html:link></p> </body> </html>

It is done by the Locale action. It is defined in struts-config.xml:


<action-mappings> ... <action path="/Locale" type="org.apache.struts.webapp.example.LocaleAction" parameter="welcome" /> </action-mappings>

LocaleAction.java is:
public final class LocaleAction extends BaseAction { private boolean isBlank(String string) { return ((string == null) || (string.trim().length() == 0)); } private static final String LANGUAGE = "language"; private static final String COUNTRY = "country"; private static final String PAGE = "page"; private static final String FORWARD = "forward"; private static final String LOCALE_LOG = "LocaleAction: Missing page or forward parameter"; public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String language = request.getParameter(LANGUAGE); String country = request.getParameter(COUNTRY); Locale locale = getLocale(request); if ((!isBlank(language)) && (!isBlank(country))) { locale = new Locale(language, country); } else if (!isBlank(language)) { locale = new Locale(language, ""); } HttpSession session = request.getSession(); session.setAttribute(Globals.LOCALE_KEY, locale); String target = request.getParameter(PAGE); if (!isBlank(target)) return new ActionForward(target); target = request.getParameter(FORWARD); if (isBlank(target)) target = mapping.getParameter(); if (isBlank(target)) { log.warn(LOCALE_LOG); return null; } return mapping.findForward(target); } }

Integrating with Struts

483

However, to set the Tapestry locale, one needs to get access to the engine and call setLocale(). But how to get the engine here? It is extremely difficult to get the engine from non-Tapestry code. So this idea doesn't really work. Instead, you can do the other way around: create a Tapestry page to replace for LocaleAction and let it set both the Tapestry locale (it can call getEngine()) and the Struts locale (it can access the session). This approach works because you work with higher level concepts (engine) in Tapestry, while you can still access lower level concepts (session). This is untrue when working with Struts. You work with lower level concepts but you can't access the higher level concepts. Anyway, let's do it. Create a ChangeLocale page. ChangeLocale.html is not used and can be empty:
<html/>

ChangeLocale.page is:
<page-specification class="org.apache.struts.webapp.example.ChangeLocale"/>

ChangeLocale.java is:
public abstract class ChangeLocale extends BasePage implements IExternalPage, PageBeginRenderListener { @InjectObject("service:tapestry.globals.WebRequest") public abstract WebRequest getRequest(); public void activateExternalPage(Object[] parameters, IRequestCycle cycle) { Locale locale = new Locale((String) parameters[0]); WebSession session = getRequest().getSession(true); session.setAttribute(Globals.LOCALE_KEY, locale); Set the Struts locale getEngine().setLocale(locale); Set the Tapestry locale } public void pageBeginRender(PageEvent event) { throw new RedirectException("Welcome.do"); } }

Always display the welcome page. This is what is needed for the moment. Let welcome.jsp call it:

Why not redirect in activateExternalPage()? This is because the Tapestry locale is saved into a cookie only when the rendering phase is started. If you redirect in the event handling phase, the locale won't be saved.

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> "sp" means "service parameter". Here, you're passing <%@ taglib uri="/tags/struts-html" prefix="html" %> the string "en" as the first parameter to <html> activateExternalPage(). Note that there is an extra "S" at <head> the beginning so that Tapestry knows that it is a String. <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> If you'd like to pass a second parameter, do it like: </head> <h3><bean:message key="index.heading"/></h3> <ul> <li><html:link action="/EditRegistration?action=Create"><bean:message key="index.registration"/></html:link></li> <li><html:link page="/app?service=page&page=Logon"><bean:message key="index.logon"/></html:link></li> </ul> <h3>Language Options</h3> <ul> <li><html:link page="/app?service=external&page=ChangeLocale&sp=Sen">English</html:link></li> <li><html:link page="/app?service=external&page=ChangeLocale&sp=Sja" useLocalEncoding="true">Japanese</html:link></li> <li><html:link page="/app?service=external&page=ChangeLocale&sp=Sru" Pass a long useLocalEncoding="true">Russian</html:link></li> value 99 </ul> <hr /> <p><html:img bundle="alternate" pageKey="struts.logo.path" altKey="struts.logo.alt"/></p> <p><html:link action="/Tour"><bean:message key="index.tour"/></html:link></p> </body> </html> <html:link page="...&sp=Sen&sp=l99"> <html:link page="...&sp=Sen&sp=T"> <html:link page="...&sp=Sen&sp=99">

Pass a boolean true

Pass an int 99

484

Chapter 15 Integrating with Struts

Now run it:

Click "Japanese" to change both the Struts locale and the Tapestry locale:

The welcome page is displayed in Japanese. So the Struts locale has been changed. To see if the Tapestry locale has been changed, open the Logon page:

Integrating with Struts

485

It means it has indeed been changed. At the moment you're using global properties files. In fact, some messages are only used for a single page. For example, the logon.title message is only used by the Logon page. So it's best to put it into Logon.properties. As there is nothing special here, you can do it yourself.

Supporting an alternate message resource bundle


At the moment your Logon page is:

This is not exactly the same as logon.jsp:

The password prompt is different. This is because the password prompt in logon.jsp is using the alternate message

486

Chapter 15 Integrating with Struts

resource bundle:
... <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <html:text property="username" size="16" maxlength="18"/> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password" bundle="alternate"/>: </th> <td align="left"> <html:password property="password" size="16" maxlength="18" redisplay="false"/> </td> </tr> <tr> <td align="right"> <html:submit property="Submit" value="Submit"/> </td> <td align="left"> <html:reset/> </td> </tr> </table>

Therefore, you should do the same thing in Logon.html:


... <table border="0" width="100%"> <tr> <th align="right"> <span key="prompt.username"/>: </th> <td align="left"> <input type="text" jwcid="username" size="16" maxlength="18"/> </td> </tr> <tr> <th align="right"> <span key="alternate.prompt.password"/>: </th> <td align="left"> <input type="password" jwcid="password" size="16" maxlength="18"/> </td> </tr> <tr> <td align="right"> <input type="submit" value="Submit"/> </td> <td align="left"> <input type="reset"/> </td> </tr> </table>

As you can't specify a message resource bundle in a <span> in Tapestry, a quick and dirty solution is to merge the bundle name into the key. Then, copy the prompt.password message from AlternateApplicationResources.properties into MailReader.properties:
error.database.missing=User database is missing, cannot validate logon credentials error.password.mismatch=Invalid username and/or password, please try again index.title=MailReader Demonstration Application (Struts 1.2.1-dev) logon.title=MailReader Demonstration Application - Logon prompt.password=Password prompt.username=Username username=Username password=Password alternate.prompt.password=Enter your Password here ==>

Now, run it again and you should see:

Integrating with Struts

487

Do the same thing for MailReader_ja.properties:


error.database.missing=\u30E6\u30FC\u30B6\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u898B\u3064\u304B\u 308A\u307E\u305B\u3093\u3002\u30ED\u30B0\u30AA\u30F3\u306E\u8A8D\u8A3C\u304C\u51FA\u6765\u307E\u305B\u3 093 error.password.mismatch=\u30E6\u30FC\u30B6\u540D\u307E\u305F\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\ u4E0D\u6B63\u3067\u3059\u3002\u518D\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044 index.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3(Struts 1.1-dev) logon.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 \u30ed\u30b0\u30aa\u30f3 prompt.password=\u30d1\u30b9\u30ef\u30fc\u30c9 prompt.username=\u30e6\u30fc\u30b6\u540d username=\u30e6\u30fc\u30b6\u540d password=\u30d1\u30b9\u30ef\u30fc\u30c9 alternate.prompt.password=\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b==>

Then the Japanese version will also work:

Summary
To integrate Struts with Tapestry, you need to note that a Tapestry page is performing the job of a JSP file and an action (form submission). So, don't try to migrate just a JSP file to Tapestry without migrating the corresponding action or vice versa. After migrating a JSP page to Tapestry, how to call it from another JSP page? You can use the <html:link page="URL"> tag in Struts to do that. In the submit listener most likely you will need to call a Struts action. To do that, just throw a RedirectException. To migrate a custom JSP tag to Tapestry, turn it into a component. To migrate a JSP include file, also turn it into a component. Struts uses global properties files for i18n. In Tapestry, you can migrate the global messages to the application's properties files and migrate the per-page messages to the properties files for each individual pages. As Struts and Tapestry each has their own current locales, when changing the current locale, you need to set both. This can be done in Tapestry but very hard to do in Struts.

488

Chapter 15 Integrating with Struts

In Tapestry, the locale is saved into a cookie only when the rendering phase is started. So if you set the locale and throw a RedirectException in the event handling phase, the change won't take effect.

Enjoying Web Development with Tapestry

489

References

Hibernate developers. Hibernate Reference Documentation. http://www.hibernate.org. Howard Lewis Ship and contributors. Tapestry User Guide. http://www.apache.org/tapestry. Howard Lewis Ship and contributors. Tapestry Component Reference. http://www.apache.org/tapestry. Howard Lewis Ship and contributors. Tapestry API doc. http://www.apache.org/tapestry. HtmlUnit developers. HtmlUnit Documentation. http://htmlunit.sourceforge.net. Kent Beck. Four Layer Architecture. http://c2.com/cgi/wiki?FourLayerArchitecture. Kent Beck. Test Driven Development: By Example. Addison-Wesley Professional (2002). Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford. Patterns of Enterprise Application Architecture. Addison-Wesley Professional (2002). Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts. Refactoring: Improving the Design of Existing Code. Addison-Wesley Professional (1999). Struts developers. The Struts User's Guide. http://struts.apache.org. Sun Microsystems. Java Servlet Specification. http://java.sun.com/products/servlet. Tomcat developers. Tomcat Documentation. http://jakarta.apache.org/tomcat.

490

Enjoying Web Development with Tapestry

Alphabetical Index
.script file...................................................................................................................................................................296 $content$..................................................................................................................................................................135 $remove$..................................................................................................................................................................205 AbstractComponent..................................................................................................................................................137 AbstractService.........................................................................................................................................................257 Activate........................................................................................................................................................................... Another page.........................................................................................................................................................37 AJAX.........................................................................................................................................................................308 With @EventListener...........................................................................................................................................337 With Autocompleter.............................................................................................................................................311 With DirectLink components................................................................................................................................334 With Submit components....................................................................................................................................336 Anonymous component..............................................................................................................................................46 Ant.............................................................................................................................................................................196 Ant task........................................................................................................................................................................... Making a jar file....................................................................................................................................................196 Any component.........................................................................................................................................................145 API doc.......................................................................................................................................................................51 Application layer........................................................................................................................................................433 Application servlet.............................................................................................................................................269, 446 Application specification..............................................................................................................................................91 Application state objects...........................................................................................................................................100 Checking the existence of...................................................................................................................................118 In application........................................................................................................................................................100 In session.............................................................................................................................................................100 Asset.........................................................................................................................................................................180 Asset service.............................................................................................................................................................198 Autocompleter component........................................................................................................................................311 BaseComponent.......................................................................................................................................................137 BasePage...................................................................................................................................................................22 BaseValidator..............................................................................................................................................................72 Bean............................................................................................................................................................................64 Binding............................................................................................................................................................................ Accessing............................................................................................................................................................140 Binding prefix.................................................................................................................................................................. Component............................................................................................................................................................71 Default...................................................................................................................................................................26 Default (in template)..............................................................................................................................................46 Listener...............................................................................................................................................................34p. Literal.....................................................................................................................................................................26 Message......................................................................................................................................................159, 163 Ognl.......................................................................................................................................................................21 Block component......................................................................................................................................................222 Body component.........................................................................................................................................................50 Border component....................................................................................................................................................285 Bucket brigade............................................................................................................................................................39 Business transaction.................................................................................................................................................426 Business transaction layer........................................................................................................................................433 Cascading style sheet...............................................................................................................................................206 Code smells..............................................................................................................................................................381 Comparator...............................................................................................................................................................231 Component..................................................................................................................................................................... Allowing body.......................................................................................................................................................135 Copy of...................................................................................................................................................................89 Defining your own................................................................................................................................................132 Documenting........................................................................................................................................................146 Getting the containing page.................................................................................................................................287

Enjoying Web Development with Tapestry

491

Hidden..................................................................................................................................................................114 In a form...............................................................................................................................................................142 Map......................................................................................................................................................................222 Must be closed.....................................................................................................................................................183 PageLink..............................................................................................................................................................111 Parameter..............................................................................................................................................................21 Reference..............................................................................................................................................................52 Reusing in another project...................................................................................................................................147 Setting specification location...............................................................................................................................147 Specification........................................................................................................................................................134 Template..............................................................................................................................................................134 Type.......................................................................................................................................................................21 When to create....................................................................................................................................................192 Conditional component................................................................................................................................................... Rendered as a <li>................................................................................................................................................76 Context descriptor.......................................................................................................................................................19 Context path........................................................................................................................................................19, 270 ContextAsset.............................................................................................................................................................180 Cookies.....................................................................................................................................................................103 CSS...........................................................................................................................................................................206 Accessed using an asset.....................................................................................................................................210 Storing in a file.....................................................................................................................................................209 Database......................................................................................................................................................................... Schema................................................................................................................................................................443 DataSet.....................................................................................................................................................................316 DateFormat...............................................................................................................................................................164 DatePicker component...............................................................................................................................................49 Must be contained by a Body component..............................................................................................................49 DBCP........................................................................................................................................................................400 Debugging a Tapestry application..............................................................................................................................26 Declared components.................................................................................................................................................46 Delegator component.................................................................................................................................................62 Direct service......................................................................................................................................................88, 406 DirectLink component.................................................................................................................................................86 Disabling a link..........................................................................................................................................................288 Disabling caching in Tapestry.....................................................................................................................................24 Dividing the application into layers............................................................................................................................426 DocBase...................................................................................................................................................................270 Dojo.............................................................................................................................................................................49 Domain logic.............................................................................................................................................................426 Eclipse........................................................................................................................................................................12 Engine...............................................................................................................................................................171, 270 Engine services.........................................................................................................................................................256 Error renderer.............................................................................................................................................................62 EvenOdd bean..........................................................................................................................................................207 EventListener annotation..........................................................................................................................................337 External service.............................................................................................................................................................. Encoding of service parameters..........................................................................................................................483 ExternalAsset............................................................................................................................................................180 ExternalPageCallback...............................................................................................................................................126 Field tracking...............................................................................................................................................................74 FieldLabel...................................................................................................................................................................71 FireBug.....................................................................................................................................................................344 For component................................................................................................................................................................ Index parameter...................................................................................................................................................204 Rendered as a <tr> element..................................................................................................................................77 Form component.........................................................................................................................................................34 Rewind just the form, not the whole page...........................................................................................................240 Submit types........................................................................................................................................................333 Formal parameters.....................................................................................................................................................77 Supporting............................................................................................................................................................140

492

Enjoying Web Development with Tapestry

Friendly URL.............................................................................................................................................................267 GetWriter vs getOutputStream.................................................................................................................................254 Hibernate..................................................................................................................................................................436 Closing a session.................................................................................................................................................442 Creating a session...............................................................................................................................................441 In a four-layered architecture...............................................................................................................................456 LazyInitializationException...................................................................................................................................445 Loading objects....................................................................................................................................................442 Mapping file..........................................................................................................................................................440 Must not access objects if session is closed.......................................................................................................444 Properties............................................................................................................................................................437 Session................................................................................................................................................................440 SessionFactory....................................................................................................................................................440 Setting transaction isolation.................................................................................................................................441 Updating the database schema...........................................................................................................................443 Using a query.......................................................................................................................................................442 Using a transaction..............................................................................................................................................442 Hidden component......................................................................................................................................................97 Hivemind......................................................................................................................................................................... Configuration.......................................................................................................................................................100 Contribution..........................................................................................................................................................100 Module.................................................................................................................................................................101 Module descriptor................................................................................................................................................101 Registry................................................................................................................................................................446 Service.................................................................................................................................................................253 Hivemind service............................................................................................................................................................. Access from a regular Java class........................................................................................................................260 Auto-wiring...........................................................................................................................................................261 Cleaning up..........................................................................................................................................................445 Creating...............................................................................................................................................................260 Manually wiring....................................................................................................................................................265 Proxy for...............................................................................................................................................................266 Singleton model...................................................................................................................................................446 Threaded model...................................................................................................................................................446 Hivemodule.xml........................................................................................................................................................100 HtmlUnit....................................................................................................................................................................351 Alert handler........................................................................................................................................................373 Checking a popup window...................................................................................................................................377 Checking a table..................................................................................................................................................365 Checking some text in the page..........................................................................................................................356 Getting an element by id......................................................................................................................................357 Getting an element by name................................................................................................................................354 Getting the result page........................................................................................................................................354 Manipulating the options of a select field.............................................................................................................361 Setting the value of a text field.............................................................................................................................356 Simulating a button click......................................................................................................................................356 WebWindowListener...........................................................................................................................................375 HttpServletResponse................................................................................................................................................252 Content type.........................................................................................................................................................252 Content-disposition..............................................................................................................................................252 Setting content length..........................................................................................................................................274 Setting header.....................................................................................................................................................252 I18n...........................................................................................................................................................................163 IAsset........................................................................................................................................................................180 IAutocompleteModel.................................................................................................................................................311 ICallback...................................................................................................................................................................126 IExternalPage...........................................................................................................................................................126 If component...............................................................................................................................................................76 Implicit components....................................................................................................................................................46 Informal parameters....................................................................................................................................................77 Inherit...........................................................................................................................................................193, 304

Enjoying Web Development with Tapestry

493

Renderring...........................................................................................................................................................139 Infrastructure.....................................................................................................................................................257, 277 InitialContext.............................................................................................................................................................401 Injecting a bean...........................................................................................................................................................65 Injecting a meta property..........................................................................................................................................275 Injecting a page...........................................................................................................................................................40 Injecting a property..................................................................................................................................................42p. Injecting an application state object..........................................................................................................................101 Injecting an object.....................................................................................................................................................253 Insert component........................................................................................................................................................21 Integrating Tapestry and Struts....................................................................................................................................... Invoking a Struts action from Tapestry................................................................................................................472 Invoking a Tapestry page from JSP....................................................................................................................472 Rewriting a JSP include file as a Tapestry component.......................................................................................479 Synchronizing their locales..................................................................................................................................483 Internationalization....................................................................................................................................................163 InvokeListener component........................................................................................................................................329 IPropertySelectionModel.....................................................................................................................................47, 167 IUploadFile................................................................................................................................................................275 Telling if a file was selected.................................................................................................................................278 Jar file............................................................................................................................................................................. Export to..............................................................................................................................................................150 Using Ant to make...............................................................................................................................................196 Javascript..................................................................................................................................................................294 Attaching an event handler to an element...........................................................................................................299 Encapsulating in a component............................................................................................................................302 Event handler.......................................................................................................................................................294 Generating a unique name..................................................................................................................................299 Initialization..........................................................................................................................................................300 Inserting into page...............................................................................................................................................297 Onclick.................................................................................................................................................................294 Onload.................................................................................................................................................................299 JDBC.........................................................................................................................................................................393 Auto commit.........................................................................................................................................................396 Connection pooling..............................................................................................................................................399 Connection URL..................................................................................................................................................393 DataSource..........................................................................................................................................................399 Driver for PostgreSQL.........................................................................................................................................393 DriverManager.....................................................................................................................................................393 Executing a statement.........................................................................................................................................394 PreparedStatement..............................................................................................................................................394 Setting parameters for a statement.....................................................................................................................394 JNDI..................................................................................................................................................................401, 438 JNDI................................................................................................................................................................................ Looking up a context............................................................................................................................................401 JUnit..........................................................................................................................................................................352 AssertEquals() method........................................................................................................................................356 AssertTrue() method............................................................................................................................................354 AssetSame() method...........................................................................................................................................377 Running a particular test case.............................................................................................................................371 Running all the tests............................................................................................................................................373 SetUp() method...................................................................................................................................................363 TestCase.............................................................................................................................................................352 TestSuite..............................................................................................................................................................373 L10n..........................................................................................................................................................................163 Library specification..................................................................................................................................................150 Specify location of................................................................................................................................................152 Link factory................................................................................................................................................................263 Link renderer.............................................................................................................................................................378 Locale.......................................................................................................................................................................164 Choosing a...........................................................................................................................................................166

494

Enjoying Web Development with Tapestry

Making the change take effect immediately........................................................................................................173 Setting the engine's.............................................................................................................................................171 Localization...............................................................................................................................................................163 Localizing an image..................................................................................................................................................184 Localizing page template..........................................................................................................................................188 Login.........................................................................................................................................................................114 Logout.......................................................................................................................................................................128 Markup writer............................................................................................................................................................138 MessageFormat........................................................................................................................................................177 Meta property..............................................................................................................................................................91 For a page/component in a library.......................................................................................................................194 Locations for defining...........................................................................................................................................275 Multi-threads in HTTP request handling...................................................................................................................406 Object Graph Navigation Language...........................................................................................................................22 Object locator............................................................................................................................................................253 Object reference.......................................................................................................................................................253 Instance prefix.....................................................................................................................................................258 Service prefix.......................................................................................................................................................253 Objects stored into a session must implement Serializable.....................................................................................103 OGNL..........................................................................................................................................................................22 Accessing a static field..........................................................................................................................................94 Creating a Map....................................................................................................................................................298 Creating an array.................................................................................................................................................212 Looking up a Map..................................................................................................................................................64 Output encoding........................................................................................................................................................191 Page................................................................................................................................................................................ External................................................................................................................................................................126 Invoking from JSP...............................................................................................................................................472 Specification..........................................................................................................................................................23 Template................................................................................................................................................................22 Page service.............................................................................................................................................................180 Page template................................................................................................................................................................. Specifying encoding of.........................................................................................................................................189 PageBeginRenderListener........................................................................................................................................175 PageCallback............................................................................................................................................................126 PageDetachListener...................................................................................................................................................41 PageRedirectException....................................................................................................................................124, 174 Vs RedirectException..........................................................................................................................................274 PageValidateListener................................................................................................................................................124 Parameter....................................................................................................................................................................... Default value........................................................................................................................................................141 Direction "form"....................................................................................................................................................142 Making optional....................................................................................................................................................141 Passing information from one page to another...........................................................................................................39 Persistence...............................................................................................................................................................426 Persistence layer......................................................................................................................................................433 Persistent property.......................................................................................................................................................... Client....................................................................................................................................................................109 Session................................................................................................................................................................112 Plain old Java object.................................................................................................................................................454 POJO........................................................................................................................................................................454 PopupLinkRenderer..................................................................................................................................................378 Portlet........................................................................................................................................................................258 PortletRequest..........................................................................................................................................................258 PortletResponse.......................................................................................................................................................258 PostgreSQL..............................................................................................................................................................384 Creating a database............................................................................................................................................388 Creating a database user....................................................................................................................................387 Creating a table...................................................................................................................................................390 Installing...............................................................................................................................................................384 Issuing SQL statements interactively..................................................................................................................392

Enjoying Web Development with Tapestry

495

PgAdmin..............................................................................................................................................................386 Viewing the records of a table.............................................................................................................................392 Preferred languages in browser................................................................................................................................160 Private asset.............................................................................................................................................................195 PrivateAsset..............................................................................................................................................................180 Properties file............................................................................................................................................................158 For a component..................................................................................................................................................193 Specifying encoding for.......................................................................................................................................160 Property........................................................................................................................................................................... Setting initial value.................................................................................................................................................44 Setting initial value using annotation.....................................................................................................................45 Property specification..................................................................................................................................................42 PropertySelection component.....................................................................................................................................47 PropertyUtils................................................................................................................................................................56 Reading a meta property..........................................................................................................................................277 RedirectException.....................................................................................................................................................273 Vs PageRedirectException..................................................................................................................................274 Refactoring................................................................................................................................................................381 Regular expression.....................................................................................................................................................82 Reloading an application on classes changes............................................................................................................25 RenderBlock component..........................................................................................................................................290 Vs RenderBody component.................................................................................................................................292 RenderBody component...........................................................................................................................................285 Vs RenderBlock component................................................................................................................................292 RenderPage() method..............................................................................................................................................255 Request cycle.............................................................................................................................................................37 RequestContext........................................................................................................................................................252 Resource path...................................................................................................................................................101, 152 Restart service..........................................................................................................................................................128 Rewind......................................................................................................................................................................143 Flag......................................................................................................................................................................144 Script component......................................................................................................................................................297 Binding symbols as parameters..........................................................................................................................298 Script file...................................................................................................................................................................296 Select... for update....................................................................................................................................................412 Select... limit... offset.................................................................................................................................................231 Separating domain logic, business transaction, persistence and UI........................................................................426 Service............................................................................................................................................................................ Creating your own................................................................................................................................................256 Generating a link to call.......................................................................................................................................263 Injecting...............................................................................................................................................................266 Registration in the application..............................................................................................................................258 Retrieving parameters.........................................................................................................................................257 Service encoder........................................................................................................................................................268 Service encoding......................................................................................................................................................268 Service layer.............................................................................................................................................................433 ServiceLink component....................................................................................................................................128, 264 Servlet.......................................................................................................................................................................257 Servlet mapping.....................................................................................................................................................270p. Session.....................................................................................................................................................................100 Accessing from a page........................................................................................................................................477 Get rid of..............................................................................................................................................................103 How to maintain...................................................................................................................................................103 Id..........................................................................................................................................................................104 JSESSIONID.......................................................................................................................................................104 Shell component.........................................................................................................................................................49 Using external stylesheets...................................................................................................................................210 Showing a popup window.........................................................................................................................................378 Specifying the packages to look for page classes......................................................................................................91 StringPropertySelectionModel....................................................................................................................................47 Struts.........................................................................................................................................................................462

496

Enjoying Web Development with Tapestry

Submit component......................................................................................................................................................93 Action listener........................................................................................................................................................96 Assigning a listener to............................................................................................................................................94 Assigning a tag to..................................................................................................................................................93 Delaying the call to the listener..............................................................................................................................96 Symbol......................................................................................................................................................................296 Table component......................................................................................................................................................213 Column definition..............................................................................................................................................217p. Customizing how to get the cell value.................................................................................................................217 Customizing the column title................................................................................................................................218 FullTableSessionStateManager...........................................................................................................................234 Getting current row..............................................................................................................................................222 IBasicTableModel................................................................................................................................................229 Making a column unsortable................................................................................................................................244 Paging..................................................................................................................................................................225 Paging state.........................................................................................................................................................234 Providing a Block to render a cell value...............................................................................................................222 Reading column title from properties file.............................................................................................................219 Setting page size.................................................................................................................................................226 Sorting state.........................................................................................................................................................234 Specifying CSS class for rows.............................................................................................................................220 Specifying CSS class for titles.............................................................................................................................221 TableColumns......................................................................................................................................................245 TablePages..........................................................................................................................................................245 TableRows...........................................................................................................................................................245 TableSessionStateManager................................................................................................................................234 TableValues.........................................................................................................................................................245 TableView............................................................................................................................................................245 Updating cached rows.........................................................................................................................................243 Use of session.....................................................................................................................................................234 Tapestry.......................................................................................................................................................................... Installing.................................................................................................................................................................14 Meaning of URL.....................................................................................................................................................20 TDD...........................................................................................................................................................................351 Cannot reveal all problems..................................................................................................................................371 Complemented by manual inspection.................................................................................................................371 Making a test pass ASAP....................................................................................................................................358 Tests as a safety net............................................................................................................................................381 Why run a test if it will definitely fail.....................................................................................................................377 Writing a failing test first before fixing a bug........................................................................................................371 Test case..................................................................................................................................................................352 Test driven development..........................................................................................................................................351 Thread safe...............................................................................................................................................................441 Tomcat........................................................................................................................................................................12 Creating a resource.............................................................................................................................................400 Manager.................................................................................................................................................................23 Reloading an application.......................................................................................................................................24 Reloading an application automatically.................................................................................................................25 Resource reference.............................................................................................................................................399 Shared folder.........................................................................................................................................................18 Transaction...............................................................................................................................................................396 Business transaction............................................................................................................................................418 Commit................................................................................................................................................................396 Deadlock..............................................................................................................................................................413 Ensuring real serializability..................................................................................................................................412 Even isolation is set to serializable, transactions may not be serializable...........................................................412 Isolation level.......................................................................................................................................................410 Making sure the data hasn't changed..................................................................................................................417 Race condition.....................................................................................................................................................410 Rollback...............................................................................................................................................................396 Serializable execution..........................................................................................................................................409

Enjoying Web Development with Tapestry

497

Serialized execution.............................................................................................................................................409 Setting isolation level to serializable....................................................................................................................411 Spanning multiple requests.................................................................................................................................417 System transaction..............................................................................................................................................418 Translator....................................................................................................................................................................58 Date.................................................................................................................................................................60, 80 Number..................................................................................................................................................................58 UI layer......................................................................................................................................................................433 Upload component....................................................................................................................................................275 URL rewriting............................................................................................................................................................107 User interface............................................................................................................................................................426 Using <span> to generate a localized string.............................................................................................................164 Validating........................................................................................................................................................................ A DatePicker..........................................................................................................................................................79 A TextArea.............................................................................................................................................................79 A TextField.............................................................................................................................................................66 Validation........................................................................................................................................................................ Using javascript.....................................................................................................................................................77 Validation delegate.....................................................................................................................................................61 Validator......................................................................................................................................................................66 Creating your own..................................................................................................................................................72 MaxDate................................................................................................................................................................80 Min.........................................................................................................................................................................66 MinDate..................................................................................................................................................................80 Others....................................................................................................................................................................81 Required................................................................................................................................................................70 Using a bean as.....................................................................................................................................................74 Web application archive............................................................................................................................................275 Web.xml....................................................................................................................................................................271 WebRequest.............................................................................................................................................................258 WebResponse..........................................................................................................................................................258

Das könnte Ihnen auch gefallen