BT

Tapestry for Nonbelievers

Posted by Renat Zubairov & Igor Drobiazko on May 10, 2008 |

Introduction

Apache Tapestry is a component-oriented framework for creating web applications in Java. Tapestry applications are build from pages which are constructed from components. The framework takes care about input validation, localization/internationalization, state/persitency management, URL construction/parameter mapping, etc.

Why should one consider to use Tapestry? Some of the reasons are:

  • It's end-user friendly. Tapestry is designed with the security and scalability requirements in mind. Ajax, input validation, internationalization and exception reporting are built in.
  • It's developer friendly. Tapestry boosts the developer's productivity with unique class-reloading feature. With Tapestry you change the source code and see the results immediately. No redeploy, no restart are required! Exception reporting is very detailed and even contains suggestions.
  • It's web-designers friendly. Tapesry pages are valid (X)HTML! You can just open them with your favorite browser!
  • It encapsulates best practices: RESTful URLs, degradable JavaScript, XML-less configuration
  • It integrates with Hibernate, Spring, Seam, Acegi, etc.

In this article we are going to introduce you the version 5 of the framework. We will develop a simple Create/Read/Update/Delete application using Tapestry 5 and show a few of the productivity benefits provided by Tapestry. We will describe different aspects of Tapestry applications, such as page navigation, dependency and resource injection, user input validation and application state management. You will also see how to use the Ajax functionality built in to Tapestry and how to create your own Ajax-capable components.

Our goal is to give you some insight into how Tapestry allows you to create better, more usable, more secure and more scalable applications with a minimum of developer effort.

Prerequisites

For this tutorial you would need to have following software installed:

  • Java SE Development Kit (JDK) version 5.0 or newer. You can download it from http://java.sun.com/javase/downloads/
  • Servlet Container, for example Apache Tomcat 5.5 or newer version.
  • Optionally you can download and install Apache Maven 2.0.8. In this case you wouldn't need a separate servlet container and you could use it to build and run your Tapestry 5 application (more information see in Appendix).
  • We would also recommend to use a modern Integrated Development Environment (IDE) for example Eclipse or NetBeans. You can use them to edit Java and HTML files for your application.

Your first Tapestry 5 application

There are many different ways to start developing with Tapestry, one of them is to download the provided Web archive (WAR) file file from here and import it into the IDE of your choice. In case you are using Eclipse together with Web Tools you need to do following:

  • Start Eclipse and switch to the Java perspective
  • Select File>Import ... or right click inside the Project Explorer and select Import ...
  • In the Import dialog select the option WAR file and click Next.
  • Choose the WAR file from your file system using the Browse... button. If not yet done, choose an installed Server Runtime Environment, for example Apache Tomcat.
  • Click Finish to create a Web Project from the imported WAR file

You can also use Apache Maven, in Appendix you will find detailed instructions how to start Tapestry project using quickstart archetype.

To launch the application, right click on the recently created project and select Run As>Run on Server. After the server is started, call the URL http://localhost:8080/app in your browser. The result will look like:


Now your first Tapestry application is up and running. Let's examine the directory structure.

 


In the source folder you will find the application's root package t5demo. Inside the application's web.xml deployment descriptor, you will find a context parameter tapestry.app-package whose value is the name of this package. Unlike nearly all Java web frameworks, Tapestry 5 doesn't require any XML configuration files. This context parameter is the sole bit of configuration you need to provide. It informs Tapestry of where to look for pages, components, and other classes needed at runtime. For example, page classes are expected to be inside the pages subpackage of our tapestry.app-package (that is, t5demo.pages). Accordingly the component classes should be placed into the t5demo.components.

Your first Tapestry page

Let's start with our first Tapestry page. A Tapestry page is a combination of a Java class and a Tapestry component template. Component templates are files with a ".tml" extension, where "tml" stands for Tapestry Markup Language. Templates for pages are stored either in the same package as page class or inside web application root folder (WebContent). Tapestry templates are well-formed XML documents; you can edit them as you would an XHTML document. Tapestry elements and attributes are defined inside the Tapestry XML namespace and, by convention, use the prefix "t:". In Tapestry, the page Start is the equivalent of an "index" page; it will be shown when the URL does not identify a specific page name.

For our example let's edit the file Start.tml. Let's throw away the initial content of this file and start fresh. Update the template with the following content:

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Tapestry 5 Demo Application</title>
</head>
<body>
${helloWorld}
</body>
</html>

Inside the body tag you can see the expression ${helloWorld}. This is called an expansion. In this case the expansion is used to access the property helloWorld of the page class, t5demo.pages.Start.

Now let's create the class Start in the package t5demo.pages. Unlike nearly all Java web frameworks, Tapestry doesn't force you to extend your classes from a base class, or implement a particular interface. Tapestry pages are POJOs (Plain Old Java Objects). Like any other POJOs, the page classes must be public and must have a no-arguments constructor. The only issue you have to consider is the root package of the application and its subpackages for pages, components, etc.

The template references a property named helloWorld, so let's create it. Tapestry follows Sun's JavaBeans coding specification, so we need to create an accessor method named getHelloWorld().

For the sake of demonstration let's create that but instead create a method named getHello() and see how Tapestry will react:

package t5demo.pages;

public class Start {

public String getHello(){
return "Hello World!";
}
}

When you run your application you should see the standard Tapestry exception page:

The Exception page is not only reporting problem (misspelled or otherwise wrong property name) but also offers some ways to solve it by presenting a list of available properties. It has also pinpointed where the error occurs in the template, not only listing the file name and line number, but even showing you a short excerpt. This attention to the needs of actual developers is one of the areas where Tapestry really distinguishes itself.

Now we can change the Start.tml to change the expansion to ${hello}; when we refresh the page in our browser, we'll see the working page:

Here's an interesting and powerful fact: you could change the code instead and Tapestry would pick up the change to your Java class as easily as the change to the template. Tapestry doesn't force you to redeploy just to see changes to your page and component classes. This way you'll find yourself working with unprecedented high levels of productivity and low levels of frustration. Another big bonus for using Tapestry. More information about the live reloading feature of Tapestry 5 is available from the Tapestry web site.

Most often, the pages of an application will have common and consistent elements for navigation, general layout, use of CSS (Cascading Style Sheets), copyright messages, and so forth. The tradition approach is to use some form of server-side include, but that's not the Tapestry way ... the Tapestry way is to create and use a component.

Your first Tapestry component

Many Java Web Frameworks use SiteMesh or Tiles for page decoration. In Tapestry this can be accomplished by a simple component. Our first component, Layout, will be used by all our pages to wrap a common layout around the page-specific content. We choose the Blue Freedom design for our demo application and copy its markup into the a new file Layout.tml located in the package t5demo.components. We also copy its stylesheet and related images into the web application root folder (WebContent).

As you can see in the example below, the consistent content that is needed in every page (the title, the header, the footer, and so forth), is placed into this file. The element <t:body/> is used to identify where the page specific content is to be placed. When a page is rendered, the <t:body/> is replaced by the page-specific content.

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Tapestry 5 Demo Application</title>
</head>
<body>
<div class="header">
<h1>Tapestry 5 Demo</h1>
<h2>Springfield</h2>
</div>
...
<t:body/>
<div id="footer">
Design by <a href="http://www.minimalistic-design.net">Minimalistic Design</a>
</div>
</body>
</html>

The HTML elements responsible for the layout can be removed from Start's page's template. To include the common HTML we use the component Layout. We can simply use an element in the Tapestry namespace whose name matches the component type. Referencing the Layout component this way, the Start's template can be reduced to following:

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
${hello}
</t:layout>

Notice how the <t:layout> component wraps around the ${hello} expansion: the expansions is the page-specific content, but other pages will have forms and tables and all the other trappings of a web application. Also, you may be wondering "is it layout or Layout"? The answer is: yes. Or really, the answer is that nearly everywhere, Tapestry doesn't care about case.

Tapestry components, like pages, consist of a component template and a Java class, and it's time to create the class. You might have noticed that the template didn't indicate a stylesheet, even though most of the look and feel is derived from a stylesheet (and a few related images). That's because its easier to provide the stylesheet in the Java class than in the template! Tapestry has a special annotation for this purpose:

@IncludeStylesheet("context:style.css")
public class Layout {

}

The "context:" prefix clues Tapestry in on where to look for the file. Another option is "classpath:", which is used by Tapestry components to reference files stored inside JARs ... Tapestry takes care of building a URL for the client that can reference such a file.

At this point, we're ready to leave behind make-work such as a "Hello World" message and dive into create a more realistic application. We'll start by defining the model, the data objects to be edited and displayed.

Defining your model

Now let's create a simple domain model for our example application. We will create a CRUD (Create, Retrieve, Update and Delete) like application for managing users, each user will looks like this:

To achieve this result we will create a following model class:

package t5demo.model;

public class User {
private long id;
private String name;
private String email;
private Date birthday;
private Role role = Role.GUEST;
...
}

Attribute id was added to identify users; it will be used as a primary key in external storage (such as a database). The Enumeration enum type describe user roles:

package t5demo.model;

public enum Role {
ADMIN, USER, GUEST
}

Model classes are usually persisted in a kind of persistent storage, such as database. A set of database access objects (DAO) is usually used to fetch the data from the database. In the next section we will create a DAO and see how to integrate it inside the Tapestry pages.

Tapestry IoC

Tapestry IoC (Inversion of Control) is a sub-framework of Tapestry that acts as a container for objects, known as services. Tapestry itself consists of over 120 inter-connected services, and the framework is designed to easily accommodate services specific to your application. You can think of an Inversion of Control container as a kind of streamlined version of Enterprise JavaBeans that is focused on managing the life cycle of the service objects and connecting them together. The connecting part is called Dependency Injection (DI).

The Tapestry IoC framework is inspired by the best features of well known containers such as Spring, Guice and Apache HiveMind and combines simplicity, flexibility, easy deployment, fast startup and considerable power.

Given the number of existing containers (and this is only a partial list), you might ask: why not use Spring or Guice? Why invent something new? Tapestry has requirements related to life cycle and extensibility that are not satisfied by any existing container, and so it has its own, suited specifically for its needs. You can even use Tapestry IoC separately from Tapestry. We refer the reader to the Tapestry IoC documentation for further details.

DAO (Data Access Object) is a standard pattern for accessing data from the external resources, i.e. databases. In our example application we will use a UserDAO service to retrieve our User instances. Please note that Tapestry IoC and Guice use the term service whereas Spring uses the term bean.

public interface UserDAO {
List<User> findAllUsers();
User find(long id);
void save(User user);
void delete(User user);
User findUserByName(String name);
}

As you can see, we have all the basic methods you might expect for a simple DAO. The implementation of the DAO is really not interesting and relevant for the article. When implementing real applications you would use an Object-Relational mapping tool (e.g. Hibernate) and most probably will use the native Tapestry 5 Hibernate integration. For our example we will emulate the database using just a java.util.ArrayList. This way you can start the demo application without any additional configuration.

Now we need to tell Tapestry which implementation to instantiate when the service UserDAO is requested. For this purpose we will edit the AppModule class inside the application services package t5demo.services. As you remember, t5demo is the application's root package. The services subpackage should be used for services. The AppModule class is where you define services specific to you application, as well as configure services built into Tapestry. Inside the AppModule class we need to bind the interface of the UserDAO service to its implementation:

public class AppModule {
public static void bind(ServiceBinder binder) {
binder.bind(UserDAO.class, UserDAOImpl.class);
}
}

That's it, the ServiceBinder will create a binding between the UserDAO interface and the UserDAOImpl implementation. Class UserDAOImpl has a default constructor and can be instantiated using Java Reflection API. Tapestry 5 will create a new instance of UserDAOImpl and inject it as soon as any page or service will need UserDAO. As you can see, no XML configuration required. You can certainly imagine how easy it would be to create unit tests for classes using UserDAO. By default all services are singletons. Therefore only one instance of UserDAOImpl will be created, and that single instance will be shared across multiple threads and, by extension, work on behalf of multiple application users.

For our demo application we would like to pre-fill the fake database with some fake data. Therefore we will add one initialization method to our UserDAOImpl, that will be invoked inside the constructor.

public class UserDAOImpl implements UserDAO {

public UserDAOImpl() { createDemoData(); }

public void createDemoData() {
save(new User("Homer Simpson", "homer@springfield.org", new Date()));
save(new User("Marge Simpson", "marge@springfield.org", new Date()));
save(new User("Bart Simpson", "bart@springfield.org", new Date()));
...
}

....
}

Now it's time to display the users.

Grid component

In our first page we are going to display a list of Users in a table:

To implement this new version of the Start page we'll combine four elements:

  • The Layout component for the general look and feel
  • The Tapestry Grid component to render the table of elements
  • The UserDAO service to access the list of Users
  • The Start page itself to integrate everything else

In the previous section we described the basics of Tapestry 5 IoC. In this section we will use the service created by our AppModule. To get our UserDAO service injected we just need to declare a private member of type t5demo.services.UserDAO and mark it with the @Inject annotation. Tapestry will find the UserDAO service based on its type and link it to the page via the field.

public class Start{

@Inject
private UserDAO userDAO;

public List<User> getUsers() {
return userDAO.findAllUsers();
}
}

The template of the page is very simple, consisting just of a single instance of the Grid component. Grid's source parameter is required and is used to get the data to display. In our case the source is a List<User> returned by the method getUsers().

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users"/>
</t:layout>

As you can see, every single row represents a User. The values of the rows are taken from properties of the User instance and formatted automatically according to their type. For example the column 'birthday' is already formated as a date, in a locale-sensitive manner. Behind the scenes, Tapestry is doing a bit of work, such as formatting the Role enum for user-presentable display ("Guest" instead of "GUEST"). Furthermore the table is sortable. If the number of available rows in the data source exceeds a certain value, the Grid will automatically add a pager to navigate the overall data. The number of rows of data displayed on each page can be changed via Grid's rowsPerPage parameter.

By default, Grid's column order is exactly the same as order of the getters inside our User class. This order can be overridden using Grid's reorder parameter: a comma-separated list of property names.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" reorder="role,email,name,id,birthday"/>
</t:layout>

So, our first pass at customizing the output:

A property such as id is really internal, it has no meaning to the end user and should be omitted. We can leverage the Grid components' exclude parameter, specifying a list of properties to be omitted.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" exclude="id,email"/>
</t:layout>

When the exclude parameter is applied, our Start page starts to take shape:

Later in this article we will demonstrate another very powerful Tapestry component that will generate user input forms based directly from JavaBeans. To provide a consistent view of our domain model we should use another approach for removing properties from the user interface. We will identify properties that should not be part of the user interface with the @NonVisual annotation. This meta-information will be recognized not only by Grid but also by several other Tapestry components (Please see the BeanEditForm component section). In our example we only remove the identifier of the User.

public class User {
...
@NonVisual
public Long getId() { return id; }
}

And using just a simple Grid component declaration:

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users"/>
</t:layout>

The result looks like this:

For our next customization, we'll add the ability to edit a user. For this purpose we will create a page, which we discuss later. In order to create an edit link we will override how the Grid component renders one column. Instead of simply rendering out the User's name property, we'll render a link to another page responsible for editing Users.

This is accomplished with another special template element, <t:parameter>. This element is, effectively, a way to pass a chunk of a component template into a component as a parameter.

The name of the <t:parameter> element is used to determine the property to override the rendering for. Per convention the name should be composed of the property name and the suffix Cell (it is also possible to override the column headers with a Header suffix).

In our example, the <t:parameter> element name is nameCell, so the Grid component will use the provided PageLink component to render the column containing each User's name.

The PageLink component renders an HTML link to the page specified by the page parameter; additional information needed by the indicated page is provided in the context parameter. For this example , the page for editing a User will be called Edit and will accept a user ID as parameter. We will also need a property to store the User being rendered, so that we can pull out its name and id properties. We will need to create a property named user in our Start class. We can then bind the Grid component's row parameter to this property.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" row="user">
<t:parameter name="nameCell">
<t:pagelink page="edit" context="user.id">${user.name}<
/t:pagelink>
</t:parameter>

</t:grid>
</t:layout>

You can see here that expansions and other property expressions are not limited to a simple property name. You may use dotted notation to navigate one or more properties, to read (and as we'll see later, edit) a nested property.

We can customize any number of the Grid's columns this way.

Don't forget to add the property user to the Start class:

public class Start {
...
private User user;
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }

}

And Tapestry will complain if there isn't at least a class for the Edit page:

public class Edit {
}

We can fill in the details of the Edit page later.

We are now nearly done with the Start page:

What if we want to add another column that doesn't correspond to a property of a User? For example, we may want a column so that we can delete unwanted users. This can be accomplished using a virtual property, but it takes a smidgen more work.

The Grid component decides how to display a bean, such as a User object, by first generating a BeanModel for the object. By creating and then customizing the BeanModel ourselves, we can have total control over how the Grid operates. We must inject new objects into the Start page:

  • org.apache.tapestry.services.BeanModelSource - a service responsible for creation of BeanModels for a particular bean class.
  • org.apache.tapestry.ComponentResources - provides some framework functionality that is needed by BeanModelSource.
BeanModel and then add our artificial column delete into it.
public class Start {
...
@Inject
private BeanModelSource beanModelSource;

@Inject
private ComponentResources resources;

public BeanModel getModel() {
BeanModel model = beanModelSource.create(User.class, false, resources);
model.add("delete", null);
return model;
}

}

In the Start page's template we have two further changes: first, to tell the Grid component explicitly about the model it should use, and second, to provide a <t:parameter> block to render the delete column.

We'll use an ActionLink component to find out when the user wants to delete a user. An ActionLink will trigger an event on the Start page. We can observe the event by providing a specially named method. Providing a method name onActionFromDelete() informs Tapestry that the method should be invoked when an "action" event is triggered in the component whose id is "delete".

But which User to delete? Once again, we'll pass the id as context; the value will then be available as a method parameter to the event handler method:

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<t:grid source="users" row="user" model="model">
<t:parameter name="nameCell">...</t:parameter>
<t:parameter name="deleteCell">
<t:actionlink t:id="delete" context="user.id">Delete<
/t:actionlink>
</t:parameter>

</t:grid>
</t:layout>

In the Start class, we provide the event handler method:

public class Start {
...
void onActionFromDelete(long id) {
user = userDAO.find(id);
if (user != null)
userDAO.delete(user);
}

}

Event handler methods do not have to be public; the preferred visibility is package private (the visibility provided with no modifier). Such a method can be invoked by Tapestry, and by other classes within the same package (such as unit tests), but is not part of the page's public API.

We have now finished the Start page and can move on to the Edit page:

Navigation patterns

As you remember, we need to implement a page where Users can be edited. Let's name the page Edit. In this section we describe how to navigate from the Start page to Edit page. For this purpose, let's create a template Edit.tml in the web application root folder (we already created an empty class, t5demo.pages.Edit). In this section we will describe navigation logic in Tapestry 5. The editing logic follows in the next section.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Edit/Create Citizen</h1>
</t:layout>

Since our page will edit an user, we would need an access to the UserDAO. To inject it, we create a private variable and mark it with @Inject annotation as before:

public class Edit {

@Inject
private UserDAO userDAO;
...
}

Our edit page needs to know the identifier of the User to edit. This kind of navigation pattern (often called master/detail) is quite common when developing web applications. Tapestry 5 has an explicit support for it using RESTfull page activation context mechanism.

REST stands for Representational State Transfer . It is a style of software architecture of distributed hypermedia systems. The REST's central principle is resource which is identified by URI, to modify the resource clients are communicating via standard interface (e.g. HTTP) and exchange representation of these resources. More on advantages of REST you can read on Wikipedia, in our case it's important to see that REST encourage us to make application state part of URL, and that's where the page activation context will help us.

All pages in Tapestry may support a page activation context. The page activation context contains the state of the page that can be preserved across the requests. According to REST paradigm, the page activation context is appended to the URL for the page. Page activation context is very similar to storing the page state inside the HTTP Session. However in this case you would need an active session and page state is not bookmarkable.

In Tapestry, pages are actively involved in providing the page activation context, and equally involved in processing that activation context. This is handled by providing event handler methods for two events: "passivate" and "activate". For the "passivate" event, the page should provide a value (or an array of values) that represent its complete internal state. For the Edit page, that is the id of the User being edited. When a later request (such as a form submission) for that page is processed, the page activation state will be provided, via the "activate" method, and the page will restore its full state from the provided value.

So, our first step is to add these two event handler methods:

public class Edit {
...
private User user = null;
private long userId = 0;

void onActivate(long id) {
user = userDAO.find(id);
userId = id;
}

long onPassivate() {
return userId;
}
}

Inside the onActivate() method we expect a single long value as our page activation context, and inside the onPassivate() method we provide that exact value. On activation, the Edit page uses the injected UserDAO service to fetch the User from the database and stores it inside the private variable.

Now let's have a look at the URLs of the pages where context is presented. If you navigate to the first page of our demo application, you will see that URL to edit a User has the following form: http://localhost:8080/app/edit/3. Taking into account that all page names in Tapestry are case insensitive, you can see that this URL will render the Edit page and the User to edit has the identifier 3. The URL is plain and simple; it is RESTfull and therefore it is bookmarkable (we could store this URL in our browser's bookmarks and come back to the exact same page tomorrow, which would not be possible if any of the state were stored in the HttpSession).

Now that we have the ability to track the User to be edited, our next task is to provide a user interface. Doing so is surprisingly simple.

BeanEditForm component

In the previous chapter we started to work on the page where existing users can be created and/or modified. We created a page class that is able to maintain it's state and knows about the user to be edited. As in majority of the component oriented web frameworks, we need to create a form component and nested fields. Each value of the model object shall be bound to the field of the appropriate type (text fields for short text, checkboxes for booleans, drop down lists for enumerated types, and so forth). We also want to consider input validation. That's the usual way, but Tapestry has an easier way.

First of all we need to create a public getter method for the user private variable. As you remember, this variable is initialized as soon as page has restored its state from the page activation context:

public class Edit {
...
public User getUser() {
return user;
}

...
}

That wasn't very hard so far. Was it? Now it's event simpler, we need to add following line to the template:

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Edit/Create Citizen</h1>
<div id="section">
<t:beaneditform t:id="form" t:object="user"/>
</div>

</t:layout>

We are using Tapestry component called BeanEditForm. It has only one required parameter object that should be bound to the property with object we want to edit. That's all. Now you can refresh the Edit page and see that it was filled with automatically generated form that contains fields for each property of the edited model class.

Tapestry 5 is again doing quite a bit of work to simplify the developer's life. Right off the bat we have a useable user interface and, as with the Grid component, lots of room for customization. This form reflects little details; field labels are properly transformed from camel case ("userName" will be shown as "User Name"), enumerated values are rendered as a drop down list with correct set of options (also transformed for readability), and a popup calendar is used for editing properties that are dates. The BeanEditForm also respects the @NonVisual annotation, so the id field is ommitted. Because both the Grid and the BeanEditForm components work off the same BeanModel they will continue to be consistent with each other.

The BeanEditForm component is very flexible and provides a lot of customization possibilities:

  • You can reorder fields inside the generated form by simple reordering of getters inside your source code, or programmatically.
  • You can hide fields using @NonVisual annotation or comma-separated list of properties provided in component parameter.
  • You can completely replace some parts of the generated form with custom blocks to render some input fields differently (for example password field).
  • You can dynamically modify meta-information for the edited bean, the same way as it was done for the Grid before.
  • You can change labels on the form and off course you get a full internationalization support.
  • You add/remove buttons inside the form.

Second step in our editing form example is validation. Since we are using automated generation of the forms, the best place for validation constraints is our model object. To add a validation constraint, just annotate the access methods of the particular field with @Validate annotation.

public class User {
...
@Validate("required")
public String getName() {
return name;
}

@Validate("required,regexp")
public String getEmail() {
return email;
}
...
}

In our example we require only name and email properties to be filled in. The @Validate annotation accepts a string parameter that will describe the validation constraints. The syntax of this parameter might be familiar for Tapestry 4 users, essentially you just list constraints inside the comma-separated list. Possible constraint names are required, max, min, maxLength, minLength and regexp. The regular expression validator needs two parameters: the expression and the human readable message to be displayed when validation fails. These two validation parameters are both pretty long and a bad fit for the template because of all the special characters. Fortunately, Tapestry pages and applications can have a message catalog, a natural place to store long, complex strings. For this purpose we create a file Edit.properties in the package t5demo.pages. As you can see, the key of the expression is email-regexp. This key is composed of the property name to be validated and the validator name. The key of the message contains a further suffix -message.

 email-regexp=^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$
email-regexp-message=Provided email is not valid

By default all forms in Tapestry have client side validation enabled. If you try to submit a form with an empty field that is required, you'll immediately see a message in a bubble like in the following figure:



Obviously client side validation is duplicated with server-side one. When you disable javascript, you will see the error message appearing after the form submit. The error message in the following figure is generated on the server side:



Once the user has submitted the form on the client side, the action picks up on the server side. Assuming there are no validation errors, the BeanEditForm component will fire a "success" (as in, a successful form submission) event. Once again we provide an event handler method, onSuccess() (it will match any component that triggers a "success" event, but the BeanEditForm is the only candidate, so it's OK to not be more specific). This time, we are not only going to perform an action (update the User back into the database) but also navigate back to the Start page.

public class Edit {
...
public Object onSuccess() {
userDAO.save(user);

return Start.class;
}
...
}

Tapestry interprets the return value on an event handler method in a special way: as a directive of which page should render the response to the user. Returning a page instance, or a page class, or even the name of a page (as a String) navigates to that appropriate page. Other return values are supported as well (see the Tapestry documentation for a complete list).

The validations built into Tapestry are limited: they operate on both the client and on the server, but they are limited in scope. They are syntactic, they are based just on the characters provided. Say we want to perform some validation related to business logic, for example, to ensure that when a User name is edited, the new name is unique.

Again, Tapestry works with our code by firing an event; this event is named "validateForm" as is fired before the "success" event.

public class Edit {
...

@Component
private BeanEditForm form;

public void onValidateForm() {
User anotherUser = userDAO.findUserByName(user.getName());
if (anotherUser != null && anotherUser.getId() != user.getId()) {
form.recordError("User with the name '" + user.getName() + "' already exists");
}
}
}

As you can see, we can use BeanEditForm.recordError() to add errors to the form programmatically.

Now, after the editing of the existing user works, we just need to think about the creation of new users. BeanEditForm component has a very nice behavior: when edited property is null it will just create a new instance of the bounded class and set it back to the page. Using this behavior we just need to add a public setter for the user variable. When page is activated, we need to take care when passed in identifier is 0.

public void onActivate(long id) {
if (id > 0) {
user = userDAO.find(id);
this.userId = id;
}
}

That's about it. Now when we will pass a user identifier to the Edit page, it will edit the user. If the identifier is not passed or passed identifier is equals to zero, then Edit page will simply create a new instance of User.

Ajax in Tapestry 5

Ajax stands for Asynchronous JavaScript and XML. Ajax is a special technique that is used for creation of dynamic and more user friendly web sites and web applications. With Ajax, responsiveness and interactivity of the web pages is achieved by exchanging data asynchronously between the client and the server. Ajax is very powerful technique, more about it you can read on Wikipedia

Tapestry Ajax support is based on popular Prototype and script.aculo.us JavaScript libraries. Since these two libraries are bundled inside Tapestry, no separate deployment or configuration is needed.

Usually, inside Ajax-enabled applications, some parts of a page are refreshed on user request. In Tapestry, ActionLink component may be used to trigger Ajax actions. As an example we'll customize the Grid component once again by adding a further column containing links to view user's details. Let's update the model of our Grid by adding a column named view, which will be will be rendered as an ActionLink. First we add the new artificial property to our BeanModel:

public class Start{
...
public BeanModel getModel() {
BeanModel model = beanModelSource.create(User.class, false, resources);
model.add("delete", null);
model.add("view", null);
return model;
}
}

Then we define the rendering of the property view by adding a further <t:parameter/>.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:grid source="users" model="model" row="user">
<t:parameter name="nameCell">...</t:parameter>
<t:parameter name="deleteCell">...</t:parameter>
<t:parameter name="viewCell">
<t:actionlink t:id="view" zone="viewZone" context="user.id">View</t:actionlink>
</t:parameter>

</t:grid>
</t:layout>

As already mentioned, ActionLink is a component that triggers an action on the server side. Per default the triggered action causes a full refresh of the page. The parameter zone tells ActionLink to make an Ajax call and to update a zone, whose client id is the value of the parameter. A Zone is Tapestry's approach to perform partial updates to the client side. Zone component marks a part of the page that can be dynamically updated. Let's first create a Zone with the client id viewZone.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:zone t:id="viewZone"/>

<t:grid source="users" model="model" row="user">...</t:grid>
</t:layout>

The content to update the Zone with, can be defined by a <t:block>. A <t:block> is a kind of a free-floating <t:parameter> that can be injected into a component. The content inside a block is not rendered by default. We create a block with the id userDetails and display some properties of the user stored into the page's property detailUser inside this block.

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Springfield citizens</h1>
<t:zone t:id="viewZone"/>
<t:block id="userDetails">
<h2>${detailUser.name}</h2>
<ul style="padding-left: 40px;">
<li>Identifier: ${detailUser.id}</li>
<li>Email: ${detailUser.email}</li>
<li>Birthday: ${detailUser.birthday}</li>
</ul>
</t:block>

<t:grid source="users" model="model" row="user">...</t:grid>
</t:layout>

When ActionLink view is clicked, an action event is triggered. Accordingly, the matching method onActionFromView is called. The method parameter represents the user identifier and is used to find the user in the database. The retrieved user is stored into the private property detailUser, which we use inside a <t:block>.

As described above, the return type of an action method is used to determine the type of the response. In a traditional non-Ajax request the return type of the action method is used to determine the page to render the response. In order to perform a partial response, we may return a component or a block whose markup will be used to update the content of our Zone without to refresh the whole page. For this purpose we inject the recently created block via @Inject annotation. Note that for injection to work, property name property class should be the same as component client id and component class (otherwise you can customize @Inject annotation). The injected block is returned by our action method onActionFromView. The block is rendered and its markup is inserted into the Zone.

public class Start{
...
@Inject
private Block userDetails;

private User detailUser;

public User getDetailUser() { return detailUser; }

Block onActionFromView(long id){
detailUser = userDAO.find(id);
return userDetails;
}

}

Here we demostrated the basic Ajax functionality - partial page refresh. With Tapestry 5 such functionality is very easy to implement and you don't need to write a line of JavaScript on your own. Functionality provided by embedded JavaScript libraries is cross-browser compatible production-ready.

Creating your own ajax component

Obviously full interactivity in Web application can not be achieved only using partial page refreshes, now we want to see how to create our own Ajax component. In this section we will create a Tapestry 5 component that will use Ajax.InPlaceEditor from script.aculo.us JavaScript library. InPlaceEditor allows you to edit some text on the page. Clicking that text creates an input field on the fly, whose value is the text we clicked on. Submitting the form we replace the original text on the page by the value of the field.

First we inject the JavaScript file with InPlaceEditor. We do it in a similar way we included our Cascading Style Sheets into the Layout - using annotation @IncludeJavaScriptLibrary. Please note, that if inside a page you are using a bundle of components including the same JavaScript file via @IncludeJavaScriptLibrary, you don't have to worry about the repeated inclusion of the particular file. Tapestry includes every single resource only once.

Secondly, we inject services needed for our component:

  • org.apache.tapestry.ComponentResources - already known from previous sections.
  • org.apache.tapestry.PageRenderSupport - provides rendering support to pages and components.
  • org.apache.tapestry.services.Request - a wrapper of HttpServletRequest.

We need to create a new class t5demo.components.InPlaceEditor with following content:

@IncludeJavaScriptLibrary("${tapestry.scriptaculous}/controls.js")
public class InPlaceEditor{

private static final String PARAM_NAME = "t:InPlaceEditor";

@Environmental
private PageRenderSupport pageRenderSupport;

@Inject
private ComponentResources resources;

@Inject
private Request request;

}

Next, we create a required component parameter named value. It is bound to a property of the InPlaceEditor's container and will be edited.

public class InPlaceEditor{
...
@Parameter(required = true)
private String value;
...
}

Now let's write a method to render the component. The rendering of Tapestry components is divided into several phases. We did not mention these phases so far. Since we want to keep the article simple, we will not discuss them all in this article. We refer you to the Tapestry documentation, if you want to learn more about them.

To render our component we plug into the phase AfterRender. This phase is usually used in combination with BeginRender to render a html tag and to decorate the component's template. Since our component has neither a tag nor a template, AfterRender will do it. To plug into the phase AfterRender we implement the method afterRender(MarkupWriter). First we try to get the element name used in the markup. If there is no element name defined, we take <span/>. Then we allocate a unique id for our component. In the next step we open the html element and render the informal parameters. Informal parameters are parameters which weren't defined by our component but were provided by the user inside the template.The only one formal parameter (specified for our component) is value. An example of a typical informal parameter is style="someCssStyle", we are not usually interested in parsing user provided styles however we would like to preserve it when we will render our component. After writing the value we close the element recently opened. Finally we write the JavaScript code to create a Ajax.InPlaceEditor.

The constructor of JavaScript "class" Ajax.InPlaceEditor takes three parameters:

  • The first is the client id of the element to support in-place editing.
  • The second is the URL to submit the changed value to.
  • The third is a JSON object containing options.

In the previous section we learned the ActionLink component that we used to trigger server side action. In this example we use the service ComponentResources to create an ActionLink programmatically. When the link is clicked, it will fire the event edit. This event name will be used to match the corresponding method to invoke when the event is triggered. The JSONObject passed to InPlaceEditor constructor contains only one key/value pair, which is used to name the request parameter containing the value submitted by Ajax.InPlaceEditor. Note that PageRenderSupport is used to add the JavaScript code into the page. This service will add a callback JavaScript function into the page, that will be invoked when the page DOM is loaded. The resulting HTML you will see later but now is full method code:

void afterRender(MarkupWriter writer){
String elementName=resources.getElementName();
if(elementName==null)elementName="span";

String clientId = pageRenderSupport.allocateClientId(resources.getId());
writer.element(elementName, "id", clientId);
resources.renderInformalParameters(writer);
if (value != null)
writer.write(value);
writer.end();

JSONObject config = new JSONObject();
config.put("paramName", PARAM_NAME);

Link link = resources.createActionLink("edit", false);
pageRenderSupport.addScript("new Ajax.InPlaceEditor('%s', '%s', %s);",
clientId, link.toAbsoluteURI(), config);
}

Finally we provide the action method to match the triggered event. When the ActionLink is clicked, it will fire the event edit. Accordingly our action method should be named onEdit. In this method we get the submitted value from the specified request parameter and assign it to the bounded property. To update the client side with the new value, we return a TextStreamResponse which provides a stream of data to be sent to the client. There are further types that may be returned to update the client.

  • Component or block. In the previous section we used a block to update a Zone.
  • JSONObject
Object onEdit(){
value = request.getParameter(PARAM_NAME);
return new TextStreamResponse("text", value);
}

Let's create an example page using InPlaceEditor. Let edit be the property of the page to be edited. Marking the field by @Persist annotation we tell Tapestry to persist the value of the field from one request to the next. Furthermore we create public getter/setter methods.

public class InPlaceEditorExample {
@Persist
private String edit="Please click here";
public String getEdit() { return edit; }
public void setEdit(String edit) { this.edit = edit; }
}

In the template we bound the value parameter to the property edit:

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h1>Using InPlaceEditor</h1>
<div t:type="InPlaceEditor" value="edit"/>
</t:layout>

This will result in the following html code:

...
<div id="inplaceeditor">Please click here</div>
...
<script type="text/javascript">
Tapestry.onDOMLoaded(function() {
new Ajax.InPlaceEditor('inplaceeditor',
'/app/inplaceeditorexample.inplaceeditor:edit',
{"paramName":"t:InPlaceEditor"});
});
</script>

When you call the page http://localhost:8080/app/inplaceeditorexample, you will see the default value of the property edit, namely Please click here.

And after you click on the editor, you will see something like this:

Just type some text and hit the button Ok. The property edit will be updated by the submitted value. Of course we should add further functionality to this component. For example we could add the validation of the editor field. But the intent of the article is to give you an overview of the Tapestry functionality, rather then to develop a full set of components, that are ready to be used in production.

Conclusion

We showed how simple it is to create a Tapestry application from scratch and how quick and productive you can be when using Tapestry components. We have calculated a number of non-commenting lines of code for our application using Java NCSS Plugin for Maven and the results were following:

  • Java code of our demo application consists of 205 NCSS (non commenting source lines).
  • Templates for our pages are 56 lines whereby 34 lines are in Layout.tml where page design is stored.
  • Two longest Java classes are UserDAOImpl and User, they are 68 lines together.
  • Largest page class is t5demo.pages.Start with only 27 lines of Java code and 12 lines of template code. Using Tapestry 5 we've managed to provide a sortable and pageable table with delete and view functionality. We were able to use Ajax for partial page rendering and that's all only using 39 lines of code.

Obviously, number of lines of code is not the only measure we should use. But it's clear that the more lines of code you have, the more challenging is your application to maintain, to understand and to test.

Significant efforts were spent by Tapestry team to improve the developer-frendlieness of the framework:

  • Tapestry 5 applications require almost zero configuration. For most of the parts, you usually need to configure, there are already sensible defaults. As you have seen before, we haven't created any line of XML in our example.
  • Unique class reloading feature boost development productivity. You no longer needed to restart you application container every time you found a small bug in the page or component's code. Pages and component classes are reloaded automatically.
  • Developer friendly error reporting not only showing the line number and code sniplet, but also suggesting the possible solutions.

Tapestry 5 gathered best practices from many application areas, such as convention over configuration and scaffolding inspired by Ruby on Rails, XML-less dependency injection and configuration inspired by Google Guice, REST-full URLs for scalability and many others. Output produced by Tapestry is valid (X)HTML. Complex components such as Grid and BeanEditForm are accessible, table-less and CSS customizable. Default output encoding is UTF-8 and localization is supported by the framework.

In our article we've shown only a small part of new Tapestry 5 features. We haven't covered Tapestry IoC to deep, localization of Tapestry applications, mixins and assets. We haven't mentioned Tapestry 5 integration modules for Hibernate, Spring, Seam, Acegi and many others. However, we hope we could show you how simple is Tapestry 5 development and how fast you can build production-ready and robust applications.

Acknowledgement

We would like to thank Howard M. Lewis Ship, creator of Tapestry, for reviewing and proof-reading this article. We appreciate the incredibly valuable feedback, he provided. Furthermore we would like to thank him for creating Tapestry. And last, but not least we would like to thank the whole Tapestry team and all the people managing open-source projects around Tapestry for amazing job.

Quickstart & Demo application

The source code of the Tapestry Quickstart and the Demo application we developed for this article can be downloaded from tapestry4nonbelievers project hosted on Google Code. Both applications are packaged as WAR files and can be imported into the IDE of you choice.

References

  1. Tapestry 5
  2. Spring
  3. Google Guice
  4. Apache HiveMind
  5. Prototype
  6. script.aculo.us
  7. Hibernate

Appendix

The most simple way to start building a Tapestry application is to use Maven and the Tapestry Quickstart archetype. A Maven archetype is a predefined project template that can be used to start a new project quickly. The Tapestry quickstart archetype defines a project directory structure, initial meta-information and even some basic application classes. If you are familiar with Apache Struts then you might be aware of the struts-blank.war file that is delivered with the Struts distribution. This file contains an empty Struts application that is fully deployable and functional.

The Tapestry 5 Maven archetype delivers an empty Tapestry application consisting of a single page, ready to be compiled and deployed to a servlet container. If you have Maven installed just execute the following command:

mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart \
-DgroupId=t5demo -DartifactId=app -DpackageName=t5demo -Dversion=1.0.0-SNAPSHOT

Maven uses the archetype to create the Tapestry application name app. Maven creates a subdirectory, app to store the new project. Let's examine what Maven has generated for you. If you are familiar with Maven you will recognize the default Maven directory structure.



To start the application using the Jetty servlet container, change to the newly created app directory and execute the following command:

mvn jetty:run

Maven will compile the generated application, download (just the first time!) the Jetty servlet container, and launch the compiled application. Once the application is launched you can access it in your browser as http://localhost:8080/app.


Unfortunately, Maven doesn't know to recompile Java classes while it is running Jetty. To benefit from the live reloading feature of Tapestry 5, you'll need to set up your IDE with an appropriate servlet support.

Hello stranger!

You need to Register an InfoQ account or to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread
Community comments

T5 is revolutionary by Joshua Partogi

T5 is definitely a revolutionary project. I am using it in my opensource project. Suhweeet. Great work Howard.

Nice by Cristian Botiza

Wow!

This is my first hands-on experience with Tapestry, although I've heard of it before. From this article, looks like it is incredible. I'm currently evaluating web frameworks for building an inventory application around a Spring-driven backend implemented with Hibernate. You made me re-consider the options :)

Are pages really POJOs? by Peter Thomas

The article does not include the full source code of Edit.java (I've pasted it below for discussion). Just a couple of thoughts here: I'm not a big fan of annotations and I'm also not very comfortable with having to remember to implement 'magic' methods such as onActivate, onPassivate, onSuccess and onValidateForm.



So - saying that Tapestry pages just need to be POJOs is a little misleading IMHO.


package t5demo.pages;

import org.apache.tapestry.annotations.Component;
import org.apache.tapestry.corelib.components.BeanEditForm;
import org.apache.tapestry.ioc.annotations.Inject;

import t5demo.model.User;
import t5demo.services.UserDAO;

/**
* Page used for editing/creating users
*/
public class Edit {

@Component
private BeanEditForm form;

@Inject
private UserDAO userDAO;

private User user;
private long userId = 0;

public void onActivate(long id) {
if (id > 0) {
user = userDAO.find(id);
this.userId = id;
}
}

public Object onSuccess() {
if (userId == 0) {
userDAO.save(user);
}
return Start.class;
}

public void onValidateForm() {
User anotherUser = userDAO.findUserByName(user.getName());
if (anotherUser != null && anotherUser.getId() != user.getId()) {
form.recordError("User with the name '" + user.getName() + "' already exists");
}
}

public long onPassivate() {
return userId;
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

}

Hibernate support is even easier by Howard Lewis Ship

This article was written a ways back (I helped with the editting) and in the meantime Tapestry 5 has rolled along; had this article been written today and for Hibernate, you'd see that Tapestry takes care of all the fiddly entity-to-id-and-back logic that is being done in explicit code here.

Re: Are pages really POJOs? by Howard Lewis Ship

So, are you saying that the Java objects would be "plainer" if:



  • You had to extend from a framework base class?

  • You had to implement a specific interface?




I define "plain" as no inheritance imposition, no visibility imposition, no interface imposition. Just the object, easily testable. Tapestry is pretty close to that (variables have to be private, though). In the article, the event handler methods were public, but I generally make them package private (no modifier). The end result? The page class is almost entirely focused on your business objects and services, with a minimal amount of framework clutter.


Java has really beat people down, language-wise, where they look at a problem, such as the way compiler makes you sit up and beg rather than being actually helpful (i.e., error messages like "you must put this cast there even though I know what you wanted") and see it as a <em>feature</em>.


Tapestry is about having the environment kowtow to <em>your</em> code instead of the other way around, which is more <em>traditional</em>. Knowing <em>what to name your method</em> (or which annotation to use) is no different than knowing <em>which interface to implement</em> or <em>which base class to extend</em>; but the Tapestry approach does a better job of encapsulation and keeps things smaller and easier to test.

Re: Are pages really POJOs? by Peter Thomas

So, are you saying that the Java objects would be "plainer" if:

* You had to extend from a framework base class?
* You had to implement a specific interface?


Of course not. Quoting from the article below:

Unlike nearly all Java web frameworks, Tapestry doesn't force you to extend your classes from a base class, or implement a particular interface. Tapestry pages are POJOs (Plain Old Java Objects). Like any other POJOs, the page classes must be public and must have a no-arguments constructor. The only issue you have to consider is the root package of the application and its subpackages for pages, components, etc.


I am just pointing out that the above statement gives the (IMHO false) impression that Tapestry page components are lighter than they really are. Having a bunch of org.apache.tapestry.* imports does not feel very POJO-like to me. And also there are these onActivate, onPassivate, onSuccess and onValidateForm framework methods.

The page class is almost entirely focused on your business objects and services, with a minimal amount of framework clutter.


Looking at the page code I posted above - I'm not really convinced.

Java has really beat people down, language-wise, where they look at a problem, such as the way compiler makes you sit up and beg rather than being actually helpful (i.e., error messages like "you must put this cast there even though I know what you wanted") and see it as a feature.


Huh? So you are saying that the compiler ensuring type safety and reminding you to implement a method is a bad thing?



If I really wanted the environment to "kowtow" to my code I would have switched to Ruby or something long back. What would happen if the user mis-spelled onSuccess as 'onSucess' or something?

Re: Are pages really POJOs? by Howard Lewis Ship

Since event handlers are always optional, Tapestry can't tell the difference between a misspelling and no event handler at all. Misspell "onSuccess" and your success event handler is not invoked and Tapestry's default behavior (redisplaying the page) is used instead. You'll notice that very quickly.



Anyone whose looked at my code knows that I'm a stickler for types, interfaces and contracts (and I have a tens of thousands of lines of code available to back that statement up. However, Tapestry 5 uses a looser, more adaptive sense of contract than traditional interfaces. It sure beats having endless configuration in XML (especially fully qualified class names and method names), and relative to other frameworks, the amount of code you write ends up being quite minuscule. Every tool has a learning curve, but the Tapestry 5 curve is very shallow for beginners and very rich for experts.

To make this even better... by Richard Clark

I noticed a handful of things that would make this even better (in decreasing priority):
1. Show the reader what you're going to build before you build it. You nailed it on "Your first Tapestry component" (picture first, text below) and started falling away from that later. It's too easy for readers to get lost in the code details if you don't show the context first.
2. Include a link to a working zip (or WAR) file after each step. One small misstep and a newbie could become completely lost. Remember that the Tapestry docs aren't 100% there yet.
3. Include details on how to run the app the first time (e.g. mvn jetty:run) and how to configure so automatic class, page, and component reloading works.
4. When the reader needs to download something external (e.g. the layout package), please provide instructions.
5. Make sure the code samples you give (e.g. Layout.tml) generate output that match your screen shots. I had to alter the HTML code to get something reasonably correct.

Let me repeat this: you did a great job. I'm just being passionate about ease-of-learning (ask Howard about my review of a class he taught... ;))

...Richard

Re: Are pages really POJOs? by Martin Batty

One of the problems Howard found with previous versions of Tapestry was that using subclassing in components exposed the page developer to the internals of Tapestry. This caused problems when the framework was upgraded and restricted the scope of changes that could be made to Tapestry.



By using a naming convention (or alternatively, annotations) the Tapestry developers can decouple the framework internals from the user's pages and components, and only a subset of the framework API should be required. Howard has made this distinction explicit by creating a org.apache.tapestry.internal.* package. Any users importing these classes should be aware that the implementation may change from version to version of Tapestry and their use should be minimised.



Moreover, as Tapestry 5 evolves more features can be added transparently and the user needs to simply add methods, import new annotations or @Inject objects using IOC.



Pages are POJO's in the sense that they are standalone classes, not that they are your business domain objects. They are still very much tied to the Tapestry framework and the page lifecycle, but it is achieved by convention rather than overriding methods and implementing specific interfaces.



It's true that you can fall afoul of typos and then get hit at runtime, but the immediate class reloading means this doesn't cost much in time.

Re: Are pages really POJOs? by Onno Scheffers

First of all: Thanks for this great article! I enjoyed it a lot.





@Howard

Since event handlers are always optional, Tapestry can't tell the difference between a misspelling and no event handler at all.


Just wondering if you can't have both: The framework supports plain POJO's, but wouldn't it be possible to provide some optional interfaces for at least the Pages/Components/AppModules, maybe even in a separate jar-file. That way, the people that want type-safety can still have it.



You can also throw in a couple of abstract classes with some convenient methods for replacing existing services from their AppModule without relying on naming-conventions etc.



It might help beginners getting up to speed (when they still need all the compiler/IDE-support they can get) without getting in the way of advanced Tapestry users.

Re: Are pages really POJOs? by Peter Thomas

Anyone whose looked at my code knows that I'm a stickler for types, interfaces and contracts (and I have a tens of thousands of lines of code available to back that statement up. However, Tapestry 5 uses a looser, more adaptive sense of contract than traditional interfaces.


@Howard: yes, for a moment I was surprised that you of all people would start knocking the Java compiler, static-typing and all. I think you have made it clear about the direction Tapestry 5 has taken. I'm not very sure that the pros outweigh the cons for end-users, but thanks for clarifying.



I'm curious whether you considered using annotations to identify the callback methods (I think Spring MVC does something like this now) so developers are free to choose a name and perhaps Tapestry-aware IDE support would be tighter in the future. Or maybe that would have introduced an unwanted explosion of annotations like @OnSuccess @OnPassivate etc?

Re: Are pages really POJOs? by Wim Deblauwe

Why not use an Annotation to mark the onSuccess, etc... methods?


It seems simular to what TestNG did to JUnit. JUnit has the method convention to start all test methods with 'test'. In TestNG, you can use any method name, but you need to annotate with @Test.

Re: Are pages really POJOs? by Onno Scheffers

I'm curious whether you considered using annotations to identify the callback methods


Tapestry 5 already does that. If you use specific method-names Tapestry 5 picks them up, but if you want to use different method-names, you can annotate them.

Re: Are pages really POJOs? by Onno Scheffers

Tapestry does that already. You can use preferred method-names or use your own method-names and then annotate them.
My suggestion would be to provide optional interfaces that specify the preferred method names of all render phases. That way you have type-safety for users that want it, without them having to know the preferred names or the annotations.

Re: Are pages really POJOs? by Peter Thomas

Tapestry 5 already does that. If you use specific method-names Tapestry 5 picks them up, but if you want to use different method-names, you can annotate them.


I did a quick search but could not find any documentation on this. Do you have a link? Can't help thinking now what would happen if you have a valid method and an annotation. Now will some precedence rules kick in or what?

Re: Are pages really POJOs? by Francois Armand

This is clearly written in this documentation : tapestry.apache.org/tapestry5/tapestry-core/gui...

"Each of the orange phases (SetupRender, BeginRender, BeforeRenderBody, etc.) corresponds to an annotation you may place on one or more methods of your class. The annotation directs Tapestry to invoke your method as part of that phase."

Re: Are pages really POJOs? by Francois Armand

Sorry, the precedence's and conflict's management is lower in the same page. Sorry for the misreading.

Mavennize by Doug Ly

It would be nice if you could provide a more mavenized structure for the project, i.e: the java files should be in main/java and template files should be in /main/resource.
This is the recommended structure from what I read on tapestry.apache.org doc.

great article, thanks

Re: Are pages really POJOs? by Igor Drobiazko

The annotation @OnEvent allows you to name your event method however you want. Please read here.


In this example you would do something like:

@OnEvent(value = "success")
public Object foo() {
if (userId == 0) {
userDAO.save(user);
}
return Start.class;
}

Re: Are pages really POJOs? by Peter Thomas

@Francois



Thanks but these appear to be for the component rendering lifecycle. Is there something similar for form processing? Can onSuccess, onActivate etc. be annotation-driven?



Or are you saying I should figure out the appropriate phase for plugging in the onSuccess implementation - say @AfterRender?

Re: Are pages really POJOs? by Peter Thomas

@Igor

Thanks - just saw your post now - I think that answers my question.



From the documentation, it looks like annotations are the 'recommended' approach - maybe the article above should have addressed this?



I can also see that there is a separate page of documentation that explains the rules how the return type of the event handler method controls the navigation.

Annotations vs. Naming Conventions by Howard Lewis Ship

What you are seeing is evolution; the annotations came first, but as I gradually turned on to convention over configuration, I found I preferred the naming conventions to the annotations where ever possible. If I was writing the documentation from scratch, naming conventions would come first and annotations would be more of a footnote.

Re: Annotations vs. Naming Conventions by Peter Thomas

Thanks, I guess that settles the "what would HLS use?" question ;)

Re: Are pages really POJOs? by Francois Armand

@Peter


Ok, so there are different topics in your question on annotation :

  • - components have rendering phase with conventionnal names or annotation as shown before ;

  • - onActivate, onPassivate are a page's topic only, used in page navigation pattern, explain here : href="http://tapestry.apache.org/tapestry5/tapestry-core/guide/pagenav.html. I believe that there is not annotation for these two methods (perhaps some kin of incoherency with the other part)

  • - pages have an other lifecyle linked to the request prossessing, with both annotation or method's name convention for "pageLoaded", "pageAttached", "pageDetached", see tapestry.apache.org/tapestry5/tapestry-core/gui... ;

  • - finally, each component may send events. Event may be caught by an annotated method handlers or conventionnaly name one :

    • - the annotation is: @onEvent(value = "action", component = "select"), where action is the event's name and select the component name

    • - the convention is: onActionFromSelect, with the same component and action name.


    All that is well documented here: tapestry.apache.org/tapestry5/tapestry-core/gui...
    So, for the form component, you can go to it's documentation (tapestry.apache.org/tapestry5/tapestry-core/ref...)and see that it send a bunch of events : prepare, validateForm, etc.



    • I hope it's clearer now, and I think you should spend a little time to read the documentation, to see how the different concepts bring themselves together.


      Francois

    Re: Are pages really POJOs? by Gabriel Kastenbaum

    I guess having some @onactivate and @onpassivate annotations would be interesting.For instance one could add these annotations to the getter and the setter of an Id.
    Maybe one could use the setuprender, to do some some dao/service work.

    @HLS : For what reasons do you prefer naming conventions over annotations?

    Re: Are pages really POJOs? by Howard Lewis Ship

    The structure of Tapestry is such that @OnActivate and @OnPassivate could be added without changing any internal implementations in Tapestry; these would be recognized by a new, contributed ComponentClassTransformWorker. There's an outstanding issue about this (actually, I'd prefer to put an annotation on a field and let Tapestry provide the activate/passivate event handling logic around the field instead). The point is not that I'm lazy, but that Tapestry's extensible architecture means you don't have to wait for the Tapestry team to implement extensions, you're on
    equal footing.


    I prefer naming conventions because I find them more concise and readable than annotations, once you know what to look for.

    Re: Are pages really POJOs? by Gabriel Kastenbaum

    I just imported the tapestry sources in a new eclipse workspace :-)

    Thx Howard

    Messages by Joshua Partogi

    One thing that is still missing from T5 is message display support. AFAIK we still need to implement this by ourself.

    Re: Messages by Howard Lewis Ship

    ${message:key} is used to display messages extracted from the page/component's message catalog.

    its great by zhao ma

    I try it tonight, and its great.

    Client Side Validation by Khaled Habiburahman

    Hi there,
    great article, but one questions: Is it possible to apply the same format for both client and server side validation messages?

    thanks

    Re: Client Side Validation by Howard Lewis Ship

    Tapestry has a perfectly good mailing list, users@tapestry.apache.org. This is not an appropriate (or monitored) forum for asking Tapestry questions.

    Great article, could use some clarification re where to put component stuff by Norris Merritt

    I ran into some strange render-time exceptions which turned out to be project structure issues. The maven archetype doesn't set you up for components, so getting all the layout/css pieces in the right places can be challenging for a newbie. So far I'm very impressed with Tapestry 5 though.

    Great by Meyyappan Muthuraman

    This looks great, I will definitely give a try - vaannila

    Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

    Email me replies to any of my messages in this thread

    Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

    Email me replies to any of my messages in this thread

    34 Discuss

    Educational Content

    General Feedback
    Bugs
    Advertising
    Editorial
    InfoQ.com and all content copyright © 2006-2013 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
    Privacy policy
    BT