Are there too many web frameworks out there? Well for those in the know the answer will be a resounding yes. Which one to pick up has become really a painful decision! Once you take one path you cannot just switch the framework mid-way. There is always the good old Struts framework. But that seems to be “oh not so fashionable nowadays”. Ah that ‘Ruby on Rails’ … and then you can sift through the web to find its java inspired half-brother. Or should we Seam with JBoss Seam? Though in all fairness JBoss Seam cannot be called just a web framework. It is a complete framework for front-end and back-end development.
Then of course there is the macho-man approach. Roll up your sleeves and write your own web framework. Being an independent consultant I am not too interested in creating my own web framework and leaving the client with an unsupported framework when I leave. So I will leave that option out.
On a brand new project what do you use? In my quest to find that answer through experimentation I decided to give Tapestry a try. I liked what I saw initially; though I got very tired of the .page files. It was possible, in most cases, to reduce them to a bare minimum. And if you are lucky to use JDK 1.5 then annotations come to the rescue. Right at the onset let me tell you one thing; there is a sharp learning curve with Tapestry. But once you get the feel of the framework things become easier and actually fun.
I am going to go through a simple example in this article on how to get up and running with Tapestry. Here are the use cases we will implement:
1. Display Home Page
2. Display current list of products in the Catalog.
3. Add new Product (go back to 2 after successful add).
Lets start with the general project setup. I am using Eclipse 3.2 with JDK 5. My project structure is:
Catalog |-src |-catalog.pages (page classes here) |-catalog.service (backend mock service here) |-META-INF |-WEB-INF |-lib |-web.xml |-*.page files |-*.html files |-catalog.application |
I am using Jetty as my web container. Download Jetty (http://www.mortbay.org) and also install Jetty Launcher (http://jettylauncher.sourceforge.net). Jetty Launcher is an eclipse plug-in that allows you to run (and deploy) your application with Jetty. I leave it up to the reader to do this required setup before proceeding.
Here is the web.xml so you know what it is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<web-app xmlns=”http://java.sun.com/xml/ns/j2ee” xmlns:xsi=”http://www.w3.org/TR/xmlschema-1/” xsi:schemaLocation=”http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd” version=”2.4″> <display-name>Catalog</display-name> <servlet> <servlet-name>catalog</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>catalog</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> </web-app> |
Now lets get to Tapestry…finally! First forget about all of the other web frameworks and how they do stuff. Forget JSTL, JSF, Struts and all.
Now lets start thinking of our application structure based on our requirements. We need to display a home page with some welcome stuff. Ok lets then create a Home.html page with following contents.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<html> <head> <title>Catalog Mania</title> </head> <body> Welcome to <span jwcid=“@Insert” value=“ognl:message”>(some message here)</span> <br /> <br /> <a href=“#” jwcid=“@PageLink” page=“Catalog”>Enter Catalog 11. Mania</a> </body> </html> |
What’s this jwcid thing on line 6? Tapestry calls the above html as a template. It contains both your static and dynamic content. You use special Tapestry decorations, on standard HTML tags, for the dynamic behavior. JWCID stands for Java Web Component ID. Those items in your template that are dynamic are decorated with jwcid notations like in the above example. Here we have made a component out of the span tag by specifying the component type @Insert. Tapestry has many of these component types built-in like @TextField, @TextArea, @For (for loop), etc.
So anything dynamic should be thought of as a component (like the span tag above). Next give it the appropriate component type (@Insert). It is very important you understand how the component paradigm works here. So let me try to summarize it again. The component you attach to the standard HTML tag takes over the responsibility of evaluating what its contents should be at runtime. It is that content which is sent out to the browser.
I need to step a little ahead before explaining the ‘ognl’ stuff. Thus far we have a Home.html. If you open a browser and point to it you will see that it displays correctly. And this is the other power of Tapestry. Pages are pure HTML so the web designer and the java developer can both view the pages in their own working domains. The web designer in his designer tool and the java developer via his servlet container.
Now we need something that will process on the server side events and requests from this Home.html page. Lets write a Home.java.
1 2 3 4 5 6 7 8 |
package catalog.pages; import org.apache.tapestry.html.BasePage; public class Home extends BasePage { public String getMessage() { return “Catalog Mania”; } } |
The class extends the Tapestry class BasePage and provides one method getMessage. Now lets jump back to line 6 in the Home.html. The string “value=ognl:message” will at runtime be evaluated to the getMessage class in Home.java. OGNL stands for ‘Object Graph Navigation Language’ and is an open source expression language (like EL in JSTL). Please google-it to get more info.
So to summarize, Home.html is attached to Home.java. Home.html uses ognl to express the desire to call getMessage for the above span-based insert component.
Lastly how does Tapestry know Home.html is connected to Home.java. Nah there is no special default naming convention here. This is done in a Home.page file. I do not like the .page concept one bit. With a large application it will be a pain to maintain so many files but to be fair it serves a purpose and is not a showstopper.
1 2 3 4 5 |
<?xml version=”1.0″?> <!DOCTYPE page-specification PUBLIC “-//Apache Software Foundation//Tapestry Specification 4.0//EN” “http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd”> <page-specification class=“catalog.pages.Home”> </page-specification> |
If you have followed the instructions carefully (including the project structure in eclipse) you should be able to deploy and run this application. Using Jetty that would be.
The app should be available at http://localhost:9090/catalog/app. Note that Tapestry by default will resolve to Home.html if no page is requested. You could also request the same page via http://localhost:9090/catalog/app?service=page&page=Home. But you are better off by not hardcoding such links.
If you look at line 10 of the Home.html we use a built-in Tapestry component @PageLink to link to another Tapestry page, in this case ‘Catalog’. We have not coded that yet. Tapestry will generate the correct links and also do session encoding when necessary.
So we are now passed some basic Tapestry stuff and have displayed the home page as per our requirements. Now the next requirement is to display the list of products in the catalog. We already put a link on line 10 of Home.html to invoke the Catalog page.
So now here is the Catalog.html and Catalog.java.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN”> <html> <head> <meta http-equiv=“Content-Type” content=“text/html; charset=ISO-8859-1″> <title>Insert title here</title> </head> <body> It is: <strong><span jwcid=“@Insert” value=“ognl:newjava.util.Date()”>June 26 2005</span></strong> <br> <a href=“#” jwcid=“@PageLink” page=“Catalog”>refresh</a> <br> <hr /> Current Product Catalog <br /> <hr /> <table border=“1″ BGCOLOR=“#FFCC00″> <tr> <th>Name</th> <th>Desc</th> <th>Product release date</th> </tr> <tr jwcid=“@For” source=“ognl:products” value=“ognl:product” element=“tr”> <td><span jwcid=“@Insert” value=“ognl:product.name”>name here</span></td> <td><span jwcid=“@Insert” value=“ognl:product.description”>desc</span></td> <td><span jwcid=“@Insert” value=“ognl:product.releaseDate”>1/1/1111</span></td> </tr> <tr jwcid=“$remove$”> <td>Books</td> <td>book description</td> <td>1/1/1111</td> </tr> <tr jwcid=“$remove$”> <td>Toys</td> <td>toy description</td> <td>1/1/2222</td> </tr> </table> <hr /> <p><a href=“#” jwcid=“@PageLink” page=“AddProduct”>Add New Product</a></p> </body> </html> |
Two things worth mentioning in Catalog.html. First the use of ‘jwcid=“$remove$”’. Like I mentioned previously Tapestry pages can be viewed in a regular browser without a servlet container. In this case obviously none of the Tapestry components are evaluated at runtime but the page being standard HTML will be displayed. In the case of the table above, it will be displayed with two rows (Books and Toys). The ‘remove’ jwcid tells Tapestry to ignore those rows at runtime. Thus the page works fine for the web designer and the java developer. This is not possible with a JSP+JSTL approach.
The other thing worth mentioning is
1 2 |
<tr jwcid="@For" source="ognl:products" value="ognl:product" element="tr"> |
We use the loop component, @for, to display all of the products returned from Catalog java class. This connection to the java class is denoted by source=“ognl:products”. The value parameter gives a name to a temporary variable that will hold the current product, as the loop is evaluated. Thus we are able to do the following:
1 2 |
<span jwcid="@Insert" value="ognl:product.description">desc</span> |
‘product.description’ will resolve to Catalog.getProduct().getDescription().
Here is Catalog.java.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package catalog.pages; import org.apache.tapestry.html.BasePage; import catalog.service.Product; import catalog.service.ProductService; import catalog.service.ProductServiceImpl; public abstract class Catalog extends BasePage { public abstract Product getProduct(); public abstract void setProduct(Product p); // hard coded the backend service for now // hmmmm ?? need to see if we can inject this from Spring ?? ProductService service = new ProductServiceImpl(); public Product[] getProducts() { return service.getProducts(); } } |
Line 1: Class is now abstract. Tapestry pools the page classes for reuse. This being the case we have to avoid putting instance variables in the class. That would require us to do cleanup every time the page class is reused. Rather than us doing this we can avoid instance variables and provide abstract getters/setters for interested properties. Tapestry will now take care cleaning up the instance before handing it out for use in a fresh request invocation.
Line 2: We need it so our for loop will work correctly. The value=’ognl:product” uses this instance variable on the page class to store the contents each time it goes through the loop. Why I have no idea? Shows I have still things to learn.
Line 3: Our mock backend product service.
Line 4: source=“ognl:products” connects to getProducts on the class.
That’s it. Remember the Catalog.page. Once you have that you can navigate to the following two pages successfully.
Clicking on the link takes you to.
Thus far you may have realized that Tapestry is indeed very different from other frameworks. But it does take some learning effort. But its well worth it.
Our final requirement is to add a new product and redisplay the catalog page (the new product should show up).
Here is AddProduct.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<html jwcid=“@Shell” title=“Add Product ”> <body jwcid=“@Body”> <h1>Add New Product</h1> <form jwcid=“form@Form” success=“listener:doAddProduct”> <table> <tr> <td> <label jwcid=“@FieldLabel” field=“component:name”>Name</label> : <input jwcid=“name@TextField” value=“ognl:product.name” validators=“validators:required” displayName=“User Name ” size=“30″ /> </td> </tr> <tr> <td> Description: <textarea jwcid=“description@TextArea” value=“ognl:product.description” rows=“5″ cols=“30″ /> </td> </tr> <tr> <td> Release Date: <input jwcid=“releaseDate@DatePicker” value=“ognl:product.releaseDate” /> </td> </tr> </table> <input type=“submit” value=“Add Project ” /> </form> </body> </html> |
Some of the new components we used here:
- @Shell – generates the html, head and title tags. Helps to resolve style sheet names at runtime.
- @Body – this generates the HTML body and any javascript that goes with your tapestry page.
- @FieldLabel – used to display a field label that is attached to a TextField in this example. In our example above if validation for required field fails the two components (FieldLabel and TextField) know to display the right UI behavior.
- @TextArea –HTML text area component
- @DatePicker – a javascript calendar object.
Refer to the online documentation at http://jakarta.apache.org/tapestry/tapestry/ComponentReference/Shell.html for more details on shell and also all of the other built-in components.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package catalog.pages; import java.util.Date; import org.apache.tapestry.IPage; import org.apache.tapestry.annotations.InjectPage; import org.apache.tapestry.event.PageBeginRenderListener; import org.apache.tapestry.event.PageEvent; import org.apache.tapestry.html.BasePage; import catalog.service.Product; import catalog.service.ProductService; import catalog.service.ProductServiceImpl; public abstract class AddProduct extends BasePage implements PageBeginRenderListener { ProductService service = new ProductServiceImpl(); @InjectPage(“Catalog”) public abstract Catalog getCatalogPage(); public abstract Product getProduct(); public abstract void setProduct(Product p); // from PageBeginRenderListener public void pageBeginRender(PageEvent event) { Product project = new Product(); project.setReleaseDate(new Date()); setProduct(project); } public IPage doAddProduct() { service.addProduct(getProduct()); return getCatalogPage(); } } |
The method pageBeginRender is from the interface PageBeginRenderListener. Tapestry invokes this method, as the name suggest, before rendering the page. Here we can do apply some default behaviour. For example when the AddProduct.html is displayed we want to provide a default values for the release date field. This is another good example of how Tapestry forces us think of web development from a Object Oriented point of view using these page classes.
Another very important part of the code is:
@InjectPage(“Catalog”)
public abstract Catalog getCatalogPage();
Remember a page in Tapestry is represented by three files; the .HTML file with the display template, the .java file with the processing logic and the .page file being the glue between the .HTML and .java code. So whenever I say go to another page I meant this logical page represented by the three things mentioned here.
After the doAddProduct method is finished doing its business we would like to return to the Catalog page and display the list of products once again. This is done by injecting the Catalog page into the AddProduct action.
Note: All of what we have talked thus far can be done using JDK 1.4. But wherever we use annotations we would have to enter XML into the .page file instead. |
Once again do not forget the AddProduct.page. Compile and redeploy and you should be able to get to the add product page which when done takes you back to the catalog list page. You should see the new product you just added in the list.
Final Notes:
Did I mention my dislike for the .page files. Maybe it’s just me, but I just think it’s redundant. I reduced my Home.page file to
<page-specification>
</page-specification>
Note I removed the class name attribute. I made sure WEB-INF\catalog.application had the following:
<application>
<meta key=“org.apache.tapestry.page-class-packages” value=“catalog.pages”/>
</application>
This tells Tapestry where to look for the page classes. Having done this I thought I could get rid of my empty Home.page file above. No luck. As soon as I did that tapestry blew up with an exception.
Using Tapestry involves a steep learning curve and a shift in mindset on how you develop web applications. I personally feel like I have only scratched the surface thus far. In the weeks to come I hope to have a follow-up article on Tapestry using some more of its features and built-in components. And maybe we can even write our own component. Yes that is entirely possible.