I started out implementing a Tapestry component library to find out if it really was that easy and ended up learning
that it is. On the way I also found out that Tapestry does not (yet?) support recursive components but the effect can be
achieved by using Block and RenderBlock components.
In the spirit of the Tapestry Inspector page I wanted to implement an inspector for browsing the contents of a Spring ApplicationContext in a running Tapestry application. I designed the page using css and javascript so that any nested structure in the Spring bean definitions can be expanded and collapsed. The page does not depend on external stylesheets or javascript so the page contents can be saved to a file and viewed offline. I've uploaded a
sample from
my own demonstration application.
The SpringInspectorPage is packaged as a Tapestry component library and is pretty easy to use, add the
JAR file to
the lib folder of the application and declare the library in the Tapestry application specification:
<library id="springinspector"
specification-path="/com/mjhenderson/tapestry/springinspector/springinspector.library"/>
Then link to the page via a listener method so that you can configure the page with the Spring ApplicationContext
before activating it:
public void showSpringInspector(IRequestCycle cycle) {
IPage page = cycle.getPage("springinspector:SpringInspectorPage");
SpringInspectorPage inspector = (SpringInspectorPage)page;
inspector.setContext(this.getUsersEngine().getApplicationContext());
cycle.activate(inspector);
}
A Spring ApplicationContext may contain bean definitions nested to some arbitrarily deep level via <bean> declarations
in the configuration file (check out the 'dummy' bean definition in the sample page above). Within nested beans there
may be Property, Map or List declarations for bean properties or constructor arguments which may themselves contain
still more bean definitions. The natural way to express this structure would be to use recursive component declarations in
my inspector components with the bean definition component referencing the Map or List display components which in turn would refer back to the bean definition display component. That's exactly what I did for my first attempt and found out that
Tapestry, while loading the page and resolving components, reads the declaration and template for every component not already
loaded. It resolves components in this way as it encounters them in the page and component definitions and any recursive reference to the component while it is being loaded causes the template and specification to be loaded again, since the component is not yet loaded! This results in an eventual Stack overflow or out of memory exception as Tapestry stacks up
loading of the same component over and over again.
Thanks to the tapestry-users mailing list I was referred to the Block and RenderBlock components, part of the basic
Tapestry distribution. Block is a component that refuses to render it's body. Blocks can be placed anywhere, around anything
in a component template. RenderBlock is a component that renders the body of a Block. The Block can be defined in a separate page or component, but the component that uses RenderBlock must resolve the Block at runtime to pass as the value for the "block" parameter to RenderBlock. RenderBlock accepts informal parameters. These can be accessed at runtime by referencing the bindings of the Block's inserter property.
In my case I declared my Block components in the page template, passing the block to my display components via a block
parameter:
<span jwcid="mapBlock@Block">
<span jwcid="@MapValue" context="ognl:page.context"
block="ognl:page.components.mapBlock">Map Value</span>
</span>
<span jwcid="listBlock@Block">
<span jwcid="@ListValue" context="ognl:page.context"
block="ognl:page.components.listBlock">List Value</span>
</span>
The blocks are referenced via RenderBlock delcarations elsewhere in the code(example from MapValue.html):
<td>
<span jwcid="@RenderBlock" block="ognl:getBlockByValueType(value.get(object))"
value="ognl:value.get(object)">
</span>
</td>
And choose the block that displays the type of data in the graph:
public Block getBlockByValueType(Object value) {
String blockName = "simpleBlock";
if (value instanceof Properties) {
blockName = "propertiesBlock";
} else if (value instanceof List) {
blockName = "listBlock";
} else if (value instanceof Set) {
blockName = "setBlock";
} else if (value instanceof Map) {
blockName = "mapBlock";
} else if (value instanceof RootBeanDefinition) {
blockName = "beanBlock";
} else if (value instanceof RuntimeBeanReference) {
blockName = "beanRefBlock";
}
return (Block)getPage().getComponent(name);
}
Each of the components for displaying different types of context object is inside a Block component in the inspector's page component.
Some of these components contain nested RenderBlock components which are used to render
the correct Block according to some value inside the context, For example the MapValue component, inside the mapBlock
may have to render a value in the map that is a Spring Bean definition, or a list, an id reference, or even another Map.
For this to work the value to be displayed by the Block must be passed into the RenderBlock. The RenderBlock is set as the inserter property of the Block it is rendering. The value parameter is visible as a binding on the Block's inserter.
So the components have to access the inserter property of the Block they are wrapped by.
Since all of my display components were defined with a "block" parameter which was passed in the component declaration in the page, the components can access the block and retrieve the Block's inserter property value, which will
be the RenderBlock which is currently rendering the component and from the inserter retrieve the informal parameters passed
to the RenderBlock component:
public abstract Block getBlock();
public Object getValue() {
return getBlock().getInserter().getBinding("value").getObject();
}
This procedure: declare Block components in the page and RenderBlock components in the components wrapped by the Block
allows one to create a display of the content of the Spring context of arbitrary depth.
It took a while to 'grok' the Block and RenderBlock components and to get the access to the informal parameters working.
Once again, I'm pleased with how little Java code it takes to implement components with Tapestry. The
Source X-Reference shows only 5 simple classes and one of those is a Dummy class for testing the inspector's display of
nested bean property declarations.
A source archive for this component library is available.
Posted: Fri - July 30, 2004 at 09:06 AM
|