Further Down the Trail Blog

Version 2



    More About Relationships
    Assumptions That Work for You and Me
    Custom Pages

    In our last article, we got a brief introduction to 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 thecreate-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 <project 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:

    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(); } } 

    Now we need to relate Ingredients toRecipes. 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:

    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; }

    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 Ingredientobjects. 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 thegetIngredients() method one at a time. The@OneToMany annotation is a JSR-220persistence annotation and tells Trails that this property represents a one-to-many relationship from Recipe toIngredient, and thecascade=CascadeType.ALL attribute tells us that all persistence operations performed on the Recipe should also be performed on (or cascaded onto) all of theIngredients in the set. The @JoinColumnannotation is also a JSR-220 persistence annotation that tells Trails that there should be a column on the Ingredienttable, called recipeId, that points back to thisRecipe.

    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 thisRecipe. 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.

    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.

    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:

    @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. 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.

    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 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, orSearch, depending on the kind of page needed. If it can't find a specific page for a given type, it will instead useDefaultEdit, DefaultList, orDefaultSearch, 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:

    OperationClassLook for page:If not found, use page:

    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 aninstructions property to our Recipeclass, like so:

    private String instructions; @PropertyDescriptor(index=6) public String getInstructions() { return instructions; } public void setInstructions(String instructions) { this.instructions = instructions; }

    If we redeploy our application right now, we will see ourinstructions 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 atextarea 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 thecreate-edit-page target of our project'sbuild.xml. We will be prompted to enter the unqualified class name, Recipe.

    Running the Ant target created two new files in thecontext/WEB-INF directory, RecipeEdit.page andRecipeEdit.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:

    <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>

    This template is composed of HTML with several elements that have a special jwcid attribute. The jwcidattributes 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 ObjectFormcomponent 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 theinstructions 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 isinstructions (this is what thejwcid="instructions@Block" attribute means). TheObjectForm 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.

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

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


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

    @Entity @ValidateUniqueness(property="title") public class Recipe { ...

    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


    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!