Introduction
Note that for this part of the guide, we've shamelessly copied material from Chris Nelson's original article.
Remember that Trails provides built-in support for CRUD (Create, Read, Update, Delete) operations, so techniques for customizing each differs. For customizing how the object gets displayed and what parts of it are editable and how, you commonly use annotations, and for customizing the UI, like layout etc. you'd commonly modify a Tapestry page. For the latter type of customization, nce you get the hang of customizing a Trails page and understand that you are dealing with highly generalized concepts (such a domain model), you realize that you are simply using Tapestry rather any Trails magic.
The creator of the framework says it better:
Trails makes a lot of assumptions from your domain model in order to produce a working application. Railsers call this "convention over configuration." It's a great idea, and one I proudly acknowledge borrowing. Of course, assumptions are wonderful when they are right--but sometimes they're wrong. Fortunately, Trails provides a great deal of flexibility in overriding the assumptions that it makes. Let's explore the many ways we can customize our Trails application.
Annotating properties with descriptors
Chris' on the roll, so let the man continue:
We'll start with the Recipe class. While functional, it's easy to think of several cosmetic adjustments we would like to make. First off, we'd like our properties to be in a different order. To do this, we will use the simplest mechanism for customization: our good friend, the Java 5 annotation. Trails provides several custom annotations for us to tell it information it can't guess on its own, or that it would guess incorrectly. One of the things that Trails cannot guess, perhaps surprisingly, is the order in which you want your properties to appear on the page. The reason for this is that there is no way at runtime to determine the order in which methods or fields appear in the source code. But it's easy to specify property order using the handy dandy @PropertyDescriptor annotation.
Here's what it looks like:
@PropertyDescriptor(index=2)
public String getDescription()
{
return description;
}
A couple of other very useful things we can specify with @PropertyDescriptor are the label and formatting of our properties. The displayName attribute lets specify a label that is different from the "un-camelcased" property name, while the format attribute lets us specify any format string that the Java format objects can understand. Here is what they look like in action:
@PropertyDescriptor(index=3,format="MM/dd/yyyy", displayName="First Cooked On") public Date getDate() { return date; }
There's also quite a few other things you can specify with @PropertyDescriptor. A full list of attributes can be found in the Javadoc. With just a few annotations, we can customize our application quite a bit.
Customized Trails pages
If you don't customize anything, Trails uses default pages. In Tapestry, a component (a page is a component as well) is comprised of three parts: a html template, a page specification and a java class. This is not a Tapestry User Guide, so read more about it on an appropriate place. With 4.1.x Tapestry you may also substitute the page spec with annotations, and Trails is moving to that direction as well. There's a default page for a particular operation, such as: DefaultEdit, DefaultList and DefaultSearch with both a .page and .html file that you'll find in you /WEB-INF directory of a Trails application. The cool thing about this is that you can also customize the default look and feel of a page for an operation type. Most of the cases, however, you want to customize a page specific for a particular object. Once again, it's hard to say it better than Chris did:
There are three kind of pages in Trails: Edit pages allow us to edit an instance of an object, List pages allow us to view a list of instances, and Search pagesallow us to enter search criteria. Trails makes decisions about what page to display based on which kind of page is needed and the class of the object(s) involved. It will first look for a page using the unqualified-type name concatenated with Edit, List, or Search, depending on the kind of page needed. If it can't find a specific page for a given type, it will instead use DefaultEdit, DefaultList, or DefaultSearch, respectively. These three pages were created for us automatically when we created our Trails application. The following table gives some examples of how Trails figures out which page to use:
| Operation | Class | Look for page: | If not found, use page: |
|---|---|---|---|
| Edit | org.trails.demo.Recipe | RecipeEdit | DefaultEdit |
| List | com.foo.Product | ProductList | DefaultList |
| Search | org.wwf.animal.Gazelle | GazzelleSearch | DefaultSearch |
What this all means is that we have fine-grained control over the appearance and functioning of our application. By changing the default pages we can change the entire application. We can also create a pages that will only affect a specific class. And we can even customize at the property level.
To start customizing a page, make a copy of the appropriate default page files and rename according the the table above. Most of the UI customization happen in the corresponding .html template. Keep in mind that a Tapestry template is just html page with selected tags marked with jwcid that will be replaced by Tapestry components (which can be comprised of other templates and so forth). Take a look at the example below:
<span jwcid="@Border"> <div id="header"> <a href="#" jwcid="@trails:ListAllLink" typeName="ognl:model.class.name" /> <a href="#" jwcid="@PageLink" page="Home">Home</a> </div> <h1><span jwcid="@Insert" value="ognl:title" /></h1> <div jwcid="@Conditional" class="error" condition="ognl:delegate.hasErrors" element="div"> Error: <span jwcid="@Delegator" delegate="ognl:delegate.firstError" /> </div> <form jwcid="@trails:ObjectForm" model="ognl:model" class="detail"> </form> </span>
Chris says:
This template is composed of HTML with several elements that have a special jwcid attribute. The jwcid attributes tell Tapestry that an HTML element corresponds to a component and will get replaced at runtime. The component we are most interested in is the trails:ObjectForm component represented by the form HTML tag at the bottom of the template. ObjectForm is a component whose job is to display an edit form for an object. The model attribute tells the component which object to display; in this case, the model property of the page (which will be a Recipe instance). ObjectForm will then interact with Trails to find all of the information it needs to build an appropriate UI for the object it receives.
If we wanted to, we could replace the ObjectForm component entirely and create a new form from scratch using standard Tapestry components. However, this would be work than we would like; after all, we really only want to change the instructions property. This is where Trails property-level overrides become useful. We can add a component to our template that tells Trails, "Use this block to replace what you would normally produce to edit the instructions property." This is how we do it:
<form jwcid="@trails:ObjectForm" model="ognl:model" class="detail"> <div jwcid="instructions@Block"> <label>Instructions</label> <span class="editor"> <textarea jwcid="@TextArea" value="ognl:model.instructions" /> </span> <br/> </div> </form>
Inside of our ObjectForm component, we are adding aBlock component whose id is instructions (this is what thejwcid="instructions@Block" attribute means). The ObjectForm component, when it renders an editor for each editable property in its model object, will first to check to see if there is a Block component whoseid is the name of the current property. If so, it will delegate to the Block rather than using the default editor. This lets us override just the properties we want, and let Trails give us default editors for the other properties.
So when Trails renders and edit page for an object, it creates a list of the object's properties and renders an appropriate editor for each one. In the case above, Chris happens to know there's a TextArea component available in standard Tapestry. For extensive component reference, see Tapestry documentation. By all means, you can also supply your own editor for a particular property. If you are interested in that, see Adding Custom Editor.
Customizing ObjectTable component.
To override how an ObjectTable column shows, you need to suffix the name of the block with 'ColumnValue'.
<table jwcid="@trails:ObjectTable" criteria="ognl:criteria" object="ognl:object"> <div jwcid="urlColumnValue@Block"> <img jwcid="@Any" src="ognl:object.url"/> </div> </table>
One step further.
You can also customize the pages java side. Just extend from ListPage or from EditPage according to your needs (remember to keep your derived class "abstract"). Then just change the the "page-specification" in you Custom[List|Edit].page to point to your newly created Custom[List|Edit]Page.java
i.e:
<page-specification class="com.mycompany.pages.CustomEditPage"> .... </page-specification>
Validation
There's nothing we can add to Chris' explanation:
Trails takes the approach that validation should be specified in your domain object, and makes it easy to do so. In keeping with the theme of not reinventing wheels, Trails leverages the excellent validation annotations that have recently been added to the Hibernate annotations project. Trails integrates them into the error reporting system provided by Tapestry, with the net result being that adding validation to your domain object takes about as long as you've just spent reading this paragraph.
Let's see it in action by making the title property of our Recipe class required.
@PropertyDescriptor(index=1) @NotNull public String getTitle() { return title; }
That's all there is to it. The @NotNull annotation tells Trails (and Hibernate) to make this a required field. When we try to create a recipe with no title, we get a useful error message.
And if we don't like the default message, it's easy to change that, too. All of the Hibernate validation annotations allow a message attribute, so we can change our @NotNull annotation like so:
@NotNull(message="is required")
There are quite a few validation annotations provided by Hibernate. Writing your own is outside of the scope of this article, but not at all difficult to do.
Finally, there is one more validation annotation that is specific to Trails: @ValidateUniqueness. Unlike the Hibernate validation annotations, it is declared on a class instead of a property, and allows us to specify that objects of this class must be unique by a given property.


