Implementing Tapestry Component Methods with Groovy (Part I)


Update 8/8/2004: I've been working with another Tapestry developer on integrating our approaches to Groovy scripting Tapestry component methods, the results are described in Part II.

I've worked with web application servers with scripting languages (Plone/Zope comes to mind) and for me there is an advantage in bypassing the compile/run development cycle in order to develop and fix code in a running application server. These types of mechanisms work great for non-developers or staff with scripting experience where the scripted code interacts with core infrastructure code.

Now that I'm working with Tapestry I've been itching to use scripting in this environment. Groovy has several features which make it ideal:

  1. Java-like syntax
  2. Scripts can be compiled to classes.
  3. Integration with Java class loading

What I wanted to be able to do was:

  • To code my component methods in a scripting language
  • Store scripted methods source files with their component specifications.
  • Execute them in a Tapestry application
  • Make code changes and test without restarting the application
  • Access component properties (even those of the enhanced component class) from the scripted methods
  • Deploy compiled Groovy classes in the WAR file
  • Integrate Groovy compiler and runtime errors into the line-precise error reporting in Tapestry.

I was able to develop a solution that meets all of these requirements.

Here's an image of the home page of my test application:

Groovy-Listener Home Page

The DirectLink and the Form components on this page are bound to listener methods implemented in Groovy.

If there are errors in the scripted methods source code they are displayed in the Tapestry Exception report:

Groovy-Listener Exception Page

I was working with the <listener-binding> in Tapestry which allows scripted listeners to be stored in the component specification XML. I found this unsatisfactory for the following reasons:

  • The script fragment has to be declared inside a component tag in the specification which means that implicit components cannot be used. Implicit components are much easier to deal with than switching back and forth between .html and .jwc files.
  • Code errors need a restart to fix and test, unless component caching is disabled which slows the whole application down.
  • Listeners cannot be compiled for deployment.
  • Errors are not integrated into Tapestry's Exception reporting.

As I was looking into this others were discussing it on the Tapestry-Users mailing list, and Richard Hensley posted code to resolve the class attribute in a page or component specification to either a compiled class or a groovy class, loaded from a source file on the classpath. This code was cool, detecting changes to the source file and reloading the class.

But I wanted the scripted methods to be kept with the component specification, which meant that the script file had to be treated like an application resource, much like the component templates in a Tapestry application.

I wanted the use of scripted methods to be as much like using a component class, written in Java, as possible. After some experimentation I was able to construct a component which uses listeners declared in a Groovy class with:

HTML:

<p> Click here to execute a groovy listener method. </p> <p> Complete the form and submit it to invoke a groovy script method. </p> <form jwcid="@Form" listener="ognl:listeners.testForm"> Enter a title: <input type="text" size="36" jwcid="@TextField" value="ognl:formTitle" /><br/> and Message : <input type="text" size="36" jwcid="@TextField" value="ognl:formMessage" /><br/> <input type="submit" value="Submit"> </form>

Specification:

<page-specification class="com.mjhenderson.tapestry.groovy.ScriptedMethodsPage"> <description><![CDATA[ Groovy-Tapestry Example Home Page ]]></description> <property-specification name="formTitle" type="java.lang.String"/> <property-specification name="formMessage" type="java.lang.String"/> <context-asset name="stylesheet" path="css/style.css"/> </page-specification>

Groovy:

public class Home extends ScriptedMethods implements PageRenderListener { public void pageBeginRender(PageEvent event) { println("Home.groovy: pageBeginRender("+event+")"); } public void pageEndRender(PageEvent event) { println("Home.groovy: pageEndRender("+event+")"); } public void testLink(IRequestCycle cycle) { page2 = cycle.getPage("Page2"); page2.title = "Ape Cheese"; page2.message = "This message was passed in the Groovy Script"; cycle.activate(page2); } public void testForm(IRequestCycle cycle) { page2 = cycle.getPage("Page2"); // Here we are accessing the properties 'formTitle' and // 'formMessage' of the Tapestry page component as if they // are properties of the scripted class. // It's all in the superclass' implementation of // getProperty(). // page2.title = "< " + formTitle + " >"; page2.message = "< " + formMessage + " >"; cycle.activate(page2); } public void finishLoad() { println("Home.groovy: finishLoad()"); } public void initialize() { println("Home.groovy: initialize()"); } }

I drew heavily on Richard's class loading code but developed a suite of classes that work together to separate the scripted methods out of the component class. I implemented class loading and management for the scripted methods so that the Groovy source files can either be compiled to .class files and packaged with the rest of the classes in the WAR file or left as .groovy files and packaged as resources along with the component specifications and the component templates. While developing in Eclipse and using JettyLauncher I was able to fix syntax errors, change source code and just refresh from the browser to introduce my changes. I even used this method to test the integration with the Tapestry exception reporting by introducing syntax errors into the code.

I have uploaded the source code: groovy-listener-src.tgz for my example as well as a pre-built WAR file: groovy-listener.war and a source-code cross-reference is also available online.

The WAR file has been tested in Tomcat and uses the uncompiled .groovy files for the component methods. You can download the source code and build with Maven, editing the maven.xml file to build and test a version with compiled versions of the scripted methods.