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

Publisher: TipTec Development Author's email: [email protected] Book website: http://www.agileskills2.org Notice: All rights reserved. No part of this publication may be reproduced, stored in a retrieval system or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher. Edition: Third edition 2006

Enjoying Web Development with Tapestry 3

Foreword

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

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

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

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

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

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

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

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

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

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

11

Chapter 1

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

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

Developing a Hello World application with Tapestry Suppose that you'd like to develop an application like this:

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

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

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

Open a browser and go to http://localhost:8080 and you should see: 14 Chapter 1 Getting Started with Tapestry

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

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

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

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

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

Set the output folder as shown below: 16 Chapter 1 Getting Started with Tapestry

Finally, you should see the project structure:

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

Click "Add Library" and choose "User Library": Getting Started with Tapestry 17

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

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

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

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

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

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

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

Next, create a new file Home.page in the same folder as Home.html with the following content: 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: HelloWorld HelloWorld org.apache.tapestry.ApplicationServlet 1 HelloWorld /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

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

Actually, this is no longer used in Tomcat 5.5. In Tomcat 5.5, it uses the filename of the context HelloWorld.xml /HelloWorld descriptor to determine the path: Foo.xml /Foo

Bar.xml /Bar

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

What does this URL mean? It is interpreted this way: Getting Started with Tapestry 21

Context 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: Hello world ! is just a regular HTML element. It is used to enclose a section of HTML code. Next, add an attribute to this span: Hello world! "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 element. That is, the process is like:

1: Look, just regular HTML code Tapestry 2: Output it Hello John ! 3: Look, we have a component here 7: Regular 8: Output it HTML code again Hello world !

4: Create the component. It 5: Generate HTML for is just a Java object. yourself

Component 6: Output HTML code for "subject" itself such as "John" However, in order for Tapestry to create the component, it needs to know what type of component it is. Therefore, you need specify its type. This is done in the Home.page file: 22 Chapter 1 Getting Started with Tapestry

We're talking about the It is an Insert component. component named It will output some plain "subject" here text as the HTML.

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

Component "subject"

...

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

1: Generate HTML for yourself 2: call the getGreetingSubject() method

Component "subject" 4: Output "John"

...

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

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

Define a getGreetingSubject() method that returns a string:

Why it is an abstract class? For the moment just remember that it must be abstract. Tapestry will create a subclass for it automatically.

package com.ttdev.helloworld;

import org.apache.tapestry.html.BasePage;

public abstract class Home extends BasePage { public String getGreetingSubject() { return "John"; } } Modify Home.page to use your subclass instead of the default BasePage: 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: 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: Hello world! Good! 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 "John Paul "; } } 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):

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

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: 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: 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: 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: StockQuote StockQuote org.apache.tapestry.ApplicationServlet 1 StockQuote /app Create a context descriptor StockQuote.xml in c:\tomcat\conf\Catalina\localhost: Create Home.html in the context/WEB-INF to display the HTML form:

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

and then render everything inside its template (called its "body"). The "stockId" component is a TextField component. It will generate an HTML element like . 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
. That is, the whole process is like: Page object 1: Render yourself

2: Output the start tag for an HTML form element stockQuoteForm

7: Output regular
HTML code 3: Look, what I need to render? 8: Output the end tag

4: Render yourself

5: Call getStockId() on the page object. Suppose the return 6: Output an HTML text value is "MSFT". input element like 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: 6: Look, what expression to evaluate? Oh, it is a listener Tapestry expression. Page object void onOk() { 1: Handle the form ... submission } 7: Create it Action listener void actionTriggered() { page.onOk(); }

9: Call it

8: Call actionTriggered() stockQuoteForm

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

4: It's "SUN" stockId

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

public String getStockId() { return "MSFT"; } public void setStockId(String stockId) { this.stockId = stockId; } public void onOk() { System.out.println("Listener called. Stock id is: " + stockId); } } Then use it for the page: 36 Chapter 2 Using Forms

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: Using Forms 37

Creating the result page Next, you'll display the result page. Create Result.html in the same folder as Home.html: The stock value is: . 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 line and the line will not be shown): Result.page

An OGNL expression (remember that ognl is the default prefix). It will call getStockValue() on the Result.java page object public abstract class Result extends BasePage { public int getStockValue() { return 100; } }

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

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

Action listener

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

public abstract class Home extends BasePage { private String stockId;

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

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

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

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

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

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

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

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

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

1: Create a page object for Home Tapestry Home

3: It is requested that a property 4: Create a named "resultPage" be added to 2: Basically just create an subclass of Home the class. to provide a getter instance of this Home class, Extends but... for the property Home.page HomeEnhanced Result getResultPage() { return cycle.getPage("Result"); }

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

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

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

Now, open a new browser window to simulate another user. Enter http://localhost:8080/StockQuote/app?service=page&page=Result. You'll see:

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

4: Render Result page (again)

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

3: Moved into the pool Result page Result page object object 6: Taken out of the pool

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

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

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

The take home message is that generally it is dangerous to have instance variables in a page object. Whenever you see them, you probably should implement the PageDetachListener interface to clear them to null or 0. As an alternative, you may let Tapestry do all that for you. Modify Result.page: 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 = ; } public XXX getStockValue() { return stockValue; } public void setStockValue(XXX stockValue) { this.stockValue = stockValue; } } By looking at the .page file Tapestry doesn't know the type of "stockValue". But let's assume it is XXX. Then it will use this subclass for the Result page. In initialize(), the stock value is set to some default value according to the type XXX. For example, if XXX is int, then the default value is 0. If XXX is Object, then the default value is null. Now, you don't need the instance variable in Result.java: Using Forms 43

1: Create a page object for Result Tapestry Result

3: It is requested that a property named "stockValue" be added to the class. 2: Basically just create an instance of this Result class, Extends but... 4: Create a subclass of Result.page Result to provide a getter, setter and an initialize() method for the property ResultEnhanced int stockValue;

How does Tapestry know that it is an int? protected void initialize() { It knows that from this method signature. If stockValue = 0; there weren't such a method, it would just } declare it as an Object. public int getStockValue() { return stockValue; } Result.java public void setStockValue(int v) { public abstract class Result extends BasePage { this.stockValue = v; int stockValue; }

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

This method is kept here so that Home.java can call it. Otherwise, you wouldn't have to use this method. What you have done is called "injecting a property" into the page. The most important effect is that the property is cleaned up automatically when it is returned to the pool, so security is ensured. Now run the application and it should continue to work. Let's check the Home class above. It also has an instance variable. Is it dangerous? It should be OK as the stock id will be set by the TextField component before it is used. But if you prefer being safe than sorry, you may use a property specification in its place: 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 = stockId getStockId() .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: 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: 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 The element there has exactly the same effect as the @InjectPage annotation here. InjectPage is a Java interface is defined in the Home.java org.apache.tapestry.annoations import org.apache.tapestry.annotations.*; package. So you must import the package. public abstract class Home extends BasePage { @InjectPage("Result") abstract public Result getResultPage();

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

public abstract class Home extends BasePage { After creating a new page object, Tapestry will @InjectPage("Result") evaluate this expression to get the value and abstract public Result getResultPage(); store it into the property. In this case you're using the literal prefix. If required, you can use @InitialValue("literal:MSFT") the ognl prefix and specify an OGNL abstract public String getStockId(); expression. public IPage onOk(IRequestCycle cycle) { int stockValue = getStockId().hashCode() % 100; Before Tapestry returns the page object back Result resultPage = getResultPage(); to the pool, it will evaluate the expression and resultPage.setStockValue(stockValue); store the value into the property again so that return resultPage; the next time when it's taken out of the pool, } the value will have already been set. } Using implicit components You've been defining the components in .page files. In fact, if you'd like, you could define them in the HTML files. For example, you could modify Home.html: It says that the type of the Write the binding here directly component is Form

Now, you don't need to define this component in Home.page: 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:
Now it is an anonymous component. If you'd like, you could turn the "stockId" component into an implicit component too:
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: 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:

Actually the HTML 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 on May 3, 2005 Define the "quoteDate" component in Home.page: Using Forms 49

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:

... The path to the dojo.js file. This file is included in a tapestry jar file. What is the path exactly? You don't need to worry about that. Read on. To generate this code, you can use a Shell component: 50 Chapter 2 Using Forms

Note that you're using an implicit component here. The Shell component will generate the and elements.

You must specify the title ...
Bring in the dojo library

Stock Quote

...

Second, the script generated by the DatePicker needs to be collected and put into the element: ...

...
You'll create a Logon Tapestry page to replace login.jsp. In addition, check the action of the form: ... ... 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: ... ... ... 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" %> <bean:message key="index.title"/>

Language Options

  • English
  • Japanese
  • Russian
Integrating with Struts 471


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: ... 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 Call it. But how? action

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 is not a page in Tapestry. It is a page in <bean:message key="index.title"/> Struts which is just a URL relative to the context (/MailReader). So, the full link will be /MailReader/app?service...

Language Options

  • English
  • Japanese
  • Russian

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 Logon.jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> <span key="logon.title"/> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %>

<bean:message key="logon.title"/> : size="16" maxlength="18"/> : : bundle="alternate"/>: redisplay="false"/>
maxlength="18"/> size="16" maxlength="18"/>
size="16" maxlength="18"
value="Submit"/>
Tapestry validation Logon.page is: 474 Chapter 15 Integrating with Struts

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: ... ... 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: 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: ... 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" %> <bean:message key="mainMenu.title"/>

It is using a 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: 476 Chapter 15 Integrating with Struts

... /tags/app /WEB-INF/app.tld /tags/tapestry /WEB-INF/tapestry-3.0.3.tld Check the app.tld file: 1.0 1.1 Application Tag Library http://jakarta.apache.org/taglibs/struts-example-1.0 This tag library contains functionality specific to the Struts Example Application. checkLogon org.apache.struts.webapp.example.CheckLogonTag empty 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] name false true page false true ... 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 LogonAction.java

public abstract class Logon extends CommonPage { public final class LogonAction extends BaseAction { public abstract String getUsername(); static String USERNAME = "username"; public abstract String getPassword(); static String PASSWORD = "password";

@Bean User getUser( public abstract ValidationDelegate getDelegate(); UserDatabase database, String username, @Message("error.database.missing") String password, public abstract String getMissingMsg(); ActionMessages errors) throws ExpiredPasswordException { @Message("error.password.mismatch") User user = null; public abstract String getMismatchMsg(); if (database == null) { errors.add( User getUser(UserDatabase database, ActionMessages.GLOBAL_MESSAGE, String username, String password) new ActionMessage( throws ExpiredPasswordException { "error.database.missing")); User user = null; } else { if (database == null) { user = database.findUser(username); getDelegate().setFormComponent(null); if ((user != null) && !user.getPassword().equals(password)) { getDelegate().record(getMissingMsg(), null); user = null; } else { } user = database.findUser(username); if (user == null) { if ((user != null) && errors.add( !user.getPassword().equals(password)) { ActionMessages.GLOBAL_MESSAGE, user = null; new ActionMessage( } "error.password.mismatch")); if (user == null) { } getDelegate().setFormComponent(null); } getDelegate().record(getMismatchMsg(), null); return user; } } } void SaveUser( return user; HttpServletRequest request, } User user) { @InjectObject("service:tapestry.globals.WebRequest") HttpSession session = request.getSession(); session.setAttribute( public abstract WebRequest getRequest(); Constants.USER_KEY, user); if (log.isDebugEnabled()) { void saveUser(User user) { log.debug("LogonAction: User '" WebSession session = getRequest().getSession(true); + user.getUsername() session.setAttribute(Constants.USER_KEY, user); + "' logged on in session " if (log.isDebugEnabled()) { + session.getId()); log.debug("LogonAction: User '" True means create } + user.getUsername() } + "' logged on in session " the session if it public ActionForward execute( + session.getId()); doesn't exist yet ActionMapping mapping, } ActionForm form, } HttpServletRequest request, public void onSubmit() { HttpServletResponse response) UserDatabase database = getUserDatabase(); throws Exception { String username = getUsername(); UserDatabase database = getUserDatabase(request); String password = getPassword(); String username = (String) User user; PropertyUtils.getSimpleProperty( try { form, USERNAME); user = getUser(database, username, password); String password = (String) } catch (ExpiredPasswordException e) { PropertyUtils.getSimpleProperty( throw new form, PASSWORD); RedirectException("ExpiredPassword.do"); ActionMessages errors = new ActionMessages(); } User user = getUser(database, saveUser(user); username, password, errors); if (getDelegate().getHasErrors()) { SaveUser(request, user); return; if (!errors.isEmpty()) { } this.saveErrors(request, errors); throw new RedirectException("MainMenu.do"); return (mapping.getInputForward()); } } } return (findSuccess(mapping)); } }

struts-config.xml

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 BaseAction.java

public abstract class CommonPage extends BasePage { public class BaseAction extends Action { protected Log log = protected Log log = LogFactory.getLog(Constants.PACKAGE); LogFactory.getLog(Constants.PACKAGE);

@InjectObject("service:tapestry.globals.WebContext") protected UserDatabase getUserDatabase( public abstract WebContext getContext(); HttpServletRequest request) { return (UserDatabase)servlet. getServletContext(). getAttribute(Constants.DATABASE_KEY); protected UserDatabase getUserDatabase() { } return (UserDatabase) protected ActionForward findFailure( getContext().getAttribute(Constants.DATABASE_KEY); ActionMapping mapping) { } return (mapping. } findForward(Constants.FAILURE)); } protected ActionForward findSuccess( ActionMapping mapping) { return (mapping. findForward(Constants.SUCCESS)); } }

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" %>


It contains a link to /Welcome. This action is defined as: 480 Chapter 15 Integrating with Struts

... It can be activated as /Welcome.do. So, modify Footer.html to simulate footer.jsp: Footer.html


Title

footer.jsp <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %>


Define the component in Footer.jwc: 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 MailReader_ja.properties and copy the relevant strings from ApplicationResources_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 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" %> <bean:message key="index.title"/>

Language Options

  • English
  • Japanese
  • Russian

It is done by the Locale action. It is defined in struts-config.xml: ... 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: ChangeLocale.page is: 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 Why not redirect in activateExternalPage()? page. This is what is needed This is because the Tapestry locale is saved for the moment. into a cookie only when the rendering phase is started. If you redirect in the event handling phase, the locale won't be saved. Let welcome.jsp call it: <%@ 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 activateExternalPage(). Note that there is an extra "S" at <bean:message key="index.title"/> the beginning so that Tapestry knows that it is a String. If you'd like to pass a second parameter, do it like:

Language Options

  • English
  • Japanese
  • Russian
  • Pass a long
value 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: ...

:
:
Therefore, you should do the same thing in Logon.html: ...
:
:
As you can't specify a message resource bundle in a 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 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

  • ...... 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 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 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