Skip to content

Tapestry 5 web framework

Lately I’ve been writing a Tapestry 5 based web application. I’ve used it before for a smaller application but this is the first time I’ve used it on a larger project. In a number of ways it is a very powerful framework to write web applications.

The basics of Tapestry is that it is a component-based web framework. Just about everything, including web pages, are components. Components may contain other components. The way it works is very simple and quite elegant, once you get used to it and weaned off the big-XML-file style of configuring a web application.

When you start, you have two main Java packages that are created for you (if you use the maven archetype, otherwise you will create these packages yourself). If your package root is say “net.crazymcphee.webapp”, then your two packages are “net.crazymcphee.webapp.pages” and “net.crazymcphee.webapp.components”. To configure a Tapestry 5 project with maven use this command and answer the prompts:

mvn archetype:generate \
    -DarchetypeCatalog=http://tapestry.formos.com/maven-snapshot-repository

Don’t use the one in the tutorial as it will not work! This is an excellent illustration of the first and most serious problem that Tapestry 5 has: the documentation not only has massive lacunas, it is also sometimes wrong and not updated.

Now, any class that you create in the “pages” package will automatically become a page in your application. But these classes need not be very complex at all. For example:

package net.crazymcphee.webapp.pages;
 
import java.util.Collections;
import java.util.List;
 
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
 
import net.crazymcphee.webapp.model.Person;
import net.crazymcphee.webapp.services.PersonService;
 
public class Persons {
 
    @Inject
    private PersonService personService;
    @Property
    @Persist
    private List persons;
    @Property
    @Persist
    private String searchTerm;
 
    Object onSubmitFromSearch() {
        persons = personService.findPersons(searchTerm);
        return this;
    }
 
    Object onSubmitFromClear() {
        persons = Collections.EMPTY_LIST;
        return this;
    }
 
}

You can see that this class doesn’t extend any infrastructure classes and has quite a simple structure. This is a fully functional page with two actions – one populates a list, the other clears it.

Now, to explain just a little here; PersonService is injected by Tapestry, but you need to configure in your AppModule which implementation you want to use. You can also use it with Spring as your IOC container but the one that comes with Tapestry is perfectly good enough for most applications.

The List, persons, and the searchTerm parameters are marked as @Property (so we don’t need to add getters and setters) and also @Persist so that the variables are preserved from request to request.

The methods “onSubmitFromSearch” and “onSubmitFromClear” are using a convention – “onSubmit” will also work, but assuming (as is true in this case) that we may have multiple forms on the one page, each method will only be fired from the “search” form in one instance, and the “clear” form in the other. These names are not special, it’s just (as you’ll see below) the forms will have these two names. They could be “Bill” and “Ben” in which case the methods would be “onSubmitFromBill” and “onSubmitFromBen”.

You will also note that these methods return the same page instance, which tells Tapestry to re-render the same page, but if you wanted to forward onto another page, you would add a instance variable, mark it with an @InjectPage annotation, and return that instance variable (after initialising it in your “onSubmit” method) instead of just returning “this”.

This page Persons, is available at {application-context-path}/persons. But there is one other part of the puzzle – the actual view. As mentioned, Tapestry uses convention over configuration and in this case, the convention is that the Page markup must be named the same: Persons.tml. Here is the matching tml file for the class above;

<html t:type=“layout” title=“Peoples I Might Know”

t:sidebarTitle=“Current Time”

xmlns:t=“http://tapestry.apache.org/schema/tapestry_5_1_0.xsd”

xmlns:p=“tapestry:parameter”>

<!– Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml –>

<t:form t:id=“search”>

<t:textfield t:id=“searchTerm” validate=“required” size=“20”/>

<t:submit t:id=“searchPeople” value=“search”/>

</t:form>

<t:grid source=“persons”/>

<t:form t:id=“clear”>

<t:submit t:id=“clearPeople” value=“clear”/>

</t:form>

</html>

Yes, it’s that bloody simple. The first form fires the “onSubmitFromSearch” method (with a little bit of validation, done in javascript) and the second method clears the list. In-between, there is this <t:grid source=“persons”/> business, which, if the ‘persons’ variable in the page class is populated, will show a list of its contents!

To test our app, we can use the command-line ‘mvn clean jetty:run’. When Jetty has run up, then we can point our browser at the web app: http://localhost:8080/sample-webapp/Persons, and with any luck we will see the following:

Initial View

Now, by default, Tapestry creates the web app to use the template design as shown above. Of course, you can make it look completely some other way – or even use a totally different template if you want. The page template is just a component that’s included.

So if you enter in a search term and click the search button, you’d expect to see a result like this:

Search Result

As you can see, the list is automatically populated with the details returned from the service method (in this particular instance, this are just a canned response, but normally of course they’d be the result of a database or a web service call of some type).

When you click the clear button, the list is cleared as you’d expect:

Clear button result

Now, here’s a really powerful feature I find in Tapestry. Let’s say our automated acceptance tests assert that when the ‘Clear’ button is pressed, the Search box as well as the persons list is cleared. What Tapestry allows us to to do, is keep Jetty running, edit the files in the IDE, and re-run the tests against the running app without restarting! We edit our method:

    Object onSubmitFromClear() {
        persons = Collections.EMPTY_LIST;
        searchTerm="";
        return this;
    }

In this case I’ve also added a bit of space around the clear button in the template as well:

Picture 4

And now our failing test will pass.

Editing files like this in the running web app only works for Pages and I think Components. If I had to change the service or model classes I’d have to restart, but “mvn jetty:run” isn’t very a heavyweight process.

As I said above, Tapestry’s not perfect: it’s major flaw is the poor documentation. Convention-over-configuration is easy to grok – if you know the convention. If you don’t and the documentation doesn’t tell you, and you can’t find a sample of what you need, it can be very frustrating. There is an excellent user list though.

Its other major flaw may be the stability of the API. Tapestry 5 is different (and incompatible) from Tapestry 4 is different from Tapestry 3. But so far I’ve used it on a couple of projects and I’m really enjoying it. I really hate the oodles of XML boilerplate and massive amounts of configuration found in Spring and I find myself somewhat reluctant to use it unless I really have to. Tapestry solves for me a range of different problems and mostly it presents a very elegant way to create a componentised web application. You might like to give it a try.

Attached is the sample code used above. It took me about 10 minutes to write (far shorter than it took me to write this blog entry!) – sample-webapp.tar bundle

One Comment

  1. Jim Kuo wrote:

    Thanks for writing this tutorial, it’s very clear and informative, tapestry 5 site should link to this blog instead of the outdated tutorial1…

    Thursday, September 3, 2009 at 00:04 | Permalink