Your browser may have trouble rendering this page. See supported browsers for more information.

|<<>>|230 of 273 Show listMobile Mode

Recursive Components in Tapestry

Published by marco on

Updated by marco on

This article was originally published on the Encodo Blogs. Browse on over to see more!


Given a recursive object structure in memory, what’s the best—and most efficient—way to render it with Tapestry? First, let’s define a tiny Java class that we’ll use for our example:

public class DataObject {
  private String name;
  private List<DataObject> subObjects = new ArrayList<DataObject>();

  public String getName() {
    return name;
  }

  public List<DataObject> getSubObjects() {
    return subObjects;
  }
}

Imagine an application has built a whole tree of DataObjects and wants to display them in a page. Since the page doesn’t know how many objects—or nesting levels—there are, it can’t be defined statically. This sounds like the perfect place to use a Tapestry component. Since each component must know about its context object (the DataObject), there must be an instance of the component for each object. This sounds like the perfect place to use recursion.

Let’s take a crack at defining the template for a component named “DataObjectTree”, which has a single property, context, which passes in the object to render[1]:

<span jwcid="@Insert" value="ognl:Context.Name">Context Name</span>
<div jwcid="@If" condition="ognl:Context.SubObjects.size() > 0">
  <div jwcid="@For" source="ognl:Context.SubObjects" value="ognl:DataObject">
    <div jwcid="@DataObjectTree" context="ognl:DataObject"/>
  </div>
</div>    

If it was that easy, you probably wouldn’t be reading this article, as it wouldn’t have been written. However, the Tapestry template parser is going to have extreme difficulties parsing this self-referential template. This value causes a stack overflow:

<div jwcid="@DataObjectTree" context="ognl:DataObject"/>

That’s a shame, but, with help from the blog entry, Recursive Tapestry Components, it’s possible to solve this problem with very little code (though the recursive solution would still be nicer).

Use Blocks to fool Tapestry

Tapestry has two components, Block and RenberBlock. Block defines a “floating” piece of template that is not rendered where it is defined, but is rather rendered in a particular place—or places—in a template by a RenderBlock. The trick boils down to this: replace the recursive call in the component definition with a call to render a block defined in the page. That is, replace the offending line above with the line below:

<div jwcid="@RenderBlock" block="ognl:Page.Components.DataObjectBlock"/>

The DataObjectBlock, in turn, is defined in the page template and includes a DataObjectTree component.

<div jwcid="DataObjectBlock@Block">
  <div jwcid="@DataObjectTree" context="???"/>
</div>

As shown above, there is a slight problem, as we need to pass a context to the nested DataObjectTree component. Since we are once again in the page template, we can’t refer to any properties defined in the component. Therefore, the iterator object, DataObject, used in the recursive (and non-functional) example above, is not available. We’ll have to access it some other way. The other way turns out to be by passing it from the RenderBlock to the Block. A RenderBlock component accepts and stores all properties, so we’ll pass it the context in a “value” property (use any name you like).

<div jwcid="@RenderBlock" block="ognl:Page.Components.DataObjectBlock" value="ognl:DataObject"/>

How can the DataObjectTree retrieve this property? It needs access to the RenderBlock that included its parent Block. That is, with a reference to its surrounding block, it can obtain a reference to the RenderBlock and retrieve the value from it. The code below shows how to declare the Block in the page template.

<div jwcid="DataObjectBlock@Block">
  <div jwcid="@DataObjectTree" block="ognl:Page.Components.DataObjectBlock"/>
</div>

Tapestry will now give each instance of DataObjectTree a reference to the Block instance that encloses it. In order to complete this solution, you’ll have to write a Java class for the DataObjectTree component itself. The component template refers to a Context, which represents the DataObject to display. When displayed from the block, this property is not directly set, so we will have to define code to retrieve it from the appropriate place.[2]

public abstract class DataObjectTree extends BaseComponent {
  public abstract Block getBlock();
  public abstract DataObject getContext();

  public DataObject getBlockContext() {
    if (getBlock() != null) {
      return (DataObject) getBlock().getParameter("value");
    }
    return getContext();
  }
}

If the component’s block property is set, then the component was instantiated from within a block. In that case, the context is retrieved from the block’s parameters (all properties from the initiating RenderBlock are automatically passed to the block as parameters). Otherwise, use the context set by the Context property of the component. Below is the completed template for the component:

<span jwcid="@Insert" value="ognl:BlockContext.Name">Context Name</span>
<div jwcid="@If" condition="ognl:BlockContext.SubObjects.size() > 0">
  <div jwcid="@For" source="ognl:BlockContext.SubObjects" value="ognl:DataObject">
    <div jwcid="@RenderBlock" block="ognl:Page.Components.DataObjectBlock" value="ognl:DataObject"/>
  </div>
</div>

As discussed above, the template now uses the BlockContext instead of the context directly, so it uses the correct DataObject. The page, on the other hand, uses the DataObjectTree component twice, once from the DataObjectBlock (as shown above) and once from the main template, as highlighted below.

<div jwcid="@DataObjectTree" context="Context"/>

<div jwcid="DataObjectBlock@Block">
  <div jwcid="@DataObjectTree" block="ognl:Page.Components.DataObjectBlock"/>
</div>

Note how the instance in the main template gets a Context representing the root of the tree and defined in the page itself. Though not as simple as the intuitive, recursive solution, the “Tapestry Way” doesn’t end up using too much code, though it did take a little while to figure out.

It’s still too complicated!

It’s kind of a shame that the page template has to not only use the component, but declare the block that it uses to render its nodes. Optimally, this part would also go into the component … but that takes us right back to the recursion problem we started with. Also, it’s kind of a shame that a separate component is required. Is there any way around around these two warts?

Using the DataObjectTree recursively is not possible, but Blocks can seemingly be nested as much as needed. In fact, all that seems to be missing is a way to cleanly access the “value” passed to the Block by the RenderBlock. Leaving the definition within a separate component (for now), we can redefine the component HTML as follows:

<div jwcid="@RenderBlock" block="Components.DataObjectBlock" value="Context">

<div jwcid="DataObjectBlock@Block">
  <span jwcid="@Insert" value="ognl:BlockContext.Name">Context Name</span>
  <div jwcid="@If" condition="ognl:BlockContext.SubObjects.size() > 0">
    <div jwcid="@For" source="ognl:BlockContext.SubObjects" value="ognl:DataObject">
      <div jwcid="@RenderBlock" block="ognl:Components.DataObjectBlock" value="ognl:DataObject"/>
    </div>
  </div>
</div>

The entirety of the component’s rendering is contained within a Block, which accesses the current context through the BlockContext. The component’s main content is simply a RenderBlock that renders that block for its Context by passing it as the “value” for the block. Since the block is now defined within the component, it is accessed using Components.DataObjectBlock rather than Page.Components.DataObjectBlock.

The only remaining magic is to implement BlockContext in the component’s Java class. The component retrieves the current instance of the “Node” component out of its component map and returns whatever was set in the “value” parameter. The page uses the getContext() property to pass in the initial context.

public abstract class DataObjectTree extends BaseComponent {
  public abstract DataObject getContext();

  public Object getBlockContext() {
    return ((Block) getComponents().get("Node")).getParameter("value");
  }
}

Now that’s clean! In fact, it’s almost the same as the original recursive solution, but uses the RenderBlock/Block trick to get around Tapestry’s limitation. Though this example is now still defined in a component, you can just move the code and HTML into a page definition. Since the page already has its own context, you only need to copy in the getBlockContext() method. The HTML can be copied directly and voila! A clean solution to recursive structures in Tapestry without defining any new components or using messy kludges.

Optimizing with @InvokeListener

Since the BlockContext is called many times from the component—real-world implementations will also likely need more such properties—, we’d like to set up all necessary data when starting the DataRowBlock. To do this, use the @InvokeListener from the HTML to call a function on the page instance.

public abstract class DataObjectTree extends BaseComponent {
  private Object blockContext;
  public abstract DataObject getContext();

  public Object getBlockContext() {
    return blockContext;
  }

  public void updateBlockContext() {
    blockContext = ((Block) getComponents().get("Node")).getParameter("value");
  }
}

From the HTML template, simply call this function at the beginning of the DataObjectBlock:

<div jwcid="DataObjectBlock@Block">
  <span jwcid="@InvokeListener" listener="listener:UpdateContext"/>
  …
</div>

This is a good pattern to follow to avoid having getters that are too computationally expensive.


[1]

For the non Tapestry-savvy, here are a few tips:

  • ognl indicates that a Object-Graph Navigation Language expression is coming – it’s the mechanism Tapestry uses to script objects from a template. This language understands get/set and can read properties.
  • The @ sign indicates a component type: For is a loop, If is a conditional and Insert adds text to the template. Text before the @ sign is an explicitly named component, which can be referred to by this name elsewhere in the template.

For more information, visit Tapestry’s home page.

[2] Again, for Tapestry novices, properties that are declared abstract are automatically given getters and setters and wired up by Tapestry in a dynamically generated descendent (that’s why the class is abstract as well.

Using Java 1.5 and Tapestry 4.0.2