Persistent page properties

Servlets, and by extension, JavaServer Pages, are inherently stateless. That is, they will be used simultaneously by many threads and clients. Because of this, they must not store (in instance variables) any properties or values that are specified to any single client.

This creates a frustration for developers, because ordinary programming techniques must be avoided. Instead, client-specific state and data must be stored in the HttpSession or as HttpServletRequest attributes. This is an awkward and limiting way to handle both transient state (state that is only needed during the actual processing of the request) and persistent state (state that should be available during the processing of this and subsequent requests).

Tapestry bypasses most of these issues by not sharing objects between threads and clients. Tapestry uses an object pool to store constructed page instances. As a page is needed, it is removed from the page pool. If there are no available pages in the pool, a fresh page instance is constructed.

For the duration of a request, a page and all components within the page are reserved to the single request. There is no chance of conflicts because only the single thread processing the request will have access to the page. At the end of the request cycle, the page is reset back to a pristine state and returned to the shared pool, ready for reuse by the same client, or by a different client.

In fact, even in a high-volume Tapestry application, there will rarely be more than a few instances of any particular page in the page pool.

For this scheme to work it is important that at the end of the request cycle, the page must return to its pristine state. The prisitine state is equivalent to a freshly created instance of the page. In other words, any properties of the page that changed during the processing of the request must be returned to their initial values.

The page is then returned to the page pool, where it will wait to be used in a future request. That request may be for the same end user, or for another user entirely.

[Note]Importance of resetting properties

Imagine a page containing a form in which a user enters their address and credit card information. When the form is submitted, properties of the page will be updated with the values supplied by the user. Those values must be cleared out before the page is stored into the page pool ... if not, then the next user who accesses the page will see the previous user's address and credit card information as default values for the form fields!

Tapestry separates the persistent state of a page from any instance of the page. This is very important, because from one request cycle to another, a different instance of the page may be used ... even when clustering is not used. Tapestry has many copies of any page in a pool, and pulls an arbitrary instance out of the pool for each request.

In Tapestry, a page may have many properties and may have many components, each with many properties, but only a tiny number of all those properties needs to persist between request cycles. On a later request, the same or different page instance may be used. With a little assistance from the developer, the Tapestry framework can create the illusion that the same page instance is being used in a later request, even though the request may use a different page instance (from the page pool) ... or (in a clustering environment) may be handled by a completely different server.

Each persistent page property is stored individually as an HttpSession attribute. A call to the static method Tapestry.fireObservedChange() must be added to the setter method for the property (as we'll see shortly, Tapestry can write this method for you, which is the best approach). When the property is changed, its value is stored as a session attribute. Like the Servlet API, persistent properties work best with immutable objects such as String and Integer;. For mutable objects (including List and Map), you must be careful not to change the internal state of a persistent property value after invoking the setter method.

Persistent properties make use of a <property-specification> element in the page or component specification. Tapestry does something special when a component contains any such elements; it dynamically fabricates a subclass that provides the desired fields, methods and whatever extra initialization or cleanup is required.

You may also, optionally, make your class abstract, and define abstract accessor methods that will be filled in by Tapestry in the fabricated subclass. This allows you to read and update properties inside your class, inside listener methods.

[Tip]Define only what you need

You only need to define abstract accessor methods if you are going to invoke those accesor methods in your code, such as in a listener method. Tapestry will create an enhanced subclass that contains the new field, a getter method and a setter method, plus any necessary initialization methods. If you are only going to access the property using OGNL expressions, then there's no need to define either accessor method.

[Note]Transient or persistent?

Properties defined this way may be either transient or persistent. It is useful to define even transient properties using the <property-specification> element because doing so ensures that the property will be properly reset at the end of the request (before the page is returned to the pool for later reuse).

Example 4.3. Persistent page property: Java class

package mypackage;

import org.apache.tapestry.html.BasePage;
	
public abstract class MyPage extends BasePage
{
    abstract public int getItemsPerPage();
	
    abstract public void setItemsPerPage(int itemsPerPage);
}

Example 4.4. Persistent page property: page specification


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC 
  "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
	
<page-specification class="mypackage.MyPage">

  <property-specification
    name="itemsPerPage"
    persistent="yes"
    type="int" initial-value="10"/>

</page-specification>

Again, making the class abstract, and defining abstract accessors is optional. It is only useful when a method within the class will need to read or update the property. It is also valid to just implement one of the two accessors. The enhanced subclass will always include both a getter and a setter.

This exact same technique can be used with components as well as pages.

A last note about initialization. After Tapestry invokes the finishLoad() method, it processes the initial value provided in the specification. If the initial-value attribute is ommitted or blank, no change takes place. Tapestry then takes a snapshot of the property value, which it retains and uses at the end of each request cycle to reset the property back to its "pristine" state.

[Warning]Warning

The previous paragraph may not be accurate; I believe Mindbridge may have changed this behavior recently.

This means that you may perform initialization for the property inside finishLoad() (instead of providing an initial-value). However, don't attempt to update the property from initialize() ... the order of operations when the page detaches is not defined and is subject to change.