The Properties of Tapestry Pages

In the previous part of this tutorial we started to build a new project, GuessTheWord. The project is very simple, but we are going to spend a lot of time working on it and experimenting with it. This is because the main aim is to learn a lot about the most basic concepts of Tapestry. For every important concept, I want to show you a number of options as to how it can be implemented and explain which option is good for what.

Today we are going to deal with the properties of Tapestry pages. Properties are very important because they facilitate the exchange of information between different parts of a Tapestry application. Components on the page obtain their values from page properties and then report the changed values back to the page. We also normally use properties to pass information from one page to another and to allow different components to speak to each other.

But before discussing properties, we need to be familiar with one important feature of Tapestry’s architecture.

Tapestry pages and pooling

It is important to remember that Tapestry pages are pooled.

To create a page, we usually extend our new page class from Tapestry’s BasePage, and that class has already plenty of functionality for us to reuse, with quite a number of different properties and methods (if you are curious, have a look at BasePage class in Tapestry API: http://tapestry.apache.org/tapestry4.1/apidocs/index.html).  As a result, creating an instance of a page class is an expensive operation, in terms of computer resources.

To avoid creating a page object every time when yet another of your numerous users requests the page, Tapestry has a pool for every kind of page and stores in that pool some number of instances of the given page class. So there is always a pool for the Home page instances in every application, but our application also has a pool for the Secret page instances.

When a user requests a page, Tapestry takes an instance of that page from the corresponding pool and uses that instance to produce appropriate HTML code and send it to the user’s browser. At some point after that, the page class instance can be sent back to the pool and then reused to serve another client. And when the first client in a few minutes submits a form with their input, some completely different page instance can be used to handle the submission.

It is important to know this detail of Tapestry’s architecture because it will influence the way we shall write our code in a few cases. In particular, this is important for managing page properties properly.

{mospagebreak title=Different ways to manage page properties}

Let’s imagine that we have requested the Home page while working with the already created part of our GuessTheWord application. Tapestry took an instance of the Home page class from the pool and used it to produce an appropriate HTML to be sent to our browser. In this process, the page class has checked whether there is some initial value to display in the secretWord text box (implemented as a TextField component). For this, the getTheWord() method of the page class was invoked.

We have trusted Tapestry to create and maintain theWord property by writing an abstract getter method like this:

public abstract String getTheWord();

Because of this, theWord property will be initially always set to null, so the empty text box will be displayed, or exactly what the creators of the Web application wanted.

But let’s imagine that we have created the same property in the traditional Java way, by writing a private class member and accessor methods for it ourselves:

private String theWord;

 

public String getTheWord() {

   return theWord;

}

 

public void setTheWord(String theWord) {

   this.theWord = theWord;

}

Say we have played with the application and tried to guess the secret word. Every time we submitted the form with a version of the secret word entered into the secretWord component, an instance of the Home page class on the server side was used to serve our submission. The setTheWord() method of that page class was invoked to store our word in its private property. Then some code (yet to be written) was used to define whether the word is correct or not and to act accordingly.

After a few hundred attempts we have finally managed to guess the secret word and left the application, to do something else equally exciting. The Home page instance we dealt with the last time still stores the word we have guessed in its private theWord member. It goes to its pool to rest and wait until another user comes try and guess the secret word.

Say Tapestry chooses to serve the new user that same Home page instance we just left, with the secret word stored in it. As usual, when producing HTML to be sent to the user, the page class will check whether components have any initial values. For the secretWord component, it will invoke the getTheWord() method, and the method will happily return that same secret word we have spent so much effort to guess.

So the next user will see the Home page with the secret word already entered in the text box. It will be masked, yes, because we have set the hidden binding for the secretWord component to true, but for the new user it will be enough to press the Submit button to see the sacred wisdom of the Secret page. Is that fair? I don’t think so.

What we actually need to do is to set theWord property to its default value, null, every time an instance of the Home page is sent to the pool. This is exactly the additional functionality Tapestry takes care of when we trust it to create a property for us (by providing only an abstract getter method). When Tapestry sees the abstract getter, it automatically creates an appropriate private member of the page class, the default concrete getter and setter methods for it and some other code needed to set the property back to its default value every time the page class instance is sent to its pool.

So you can see that it makes a lot of sense to trust Tapestry with creating page properties for us. There can be, however, cases when we have to do all the work ourselves.

{mospagebreak title=Resetting page properties by hand}

When Tapestry creates a page property automatically, it creates basic getter and setter methods that do nothing but retrieve or set the value of the property. But sometimes we might want to put some functionality into the getter and/or setter methods. In the GuessTheWord application, for example, we might want to do something depending on the length of the word submitted by the user and write a setter like this:

public void setTheWord(String theWord) {

  

   if (theWord != null) {

       int length = theWord.length();

      

       // Do something…

   }

 

   this.theWord = theWord;

}

This might be not be a very convincing example, but believe me, sometimes you just cannot avoid writing some code in a getter or a setter. And as soon as you have provided a getter or a setter of your own, you have to write all the remaining code yourself. So Tapestry will not take care of setting theWord to null; you’ll need to do that yourself too.

In Tapestry 3, page classes have a very convenient method for this purpose, initialize(). In Tapestry 4, this method still exists but it is deprecated. So it is not recommended that we use it, and I should not be mentioning it here. But still, this method is more convenient than the alternative solution and I have to admit that I use the deprecated initialize() method in my work.

It looks like this:           

protected void initialize() {

   theWord = null;

}

As soon as we have provided this method in our page class, it will be called every time the page instance is sent to the pool, so we have achieved what we wanted.

The alternative approach, recommended for use in Tapestry 4, is to implement the PageDetachListener interface and its only method pageDetached(PageEvent event). So if you do everything properly, your Home class will look like this:

public abstract class Home extends BasePage implements PageDetachListener {

 

    private String theWord;

 

    private void setTheWord(String theWord) {

          this.theWord = theWord;

    }   

 

    public String getTheWord() {

        return theWord;

    }

 

    public void pageDetached(PageEvent event) {

        theWord = null;

    }

   

    public String onWordSubmit() {

        return "Secret";

    }

}

For me, it’s just the same result with more coding, but of course the creators of Tapestry know better.

{mospagebreak title=Configuring a property in page specification}

There is yet another way to configure a page property that you might find useful in some cases. Say you want to have a property, but you are not going to read or set the value of that property in your Java code, in the page class. This can happen when the property is going to be used exclusively by some components on the page, so it can be wired to those components within the page specification without writing any Java code at all.

All you need to do is define the property in the page specification:

<property name="theWord"/>

And then you can reference this property in the bindings of your components, exactly as we do now in the secretWord component binding.

Having found this <property> element in the page specification, Tapestry will act exactly as it does when we’ve provided an abstract getter method: it will create a private property, public getter and setter and will also reset the property to its default value before sending the page to its pool.

All right, but what exactly is this “default value"? According to the rules of Java, any object, including String, will be set to null, while any numeric variable will be sent to zero. But what if you want to have an initial value different from the default one? Say in the GuessTheWord application you want the text box to be not empty, but to display some prompt when the Home page is initially shown to a user.

{mospagebreak title=Setting an initial value}

Let’s experiment with our current project. First of all, comment out the hidden binding of the secretWord component, so that we are able to see the initial value, perhaps like this:

<component id="secretWord" type="TextField">

    <binding name="value" value="theWord"/>

    <!–binding name="hidden" value="true"/–>

</component>

If you made any changes to how theWord property is configured, please revert now to the most convenient (and most often used) approach, an abstract getter. Now, if we want to give this property an initial value, we can use a relatively new feature of Java – an annotation. The result will look like this:

@InitialValue("literal:enter a word")

public abstract String getTheWord();

Simple and elegant, isn’t it? Run the project, and you should see something similar to the following screen shot:

 

Note that by default, the value inside of the double quotes inside the annotation is an OGNL expression. This means you can have a method providing an initial value for your property. Try something like this:

@InitialValue("initialValue")

public abstract String getTheWord();

   

public String getInitialValue() {

    return "enter a word";

}

Run the project, and it should work exactly as it did before.

But what if we wanted to configure this property in the page specification only? Let’s try this. Comment out the above abstract getter, its annotation and the getInitialValue() method (if you created it). Instead, add the following line of XML to the Home page specification:

<property name="theWord" initial-value="literal:enter a word"/>

Again, run the project, and it should work exactly as before.

Finally, if you had to configure a property by hand, here is one possible approach to assigning it an initial value:

public String getTheWord() {

   

    if (theWord == null) theWord = “enter a word";

  

    return theWord;

}

 

However, the more fundamental way to initialize a property is to override the finishLoad() method of the BasePage class:

protected void finishLoad() {

    theWord = “enter a word";

}

This method will be invoked only once, when the page is loaded for the first time.

All right, this seems to be everything I wanted to tell you about the properties of Tapestry pages for now. We have made some changes to the GuessTHeWord project, but that was for learning purposes only. You might want to revert the code to its initial state, undoing any changes we have made in this part of tutorial.

What comes next

In the next part, we are going to take a more detailed look at listener methods. They can be written in a few different ways and we are going to decide when each of the approaches can be useful for us, thus gradually expanding our Tapestry toolbox.

If the space will allow, we might also try different ways of submitting a form, but on the other hand, this might be delayed until a later part.

See you in a week.

[gp-comments width="770" linklove="off" ]

chat