In this tutorial, we'll get introduced to one of the real workhorses of Tapestry, the DirectLink component. It is one of the most common ways of triggering server-side behavior. Along the way, we'll start seeing some other common aspects of developing web applications using Tapestry.
This application simply counts the number of times we click a link.
This requires a little more than we can accomplish with just an HTML template; we'll need to supplement with a Java class. This java class will contain a property that stores the count, and contain logic used to increment the count.
We'll start again with the Home page's template:
<html> <head> <title>Tutorial: DirectLink</title> </head> <body> <h1>DirectLink Tutorial</h1> <p> The current value is: <span style="font-size:xx-large"><span jwcid="@Insert" value="ognl:counter">37</span></span> </p> <p> <a href="#" jwcid="@DirectLink" listener="listener:doClick">increment counter</a> </p> <p> <a href="#" jwcid="@PageLink" page="Home">refresh</a> </p> </body> </html>
Much of this should look familiar. We're again using the Insert component, and we're using OGNL again. Instead of creating a new instance, we're using OGNL in a simpler way; it will read the counter property and provide that to the Insert component in its value parameter.
That's fine, but where does this counter property live? In the Home page's Java class. We'll see how to create that class shortly.
Just displaying the current value isn't enough, we need a way to change that value. That's where the DirectLink component comes in; it will invoke a method of our Java class for us. This connection between component and method is supplied in the listener parameter. The "listener:" prefix activates the logic that lets Tapestry invoke the method with the matching name. We provide a method, doClick(), in the page's Java class, and the DirectLink component will invoke this method for us, in response to the user clicking the rendered link in their web browser.
So, we have half the puzzle: the HTML template. But we need the Java class that will contain the properties to be stored, and the methods to be invoked.
First, we need a Java package to store the class. For this tutorial, we'll use tutorials.directlink.pages. That means we'll create the Home.java source file as src/java/tutorials/directlink/pages/Home.java:
package tutorials.directlink.pages; import org.apache.tapestry.annotations.Persist; import org.apache.tapestry.html.BasePage; public abstract class Home extends BasePage { @Persist public abstract int getCounter(); public abstract void setCounter(int counter); public void doClick() { int counter = getCounter(); counter++; setCounter(counter); } }
The Java classes you write will extend from the Tapestry BasePage class 1 . To this base class, we are adding a property, counter, and a listener method, doClick() 2 .
Abstract? What's up with that? That's a pretty typical first reaction to seeing a Tapestry page class; why is it abstract, why is it not an ordinary JavaBean?
The answer involves a bit of a digression. Tapestry pages exist on the server, and are somewhat expensive to create ... expensive enough that you don't want to constantly create them and discard them. In this way, they are much like database connections ... you want to pool pages for reuse from one request to the next.
Because the pages are pooled and shared, in fact shared between different users , it's very important that they page objects be cleansed of any user-specific or request-specific data before they go back into the pool. You can do this in your own code (there are additional interfaces to implement and additional code to write), but it is easier to let Tapestry do that work for you.
By declaring an accessor method as abstract, you are implicitly directing Tapestry to "fill in" the details; at runtime , it will create a subclass of the Java class you provide, extending your implementation with all the grinding details. As well see in later tutorials, this in fact goes far beyond just properties; all sorts of useful features can be tied to different flavors of abstract methods (often coupled with different annotations ).
There's something special about this counter property. It has to remember its value between requests. The @Persist annotation, attached to the getCounter() accessor method, directs Tapestry to make this a persistent page property . Despite the name, this has nothing to do with database persistence, it's about storing the value for the property in the HttpSession between requests, and restoring it the next time that the same user, in the same session, accesses the page.
This is another pivotal feature in Tapestry; individual page properties (or properties of components used within the page) can store their value in the HttpSession automatically. We can seperate out the persistent state of the page from any instance of the page. This minimizes the amount of information that must be stored in the HttpSession; rather than entire page objects (with all those templates and nested components), we store just the tiny handful of properties that need to "stick around" until the next request.
This approach to session management, combined with the pooling of page instances, is critical to achieving another of Tapestry fundamental principals: Efficiency . Tapestry applications will scale because of how they manage server-side state. The cost of this is that the classes and methods are abstract, with the implementations of many methods only provided dynamically, by Tapestry, at runtime.
Back from our digression. We now have the counter property, and we understand how it is stored in the HttpSession between requests. That makes the implementation of the doClick() listener method straight forward: get the current value for the proeprty, increment it, and store it back into the property.
Again, we're demonstrating part of our promise about Tapestry: we're talking about objects and methods and properties. There's a URL in there, generated by Tapestry, for the DirectLink. There's attributes stored in the HttpSession. We don't see those or care about them.
We've provided the Home page's template and Java class, but we haven't quite connected the dots enough for our application to run. If we tried to run the application (by opening a web browser to http://localhost:8080/directlink/app ), we'd get the Tapestry exception page:
That's quite a lot of information. The root cause of the exception is the fact that
Tapestry couldn't find the Home class we created, so it instead used BasePage as-is.
BasePage doesn't have a counter property, so the OGNL expression
counter
couldn't be evaluated (you can see that in the deepest exception). You can see in
the target property of the ognl.NoSuchPropertyException, the value
$BasePage_0@cec78d[Home]
is the toString() of a page class; the first part is the name of the class
(remember, this is a subclass generated at runtime by Tapestry), the value in
brackets is the name of the page.
This exception bubbled up to the top-level of Tapestry, getting wrapped inside other exceptions along the way. The framework couldn't continue with the Home page, so it generated this exception report instead.
As you can see, the exception report is quite detailed; it shows the entire stack of exceptions, including their properties. It identifies the file and line at the root of the problem, and even displays an excerpt from that file. Further down on the page are exhaustive details about all the Servlet API objects ... in short you are given all the information you need to understand what was going on in your application at the time of the failure, without having to restart using a debugger. This is another Tapestry priciple in action: Feedback . When things go wrong, Tapestry should help you fix your problem, rather than get in the way.
So, the root of our problem is that Tapestry can't find our Home page, so we need to tell it where to look. This involves providing Tapestry with a little bit of configuration about our application.
We'll create an application specification for our application, and store the configuration data there. An application specification is an XML file that provides extra information about the application to Tapestry. It is optional; we didn't have one in the previous example.
The name of the servlet ("app", in this example) is appended with the extension ".application" to form the name of the specification. The specification itself is stored in the WEB-INF folder of the web application. In our project, it is stored as src/context/WEB-INF/app.application:
<?xml version="1.0"?> <!DOCTYPE application PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN" "http://tapestry.apache.org/dtd/Tapestry_4_0.dtd"> <application> <meta key="org.apache.tapestry.page-class-packages" value="tutorials.directlink.pages"/> </application>
Application specifications are validated XML files, with a real DTD (at the specified location). The <meta> element is used to specify meta data ... configuration data that doesn't fit in elsewhere. Here we're using it to inform Tapestry about which Java packages to search for pages. This value can even be a comma-seperated list of packages, if there is more than one package to search.
With this file in place, Tapestry has all it needs to run our application: the Home page's HTML template, its Java class, at the connection between the two. Now, let's look at improving our example a bit.
The URLs generated by the DirectLink component are more complex than those generated by the PageLink component. Let's look at one:
http://localhost:8080/directlink/app?component=%24DirectLink&page=Home&service=direct&session=T
The first query parameter, component, identifies the component within the page. That %24 is "URL-ese" for a dollar sign. In Tapestry, every component ends up with a unique id within its page. If you don't provide one, Tapestry creates one from the component type, prefixed with a dollar sign. Here, our annoynmous DirectLink component was given the id $DirectLink. If you had many different DirectLinks on a page, you'd start seeing component ids such as $DirectLink_0, $DirectLink_1, etc.
You can give a component a shorter and more mneumonic id by putting the desired id before the "@" sign:
<a href="#" jwcid="inc@DirectLink" listener="listener:doClick">increment counter</a>
After making that change to the template, the URL for the DirectLink component is just a bit easier to read:
http://localhost:8080/directlink/app?component=inc&page=Home&service=direct&session=T
The other changes from the previous examples are the service query parameter and the session query parameter. The service query parameter indicates that the processing of the request is different than for a link created by the PageLink component. Here we need to get a page, find a component in the page and invoke a listener method before we can render the response. With PageLink, we just get the page and render it.
Lastly, the session query parameter indicates whether there was an HttpSession at the time the link was rendered. Tapestry uses this to detect when the HttpSession expired ... perhaps because the user walked away from the computer for a while before clicking the link. If the application was stateless (no HttpSession) when this link was generated, then the session parameter simply wouldn't appear in the URL.
One thing to take note of is that the method name is not part of the URL, just the id of the component. This is very desirable ... why expose more of the construction of your application than you have to? As importantly, this helps to prevent malicious users from subverting your application; there simply isn't a way to get an arbitrary listener method to be invoked, only one that you, as the developer, wired to a specific component.
These are what Tapestry pros call "ugly URLs". The ugly part is the use of query parameters, rather than paths, to express the information in the URL. Ugly URLs can cause some problems; since the entire application is routed through the /app path, it's hard to apply J2EE declarative security. Likewise, the use of query parameters means that most search engines will not spider the site. The solution is to use "friendly URLs" 3 , which is covered in a later tutorial.
This application is good, but we should have a way to reset the counter back to zero. We're going to add a link to the page to do just that. The end result will look like:
To accomplish this we need to add another link to the Home page's HTML template, and connect that to logic expressed as a new method on the Home page class. First the template:
<p> <a href="#" jwcid="clear@DirectLink" listener="listener:doClear">clear counter</a> </p>
This is just another DirectLink component, on the same page, but with a different component id, and a different configuration. Here, we've called the component "clear", and connected it to the doClear() listener method.
That method is also quite simple:
public void doClear() { setCounter(0); }
And that's all it takes. We've added a new operation to our page, clearing the counter, in four lines of Java code (three if you format your code the way Sun likes you to), and a couple of lines of HTML. No outside configuration beyond that. This conforms to another Tapestry principle: Consistency . Adding more operations is not different from adding the first operation. Add as many as you like, Tapestry will take care of it.
By contrast, using traditional servlets, we would have had to:
<p> <a href="#" jwcid="by1@DirectLink" listener="listener:doClick" parameters="ognl:1">increment counter by 1</a> </p> <p> <a href="#" jwcid="by5@DirectLink" listener="listener:doClick" parameters="ognl:5">increment counter by 5</a> </p> <p> <a href="#" jwcid="by10@DirectLink" listener="listener:doClick" parameters="ognl:10">increment counter by 10</a> </p>
http://localhost:8080/directlink/app?component=by10&page=Home&service=direct&session=T&sp=10
The sp query parameter holds the value. "sp" is short for "service parameter", and is a hold over from Tapestry 3.0. In Tapestry 4.0, these are called "listener parameters", because they are only meaningful to the listener method. Also, we're only showing a single parameter, but the same mechanism supports multiple parameters.
That's how information gets encoded into the URL, but how does the listener method find out about it? By adding a parameter to the doClick() listener method:
public void doClick(int increment) { int counter = getCounter(); counter += increment; setCounter(counter); }
Tapestry maps the values in the sp query parameter to the parameters of the listener method. Also, note that type of the value has been maintained. it started as a number, and is still a number. Listener parameters can be of virtually any type, and will keep their type through being encoded into the URL and decoded in the subsequent request. You can even pass arbitrary objects ... as long as they implement java.io.Serializable (but you will start seeing some very long URLs if you do).
Again, we're seeing consistency. We wanted to pass information in the URL, and were able to use the same mechanisms; the DirectLink component, the listener method ... we just added a little sensible extra to get the needed information from point A (the page as it renders) to point B (the listener method when the link is clicked).
DirectLink may be a real workhorse, but the heart of most web applications are the subject of our next tutorial: Tapestry Forms .
1 This is my (Howard Lewis Ship's) least favorite thing in Tapestry 4.0; it is something that should be erradicated from Tapestry (you should not have to extend a base class at all), but that will cause some significant backwards compatibility issues.
2 Listener methods don't have to be named in any special way, they just have to be public methods. This naming convention, do Something , is a good one, but is anything but mandatory.
3 For some reason, "ugly" is the opposite of "friendly".
4 Say that a few times fast.