Skip to main content

Further Down the Trail

November 4, 2005

{cs.r.title}









Contents
More About Relationships
Assumptions That Work for You and Me
Custom Pages
Validation
Conclusion
Resources

In our "http://today.java.net/pub/a/today/2005/06/23/trails.html">last
article
, we got a brief introduction to "https://trails.dev.java.net/">Trails, a framework that aims to
bring a drastic productivity increase to Java web application
development. We quickly created a simple application and saw how
easy it is to get started, but we didn't have time to cover a lot
of the more interesting features of Trails. To steal a great quote
from Larry Wall, the inventor of Perl, Trails is designed "to make
easy things easy and hard things possible." I hope I convinced you
last time that easy things are indeed easy. Now it's time to look
at some of the features that allow you to build a real
application.

In this article, we are going to pick up where we left off
building our recipe management application. But first we need to
download the latest version of Trails, version 0.8. Next, we need
to upgrade our application to use this new version of Trails.
Trails provides an upgrade-project target that will do
this for us. It will prompt us for the same information as the
create-project target, namely a base directory and
project name. Enter the same information as you used to create your
earlier application, and Trails will:

  1. Move your existing project to name>.old.

  2. Create a new project of the same name.

  3. Copy your project's source code into the newly created
    project.

The outcome of all this should be that we have our familiar
recipe application running in Trails 0.8. To test this, you can run
the redeploy target and visit your application with a browser.

More About Relationships

When we last left our humble application, we had a recipe page
with a few simple properties and a category. No recipe is useful
without a list of ingredients, so let's add that to our application
next. As before, we will start with a new domain object,
Ingredient:

[prettify]package org.demo;

import javax.persistence.Entity;
import javax.persistence.GeneratorType;
import javax.persistence.Id;

import org.apache.commons.lang.builder.EqualsBuilder;

@Entity
public class Ingredient
{

    private Integer id;
    
    private String amount;
    
    private String name;

    public String getAmount()
    {
        return amount;
    }

    public void setAmount(String amount)
    {
        this.amount = amount;
    }

    @Id(generate=GeneratorType.AUTO)
    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }
    
    public boolean equals(Object obj)
    {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

    public String toString()
    {
        return getAmount() + " " + getName();
    }
}
[/prettify]

Now we need to relate Ingredients to
Recipes. In the previous article, we saw an example of
a many-to-one relationship, but here we have the inverse. Recipes
(usually) have multiple ingredients, so this is a one-to-many
relationship from Recipe to Ingredient.
Here is how we do it:

[prettify]    private Set<Ingredient> ingredients = new HashSet<Ingredient>();
    
    @OneToMany(cascade=CascadeType.ALL)
    @JoinColumn(name="recipeId")
    @Collection(child=true)
    public Set<Ingredient> getIngredients()
    {
        return ingredients;
    }

    public void setIngredients(Set<Ingredient> ingredients)
    {
        this.ingredients = ingredients;
    }
[/prettify]

The first thing you will probably notice is that we are using
Java 5 generics. In this case generics help us out in more ways
than just type safety and eliminating casts, as they give Trails
information it can use. Specifically, Trails knows that the
ingredients set is going to contain Ingredient objects. Although it
might be possible to figure this out based on the property name,
with a Set<Ingredient> instead of
Set, we don't have to guess.

Now let's look at the annotations on the getIngredients() method
one at a time. The @OneToMany annotation is a "http://www.jcp.org/en/jsr/detail?id=220">JSR-220 persistence
annotation and tells Trails that this property represents a one-to-many relationship from Recipe to
Ingredient, and the
cascade=CascadeType.ALL attribute tells us that all
persistence operations performed on the Recipe should also be
performed on (or cascaded onto) all of the Ingredients in the set. The
@JoinColumn annotation is also a JSR-220 persistence
annotation that tells Trails that there should be a column on the
Ingredient table, called recipeId, that
points back to this Recipe.

The final annotation, @Collection(child=true), is
specific to Trails and bears a little more explanation. One-to-many
relationships in Trails can be of two types: child relationships
and non-child relationships. Child relationships are those in which
objects only exist in the context of their parent object. If this
were not a child relationship, Trails would allow us to choose from
a list of all of the Ingredient instances for all
recipes. This really isn't what we want. Because we have defined
this as a child relationship, Trails will use an editor that lets
us create Ingredient objects specific to this
Recipe. Run the redeploy target once
again. Fire up your web browser and let's see what we did.

First, create a new recipe and click the Apply button, as in
Figure 1:

Editing a new recipe
Figure 1. Editing a new recipe

Next, click the Add New button to add an ingredient to our
recipe, like Figure 2.

<br "The Add Ingredient Page" />
Figure 2. The Add Ingredient page

Finally, click the OK button to show the recipe with the new
ingredient. This is illustrated in Figure 3.

<br "A recipe with an ingredient" />
Figure 3. A recipe with an ingredient

Assumptions That Work for You and Me

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.

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:

[prettify]    @PropertyDescriptor(index=2)
    public String getDescription()
    {
        return description;
    }
[/prettify]

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:

[prettify]    @PropertyDescriptor(index=3,format="MM/dd/yyyy",
        displayName="First Cooked On")
    public Date getDate()
    {
        return date;
    }
[/prettify]

There's also quite a few other things you can specify with
@PropertyDescriptor. A full list of attributes can be
found in the "http://trails.dev.java.net/apidocs/overview-summary.html">Javadoc.
With just a few annotations, we can customize our application quite
a bit. When we run the redeploy target and create a recipe, as
shown in Figure 4, we see our properties listed in a more
appropriate order, our dates formatted much more nicely, and our
customized label.

<br "Recipe with annotations applied" />
Figure 4. Recipe with annotations applied

Custom Pages

Annotations are a great way to give Trails more hints about what
you want to happen. But what if you want to yank the steering wheel
away and have complete control over what's going on? Well, Trails
has you covered there, too. Before we see how this works, we need to
take a moment to talk about how Trails produces the oh-so-lovely
pages you have thus far been feasting your eyes upon.

As mentioned in the previous article, Trails uses "http://jakarta.apache.org/tapestry/">Tapestry as its web
framework, so all Trails pages are actually Tapestry pages. 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 pages allow 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 see how this all works, let's go back to our application. We
now have a list of ingredients, but it would be nice to tell our
poor chef what to do with them. To do that, let's add an
instructions property to our Recipe class,
like so:

[prettify]    private String instructions;

    @PropertyDescriptor(index=6)
    public String getInstructions()
    {
        return instructions;
    }

    public void setInstructions(String instructions)
    {
        this.instructions = instructions;
    }
[/prettify]

If we redeploy our application right now, we will see our
instructions property appear on the page as a text
field. It would be nicer to have this be a text area. To do this,
we'll create a custom page and override the display of the
instructions property. (It is also possible to make this field a
textarea by specifying a Hibernate @Column annotation
with length greater than 100, but if I did it that way, I wouldn't
be able to show you how to do a custom page.) Creating our custom
page is done by using the create-edit-page target of
our project's build.xml. We will be prompted to enter the
unqualified class name, Recipe.

Running the Ant target created two new files in the
context/WEB-INF directory, RecipeEdit.page and
RecipeEdit.html. The .page file is a Tapestry
configuration file and isn't really too interesting. We want to
look at the RecipeEdit.html file. This is the page template, in
Tapestry parlance, and is where we will make our changes. Here's
what it looks like:

[prettify]&lt;span jwcid="@Border"&gt;
    &lt;div id="header"&gt;
        &lt;a href="#" jwcid="@trails:ListAllLink" 
            typeName="ognl:model.class.name" /&gt;
        &amp;nbsp;
        &lt;a href="#" jwcid="@PageLink" page="Home"&gt;Home&lt;/a&gt;
    &lt;/div&gt;
        &lt;h1&gt;&lt;span jwcid="@Insert" value="ognl:title" /&gt;&lt;/h1&gt;
    &lt;div jwcid="@Conditional" class="error" 
        condition="ognl:delegate.hasErrors" element="div"&gt;
        Error: &lt;span jwcid="@Delegator"
                delegate="ognl:delegate.firstError" /&gt;
    &lt;/div&gt;
    &lt;form jwcid="@trails:ObjectForm" model="ognl:model" 
        class="detail"&gt;
        
        &lt;/form&gt;

&lt;/span&gt;
[/prettify]

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:

[prettify]&lt;form jwcid="@trails:ObjectForm" model="ognl:model"
    class="detail"&gt;
    &lt;div jwcid="instructions@Block"&gt;
        &lt;label&gt;Instructions&lt;/label&gt;
        &lt;span class="editor"&gt;
          &lt;textarea jwcid="@TextArea"
            value="ognl:model.instructions" /&gt;
        &lt;/span&gt;
        &lt;br/&gt;
    &lt;/div&gt;
&lt;/form&gt;
[/prettify]

Inside of our ObjectForm component, we are adding a
Block component whose id is
instructions (this is what the
jwcid="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 whose
id 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.

Run the redeploy target again. Figure 5 shows the final
result.

Instructions in a text area
Figure 5. Instructions in a text area

Validation

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.

[prettify]    @PropertyDescriptor(index=1)
    @NotNull
    public String getTitle()
    {
        return title;
    }
[/prettify]

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, like in Figure 6.

Title is required
Figure 6. Title is required

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:

[prettify]@NotNull(message="is required")
[/prettify]

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. We can add it our
Recipe class to specify that all Recipes
must have a unique title.

[prettify]@Entity
@ValidateUniqueness(property="title")
public class Recipe
{
...
[/prettify]

When we try to create a second recipe with the same title, we
will again get a user-friendly error message, as shown in Figure
7:

Title must be unique
Figure 7. Title must be unique

Conclusion

Phew, we've covered a lot of ground in this article. We've learned
more about how Trails supports various types of relationships in
your domain model. We've learned how to customize Trails in terms
of both appearance and behavior. And we've seen how easy it is to
add validation to our domain model, as well. But as the Cat in the
Hat said, "That is not all. Oh no, that is not all!" Trails
continues to evolve and mature as we march towards our 1.0 release
(hopefully this year). So stay tuned!

Resources

width="1" height="1" border="0" alt=" " />
Chris Nelson is the founder of the Trails project and has been developing server-side Java applications since 1997.
Related Topics >> Programming   |   Web Development Tools   |