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'd like to create AJAX web-based applications easily, then this book is for you. More importantly, it shows you how to do that with joy and feel good about your own work! You don't need to know servlet or JSP while your productivity will be much higher than using servlet or JSP directly. This is possible because we're going to use a library called "Tapestry" that makes complicated stuff simple and elegant. How does it do that? First, it allows the web designer to work on the static contents and design of a page while allowing the developer to work on the dynamic contents of that page without stepping on each other's toes; Second, it allows developers to work with high level concepts such as objects and properties instead of HTTP URLs, query parameters or HTML string values; Third, it comes with powerful components such as calendar, tree and data grid and it allows you to create your own components for reuse in your own project. However, don't take our word for it! This book will quickly walk you through real world use cases to show you how to use Tapestry and leave it up to you to judge. It is best said by Geoff Longman, a Tapestry expert and the creator of a famous Eclipse plugin for Tapestry, "this is material designed to get your feet dirty *right away* and not really intended to debate whether Tapestry is right, wrong, the best, or the worst framework for you, me, or my brother."
How this book can help you learn Tapestry? • It has a tutorial style that walks you through in a step-by-step manner. • It is concise. There is no lengthy, abstract description. • Many diagrams are used to show the flow of processing and high level concepts so that you get a whole picture of what's happening. • Free sample chapters are available on http://www.agileskills2.org. You can judge it yourself. 4 Enjoying Web Development with Tapestry
Unique contents in this book This book covers the following topics not found in other books on Tapestry: • How to work with Tapestry 4.1, including the AJAX features. • How to use FireBug to debug AJAX effects. • How to do test-driven development (TDD) with Tapestry and HtmlUnit. • How to integrate Struts with Tapestry. • How to use PostgreSQL and DBCP connection pooling with Tapestry. • How to apply the four layered architecture in a Tapestry application.
Target audience and prerequisites This book is suitable for those learning how to develop web-based applications and those who are experienced in servlet, JSP, Struts and would like to see if Tapestry can make their jobs easier. In order to understand what's in the book, you need to know Java, HTML and some simple SQL. However, you do NOT need to know servlet, JSP, Tomcat, HtmlUnit or Hibernate. The chapter on Struts integration does assume that you know Struts. If not, you may skip that chapter.
Acknowledgments I'd like to thank: • Howard Lewis Ship for creating Tapestry. • Mike Bowler, the creator of HtmlUnit, for reviewing the chapter on HtmlUnit. • Helena Lei for proofreading this book. • Eugenia Chan Peng U for doing book cover and layout design. Enjoying Web Development with Tapestry 5
Table of Contents Foreword...... 3 How to create AJAX web-based application easily?...... 3 How this book can help you learn Tapestry?...... 3 Unique contents in this book...... 4 Target audience and prerequisites...... 4 Acknowledgments...... 4 Chapter 1 Getting Started with Tapestry...... 11 What's in this chapter?...... 12 Developing a Hello World application with Tapestry...... 12 Installing Eclipse...... 12 Installing Tomcat...... 12 Installing Tapestry...... 14 Creating a Hello Word application...... 15 Generating dynamic content...... 21 Disabling caching in Tapestry...... 24 Making changes to Java code take effect...... 25 Other ways to set the value...... 25 Debugging a Tapestry application...... 26 Summary...... 29 Chapter 2 Using Forms...... 31 What's in this chapter?...... 32 Developing a stock quote application...... 32 Creating the result page...... 37 Displaying the Result page in the listener...... 37 Easier way to get access to another page...... 40 Instance variables may breach security...... 40 Using Java annotations to inject pages and properties...... 44 Using implicit components...... 46 Using a combo box...... 47 Using the DatePicker...... 48 Using the API doc...... 51 Using the component reference...... 52 Summary...... 53 Chapter 3 Validating Input...... 55 What's in this chapter?...... 56 Postage calculator...... 56 Accepting integer input...... 57 What if the input is invalid?...... 60 Using validators...... 66 What if the translator can't translate the string?...... 69 Handling null input...... 69 Setting the display message...... 70 Using a FieldLabel...... 71 Creating your own validator...... 72 Showing all the errors...... 74 Using informal parameters...... 77 Performing validation using Javascript...... 77 6 Enjoying Web Development with Tapestry
Errors that don't belong to any input field...... 78 Validation for a DatePicker component and a TextArea component...... 79 Other validators...... 81 Summary...... 82 Chapter 4 Creating an e-Shop...... 83 What's in this chapter?...... 84 Creating an e-shop...... 84 Showing the product details...... 86 Setting the packages to look for page classes...... 91 Implementing a shopping cart...... 92 Distinguishing which button was clicked...... 93 Adding a product to the shopping cart...... 96 How Tomcat and the browser maintain the session...... 103 Another way to maintain a session...... 107 Unified method to let a page remember its data...... 108 Storing persistent property into the session...... 112 Implementing checkout...... 112 Letting the Confirm page protect itself...... 123 Calling back a page that takes parameters...... 125 Passwords are exposed...... 127 Implementing logout...... 128 Summary...... 129 Chapter 5 Creating Custom Components...... 131 What's in this chapter...... 132 Displaying a copyright notice on all pages...... 132 Should Copyright.html be a complete page?...... 135 Stating that the body will be discarded...... 135 Creating a Box component...... 136 Customizing the Box component using informal parameters...... 139 Customizing the Copyright component using formal parameters...... 140 Making a parameter optional...... 141 Using annotation to declare a parameter...... 141 Looking for the component class in the specified packages...... 142 Creating a component that takes input...... 142 Documenting a component...... 146 Reusing components in another project...... 147 Summary...... 154 Chapter 6 Supporting Other Languages...... 157 What's in this chapter...... 158 A sample application...... 158 Supporting Chinese...... 158 How to internationalize an implicit component...... 163 An easier way to insert a message...... 164 Internationalize the page content...... 164 Letting the user change the locale...... 166 Selecting the current locale in the combo box...... 175 Localizing the full stop...... 176 Displaying a logo...... 178 Localizing the logo...... 184 Putting the images into other places...... 186 Enjoying Web Development with Tapestry 7
Creating a license page...... 187 Observing the output encoding...... 191 Creating a Logo component...... 191 Setting the ALT attribute of the logo...... 193 Packaging the Logo component...... 194 Automating the package process...... 196 How can the browser access the GIF files?...... 198 Summary...... 198 Chapter 7 Using the Table Component...... 201 What's in this chapter?...... 202 Creating a phone book...... 202 List the entries in alternating colors...... 206 Storing the styles in a file...... 209 Sorting the entries...... 212 Customizing how to get the cell value...... 217 Customizing the column titles...... 218 Making the styles work again...... 220 Making the first name a link...... 222 Listing more entries...... 225 Tuning the performance of the Table component...... 226 Session is used...... 234 Caching the entries...... 234 Adding a delete button...... 236 Sort by Delete?...... 244 Moving the page links to the bottom...... 244 Summary...... 248 Chapter 8 Handling File Downloads and Uploads...... 251 What's in this chapter?...... 252 Downloading a photo...... 252 Using a service...... 256 Generating the link to call the service...... 263 Displaying a photo...... 266 Using friendly URL...... 267 Downloading a photo using a form...... 273 Telling the size of the download...... 274 Uploading a photo...... 274 Summary...... 278 Chapter 9 Providing a Common Layout...... 281 What's in this chapter?...... 282 Providing a common layout...... 282 Setting the page title...... 287 Disabling the link for the current page...... 288 Using a header...... 289 Summary...... 292 Chapter 10 Using Javascript...... 293 What's in this chapter?...... 294 Are you sure to delete it?...... 294 Reusing the script...... 296 Generating a unique function name...... 298 Encapsulating the use of scripts in a component...... 302 8 Enjoying Web Development with Tapestry
Summary...... 304 Chapter 11 Building Interactive Forms with AJAX...... 307 What's in this chapter?...... 308 A sample AJAX application...... 308 Creating the Home page...... 310 Loading a single customer...... 311 Using a DataSet to store the Customer objects...... 316 Listing all the customers...... 321 Implementing the Edit function...... 324 Skipping validation for form cancellation...... 331 Refreshing the current row only...... 334 Refreshing the city list when the country is changed...... 337 Preventing multiple forms...... 340 Implementing the Delete function...... 341 Implementing the Add and the Commit function...... 342 Using FireBug...... 344 Summary...... 348 Chapter 12 Test Driven Development with HtmlUnit...... 349 What's in this chapter?...... 350 Developing a calculator using test driven development...... 350 Setting up HtmlUnit...... 351 Setting up the web application context...... 352 Implementing the add operation...... 354 Providing a list of operations...... 361 Using the setUp() method...... 362 Implementing minus...... 364 Implementing the History link...... 365 Fixing the problems revealed by manual inspection...... 371 Running all the tests...... 373 Implementing validation...... 373 Implementing the Help link...... 375 Refactoring...... 379 Summary...... 381 Chapter 13 Database and Concurrency Issues...... 383 What's in this chapter?...... 384 Developing a banking application...... 384 Setting up PostgreSQL...... 384 Hard coding some bank accounts...... 392 Transferring some money...... 393 Using a transaction...... 396 Connection pooling...... 398 Concurrency issues...... 402 Long transaction...... 416 Dividing the application into layers...... 426 Summary...... 434 Chapter 14 Using Hibernate...... 435 What's in this chapter?...... 436 Setting up Hibernate...... 436 Adding an id not exposed to the user...... 439 Specifying the mapping...... 440 Enjoying Web Development with Tapestry 9
Accessing objects with Hibernate...... 440 Updating the database schema...... 443 Hard coding some customers programmatically...... 443 Do NOT access objects loaded after its session is closed...... 444 Editing a Customer object...... 449 Adding a Customer object...... 451 Deleting a Customer object...... 453 Handling concurrency issues...... 454 Separating UI code and database code...... 456 Summary...... 460 Chapter 15 Integrating with Struts...... 461 What's in this chapter?...... 462 Integrating Tapestry with Struts...... 462 Running a sample Struts application...... 462 Rewriting the Logon page in Tapestry...... 468 Invoking a Tapestry page from JSP and invoking a Struts action from Tapestry...... 470 Implementing rendering part of the Logon page...... 472 Implementing the rewinding part of the Logon page...... 477 Rewriting a JSP include file as a Tapestry component...... 479 Using localized messages...... 481 Supporting an alternate message resource bundle...... 485 Summary...... 487 References...... 489 Alphabetical Index...... 490
11
Chapter 1
Chapter 1 Getting Started with Tapestry 12 Chapter 1 Getting Started with Tapestry
What's in this chapter? In this chapter you'll learn to how to setup a development environment and develop a Hello World application with Tapestry.
Developing a Hello World application with Tapestry Suppose that you'd like to develop an application like this:
Installing Eclipse First, you need to make sure you have Eclipse installed. If not, go to http://www.eclipse.org to download the Eclipse platform (e.g., eclipse-platform-3.1-win32.zip) and the Eclipse Java Development Tool (eclipse-JDT-3.1.zip). Unzip both into c:\eclipse. Then, create a shortcut to run "c:\eclipse\eclipse -data c:\workspace". This way, it will store your projects under the c:\workspace folder. To see if it's working, run it and then you should be able to switch to the Java perspective:
Installing Tomcat Next, you need to install Tomcat. Go to http://jakarta.apache.org to download a binary package of Tomcat. Download the zip version instead of the Windows exe version. Suppose that it is jakarta-tomcat-5.5.7.zip. Unzip it into a folder say c:\tomcat. If you're going to use Tomcat 5.5 with JDK 1.4 or 1.3, you also need to download the compat package and unzip it into c:\tomcat. Before you can run it, make sure the environment variable JAVA_HOME is defined to point to your JDK folder (e.g., C:\Program Files\Java\jdk1.5.0_02): Getting Started with Tapestry 13
If you don't have it, define it now. Now, run open a command line, change to c:\tomcat\bin and then run startup.bat. If it is working, you should see:
Open a browser and go to http://localhost:8080 and you should see: 14 Chapter 1 Getting Started with Tapestry
Let's shut it down by changing to c:\tomcat\bin and running shutdown.bat.
Installing Tapestry Next, go to http://tapestry.apache.org to download a binary package of Tapestry (e.g., tapestry-project-4.1.1-bin.zip) and unzip it into a folder say c:\tapestry. It contains a quite some jar files in different sub-folders (each folder is a "module"):
It's quite difficult to use these jar files as they're in different sub-folders. To solve the problem, you'll copy them available in one folder. Create a folder c:\tapestry\jars, then choose "Search" on the Start Menu to search for files named *.jar inside c:\tapestry. The result should be like: Getting Started with Tapestry 15
Copy all of the files and paste them into c:\tapestry\jars. That's it. You can't run it yet because Tapestry is a library, not an application.
Creating a Hello Word application Now, create a new Java project. Name it "HelloWorld" and make sure it uses a separate output folder:
Set the output folder as shown below: 16 Chapter 1 Getting Started with Tapestry
Finally, you should see the project structure:
The bin folder is useless so you can delete it. Then right click the project and choose "Properties", choose "Java Build Path" on the left hand side, choose the "Libraries" tab:
Click "Add Library" and choose "User Library": Getting Started with Tapestry 17
If you see a "Tapestry Framework" library as shown above, Do NOT choose it! It is Tapestry 3.0 coming with Spindle. Click "Next":
Click "User Libraries" to define your own Tapestry library:
Click "New" to define a new one and enter "Tapestry 4" as the name of the library:
Click "Add JARs", browse to c:\tapestry\jars and add all the jar files there: 18 Chapter 1 Getting Started with Tapestry
Then close all the dialog boxes. Next, create a new file Home.html in context\WEB-INF in the project. It will act as the home page of your application. Next, use DreamWeaver, FrontPage or your favorite web page editor to modify it. But where is it located? It is in context/WEB-INF in your project and the whole project is in c:\workspace:
So, its full path is c:\workspace\HelloWorld\context\WEB-INF\Home.html. Knowing its full path, you can modify it to look like:
If you'd like, you can edit the HTML code directly in Eclipse:
Next, create a new file Home.page in the same folder as Home.html with the following content: 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: HelloWorldHelloWorldorg.apache.tapestry.ApplicationServlet1HelloWorld/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"
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: StockQuoteStockQuoteorg.apache.tapestry.ApplicationServlet1StockQuote/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
. That is, the whole process is like: Page object 1: Render yourself
2: Output the start tag for an HTML form element stockQuoteForm
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: