Friday, September 14, 2007

Grapestry view and controller resolvers

This is a posting that I had started quite a while ago (right when I was starting w/ Grapestry), and I never posted it. Anyway, I thought that the content might be useful to someone trying to use hivemind with Tapestry 4. It would be very interesting when I will get a chance to try the same using the new and shiny Tapestry 5.


The first couple of steps from the Grapestry wish list is to make the templates and pages be located in the same spot where the standard Grails gsp-s and controllers are (e.g. in grails-app/views and grails-app/controllers respectively).

First to set up the stage to what I'll talk about. For starters, I knew that HiveMind is what makes Tapestry tick. Secondly, I knew that Tapestry is very flexible and customizable, and I expected to fairly easy be able to make it look in all the right places.

So, first stop was the Tapestry User Guide configuration section. At the bottom of the page it says that there is a "configuration point" for org.apache.tapestry. specification-resolver-delegate and org.apache.tapestry. template-source-delegate - the explanations seem to point out that this is exactly what I need. So, I start thinking : how hard could it be to implement a couple of interfaces to just point to the directories I want and have it do it's magic. Well, it turns out, not as easy as it sounds.

  1. So, for the template resolver, I only need to implement this one interface, that should be easy

    public ComponentTemplate findTemplate(IRequestCycle cycle, IComponent component, Locale locale) { }

  2. Alright, I got right on it. Now, I only need to figure out how to produce a ComponentTemplate. I start digging through the Tapestry core source code, So, the constructor for ComponentTemplate looks like this:

    public ComponentTemplate(char[] templateData, TemplateToken[] tokens) {}

    OK, I can figure out how to produce a char array from a file, but these TemplateToken-s... At this point, I started realizing that this is not something that I should try to figure out from the top of my head, but rather go out and find an example that already implements the ITemplateSourceDelegate interface, surely somebody has done this before.

  3. So, I start digging around for an example. This is where it started getting scary. The first couple of links that come up talk about how nobody has really posted a good example of how this is done. Overall, the problem is that there is a boatload of "helper" objects (e.g. DefaultParserDelegate, TemplateParser, ComponentSpecificationResolverImpl, etc. the list goes for quite a bit) that you need to create before you can actually create an instance of this ComponentTemplate. On one hand, it all makes sense: Tapestry is a very modular framework, and you could potentially replace each one of it's pieces with some other component that implements the contract. On the downside, if you don't know much about the guts of Tapestry (e.g. for someone like me), digging into the guts is not that much fun.

  4. So, I say, I'll give the code that was posted on the mailing list, that should certainly work, especially since the responders say that they do work. Great, I copy-paste-compile.. and when I run my test Tapestry app (with a similar structure to a grails app - e.g. with WEB-INF/grails-app/views, etc)... a NullPointerException.. Luckily, Tapestry is open source, I dig into the source and I realize that I get the NPE when it's trying to log something.. So, do I need to inject a log into every object that I create ???

  5. This is when I realized that I just need to use the facilities that Tapestry uses to inject it's dependencies - namely, Hivemind. The thing is, I know nothing about it.. After reading up about it, it turns out that it uses these configuration points that you can use as well. So, my task moved from "copy and paste an example from the mailing list" to "figure out how this HiveMind thing works and then make it work for my ITemplateSourceDelegate implementation. I have read 2 books on Tapestry, but neither of them says anything about HiveMind (I admit, one was older, on Tapestry 3.0, the other one more focused on Tapestry itself, not its infrastructure). So, here is what I ended up using for my hivemind config file:
    <module id="id" version="0.0.1" package="package">
    <implementation service-id="">
    <construct class="com.troymaxventures.grapestry.framework.ViewsSpecificationResolverDelegate">
    <set property="pagePath" value="grails-app/controllers"/>
    <set property="componentPath" value="grails-app/views"/>

    <implementation service-id="tapestry.parse.TemplateSourceDelegate">
    <construct class="com.troymaxventures.grapestry.framework.ViewsTemplateSourceDelegate">
    <set property="grailsAppPath" value="WEB-INF"/>
    <set-service property="parser" service-id="tapestry.parse.TemplateParser" />
    <set-object property="contextRoot" value="infrastructure:contextRoot" />
    <set-service property="componentSpecificationResolver" service-id="" />
    <set-object property="componentPropertySource" value="infrastructure:componentPropertySource" />
    <set-object property="rootOverride" value="app-property:grapestry-webapp-root-override" />

This hivemind configuration almost works for what I needed it to do in Grapestry. Unfortunately, it didn't get too far as the Grails test / development configurations work slightly differently than in production.

The general conclusion on the technology / Hivemind : it definitely seems like a cool dependency injection solution, especially in the way that it can dynamically aggregate modules and create a configuration on the fly (e.g. compared to Spring, where you have to explicitly say what goes into the config and how the different configs will interact). However, as with anything else, the high level of decomposition, where each class does only one job, and the many dependencies between the classes, make it sometimes difficult to figure out : e.g. if for a simple change like this, you have to track down 10 dependencies, that might have dependencies on their own, it sometimes makes you wish that the software you're trying to use wasn't as clean.