Friday, December 26, 2008

Exception Reporting: The Why

When things go wrong in a complicated system, I have one question: why?

First off: if you are building in Java you are building a complicated system. The reason people cling to Java nowadays is because of the 10+ years of libraries that have evolved. All those libraries are strung together using raw code, or Spring, or Guice, or Tapestry 5 IoC. In an add-hoc, just-in-time, per-thread, abstractions-R-us world, knowing Why a particular operation was invoked is often more use than knowing what in particular failed.

Today's example: I'm working on better integrating Tapestry and Spring as a first step towards Spring Web Flow integration.

I'm mid way through and things started breaking. Now, a normal system can output an exception:

[ERROR] ContextLoader Context initialization failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'upcase' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.example.testapp.services.StringTransformer]: : No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean
 at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:591)
 at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:193)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:925)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:835)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:440)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
 at java.security.AccessController.doPrivileged(Native Method)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:380)
 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:264)
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:261)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:429)
 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:728)
 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:380)
 at org.springframework.web.context.ContextLoader.createWebApplicationContext(ContextLoader.java:255)
 at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:199)
 at org.apache.tapestry5.internal.spring.SpringModuleDef$1$1$1.invoke(SpringModuleDef.java:60)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.internal.spring.SpringModuleDef$1$1.createObject(SpringModuleDef.java:56)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator$1.invoke(OperationTrackingObjectCreator.java:45)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator.createObject(OperationTrackingObjectCreator.java:49)
 at org.apache.tapestry5.ioc.internal.SingletonServiceLifecycle.createService(SingletonServiceLifecycle.java:29)
 at org.apache.tapestry5.ioc.internal.LifecycleWrappedServiceCreator.createObject(LifecycleWrappedServiceCreator.java:52)
 at org.apache.tapestry5.ioc.internal.InterceptorStackBuilder.createObject(InterceptorStackBuilder.java:56)
 at org.apache.tapestry5.ioc.internal.RecursiveServiceCreationCheckWrapper.createObject(RecursiveServiceCreationCheckWrapper.java:60)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator$1.invoke(OperationTrackingObjectCreator.java:45)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator.createObject(OperationTrackingObjectCreator.java:49)
 at org.apache.tapestry5.ioc.internal.services.JustInTimeObjectCreator.createObject(JustInTimeObjectCreator.java:65)
 at $ConfigurableWebApplicationContext_11e7601a600.delegate($ConfigurableWebApplicationContext_11e7601a600.java)
 at $ConfigurableWebApplicationContext_11e7601a600.getBeansOfType($ConfigurableWebApplicationContext_11e7601a600.java)
 at org.apache.tapestry5.internal.spring.SpringModuleDef$2.provide(SpringModuleDef.java:121)
 at org.apache.tapestry5.internal.spring.SpringModuleDef$3$1$1.invoke(SpringModuleDef.java:170)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.internal.spring.SpringModuleDef$3$1.provide(SpringModuleDef.java:164)
 at org.apache.tapestry5.ioc.internal.services.MasterObjectProviderImpl.provide(MasterObjectProviderImpl.java:38)
 at $MasterObjectProvider_11e7601a5f7.provide($MasterObjectProvider_11e7601a5f7.java)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.getObject(RegistryImpl.java:626)
 at org.apache.tapestry5.ioc.internal.ObjectLocatorImpl.getObject(ObjectLocatorImpl.java:49)
 at org.apache.tapestry5.ioc.internal.util.InternalUtils.calculateInjection(InternalUtils.java:208)
 at org.apache.tapestry5.ioc.internal.util.InternalUtils.access$000(InternalUtils.java:42)
 at org.apache.tapestry5.ioc.internal.util.InternalUtils$2.invoke(InternalUtils.java:255)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.util.InternalUtils.calculateParameters(InternalUtils.java:259)
 at org.apache.tapestry5.ioc.internal.ModuleImpl.constructModuleBuilder(ModuleImpl.java:380)
 at org.apache.tapestry5.ioc.internal.ModuleImpl.access$1000(ModuleImpl.java:36)
 at org.apache.tapestry5.ioc.internal.ModuleImpl$5$1.invoke(ModuleImpl.java:313)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.ModuleImpl$5.run(ModuleImpl.java:308)
 at org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier$2.invoke(ConcurrentBarrier.java:198)
 at org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier$2.invoke(ConcurrentBarrier.java:196)
 at org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier.withWrite(ConcurrentBarrier.java:138)
 at org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier.withWrite(ConcurrentBarrier.java:204)
 at org.apache.tapestry5.ioc.internal.ModuleImpl$6.invoke(ModuleImpl.java:323)
 at org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier.withRead(ConcurrentBarrier.java:83)
 at org.apache.tapestry5.ioc.internal.ModuleImpl.getModuleBuilder(ModuleImpl.java:331)
 at org.apache.tapestry5.ioc.internal.ServiceResourcesImpl.getModuleBuilder(ServiceResourcesImpl.java:137)
 at org.apache.tapestry5.ioc.internal.ServiceBuilderMethodInvoker.createObject(ServiceBuilderMethodInvoker.java:47)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator$1.invoke(OperationTrackingObjectCreator.java:45)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator.createObject(OperationTrackingObjectCreator.java:49)
 at org.apache.tapestry5.ioc.internal.SingletonServiceLifecycle.createService(SingletonServiceLifecycle.java:29)
 at org.apache.tapestry5.ioc.internal.LifecycleWrappedServiceCreator.createObject(LifecycleWrappedServiceCreator.java:52)
 at org.apache.tapestry5.ioc.internal.InterceptorStackBuilder.createObject(InterceptorStackBuilder.java:56)
 at org.apache.tapestry5.ioc.internal.RecursiveServiceCreationCheckWrapper.createObject(RecursiveServiceCreationCheckWrapper.java:60)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator$1.invoke(OperationTrackingObjectCreator.java:45)
 at org.apache.tapestry5.ioc.internal.InvokableToRunnable.run(InvokableToRunnable.java:36)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.run(OperationTrackerImpl.java:48)
 at org.apache.tapestry5.ioc.internal.OperationTrackerImpl.invoke(OperationTrackerImpl.java:89)
 at org.apache.tapestry5.ioc.internal.PerThreadOperationTracker.invoke(PerThreadOperationTracker.java:68)
 at org.apache.tapestry5.ioc.internal.RegistryImpl.invoke(RegistryImpl.java:869)
 at org.apache.tapestry5.ioc.internal.OperationTrackingObjectCreator.createObject(OperationTrackingObjectCreator.java:49)
 at org.apache.tapestry5.ioc.internal.services.JustInTimeObjectCreator.createObject(JustInTimeObjectCreator.java:65)
 at $ServletApplicationInitializer_11e7601a5f6.delegate($ServletApplicationInitializer_11e7601a5f6.java)
 at $ServletApplicationInitializer_11e7601a5f6.initializeApplication($ServletApplicationInitializer_11e7601a5f6.java)
 at org.apache.tapestry5.TapestryFilter.init(TapestryFilter.java:91)
 at org.mortbay.jetty.servlet.FilterHolder.start(FilterHolder.java:71)
 at org.mortbay.jetty.servlet.WebApplicationHandler.initializeServlets(WebApplicationHandler.java:310)
 at org.mortbay.jetty.servlet.WebApplicationContext.doStart(WebApplicationContext.java:509)
 at org.mortbay.util.Container.start(Container.java:72)
 at org.mortbay.http.HttpServer.doStart(HttpServer.java:708)
 at org.mortbay.util.Container.start(Container.java:72)
 at org.apache.tapestry5.test.JettyRunner.createAndStart(JettyRunner.java:140)
 at org.apache.tapestry5.test.JettyRunner.(JettyRunner.java:65)
 at org.apache.tapestry5.test.AbstractIntegrationTestSuite.setup(AbstractIntegrationTestSuite.java:261)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:585)
 at org.testng.internal.MethodHelper.invokeMethod(MethodHelper.java:580)
 at org.testng.internal.Invoker.invokeConfigurationMethod(Invoker.java:416)
 at org.testng.internal.Invoker.invokeConfigurations(Invoker.java:154)
 at org.testng.internal.Invoker.invokeConfigurations(Invoker.java:88)
 at org.testng.internal.TestMethodWorker.invokeBeforeClassMethods(TestMethodWorker.java:167)
 at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:104)
 at org.testng.TestRunner.runWorkers(TestRunner.java:720)
 at org.testng.TestRunner.privateRun(TestRunner.java:590)
 at org.testng.TestRunner.run(TestRunner.java:484)
 at org.testng.SuiteRunner.runTest(SuiteRunner.java:332)
 at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:327)
 at org.testng.SuiteRunner.privateRun(SuiteRunner.java:299)
 at org.testng.SuiteRunner.run(SuiteRunner.java:204)
 at org.testng.TestNG.createAndRunSuiteRunners(TestNG.java:864)
 at org.testng.TestNG.runSuitesLocally(TestNG.java:830)
 at org.testng.TestNG.run(TestNG.java:748)
 at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:73)
 at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:124)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:613)
 at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:622)
 at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:584)
 ... 137 more

... but that's not exactly helpful. Why was it trying to build the Spring ApplicationContext at that time?

That's where Tapestry 5.1 comes in; it carefully tracks what is going on in the IoC container, using a kind of nested diagnostic context:

[ERROR] Registry Error creating bean with name 'upcase' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.example.testapp.services.StringTransformer]: : No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean
[ERROR] Registry Operations trace:
[ERROR] Registry [ 1] Realizing service ServletApplicationInitializer
[ERROR] Registry [ 2] Invoking org.apache.tapestry5.services.TapestryModule.buildServletApplicationInitializer(Logger, List, ApplicationInitializer) (at TapestryModule.java:1031)
[ERROR] Registry [ 3] Constructing module class org.apache.tapestry5.services.TapestryModule
[ERROR] Registry [ 4] Determining injection value for parameter #1 (org.apache.tapestry5.ioc.services.PipelineBuilder)
[ERROR] Registry [ 5] Resolving object of type org.apache.tapestry5.ioc.services.PipelineBuilder using MasterObjectProvider
[ERROR] Registry [ 6] Resolving Spring bean of type org.apache.tapestry5.ioc.services.PipelineBuilder
[ERROR] Registry [ 7] Realizing service ApplicationContext
[ERROR] Registry [ 8] Invoking org.apache.tapestry5.internal.spring.SpringModuleDef$1$1@2cb491
[ERROR] Registry [ 9] Creating Spring Application Context
[ERROR] ApplicationContext Construction of service ApplicationContext failed: Error creating bean with name 'upcase' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.example.testapp.services.StringTransformer]: : No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean
org.apache.tapestry5.ioc.internal.OperationException: Error creating bean with name 'upcase' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.example.testapp.services.StringTransformer]: : No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.example.testapp.services.StringTransformer] is defined: Unsatisfied dependency of type [interface org.example.testapp.services.StringTransformer]: expected at least 1 matching bean

That's a bit clearer, isn't it, as long as you realize what the MasterObjectProvider is ... and that we've contributed into it some code to resolve dependencies against the Spring ApplicationContext. In fact, what happened is that the test case failed because I haven't implemented a way for a Spring bean to receive a Tapestry service as an injection, and because Spring does not lazily initialize its beans by default, we see that failure immediately the first time we try to calculate an injection.

Off topic: You Aint Got No Pancake Mix!

I have a new hero:

Taking on intolerance (religious or otherwise) with humor is a great tactic.

Friday, December 12, 2008

Tapestry 5.0 Final Release - 5.0.18

After nearly three years of development, the final release of Apache Tapestry 5.0 is now available for download.

Apache Tapestry 5 is a total rewrite of the Tapestry web application framework, bringing forward Tapestry's core concepts: reusable components, true encapsulation, readable templates, well thought-out localization/internationalization, and easy management of server-side state.

Tapestry 5 builds on top of this with a host of new features:

  • True POJO component classes: no base classes to extend, no interfaces to implement.
  • Live class reloading: no need to redeploy to see code changes.
  • XML templates with namespaces.
  • Minimal configuration via naming conventions and annotations.
  • Integrated Ajax support, built on top of Prototype and Scriptaculous.
  • Automatic client-side form input validation.
  • High performance via pooled objects (and by avoiding the use of reflection).
  • Automatic REST-style URLs.
  • Built-in integration with Hibernate and Spring.
  • Best-of-breed exception reporting.
  • Built-in extensible mega-components: BeanEditForm, BeanDisplay and Grid (to edit and display any JavaBean or collection of JavaBeans).

Tapestry organizes your application into pages, and components within pages; pages and components are ordinary POJOs: not singletons (like servlets). Tapestry combines pages, page templates, components, component templates, and other resources together for you, managing server-side state, the creation of URLs and the dispatch of incoming requests. You build your application in terms of the methods and properties of your objects, not in terms of URLs or the Servlet API.

Tapestry features great exception reporting to keep you on track, and live class reloading to keep you agile. Tapestry templates are XML documents, using a namespace for Tapestry-specific elements. Tapestry is designed to be easy to develop, using any standard IDE with an XML editor.

Tapestry is simple, sensible and fun. It keeps you productive by freeing you from the boring, mechanical aspects of web application development. You can stay focused on what makes your application interesting and unique, and let Tapestry handle all the ugly plumbing.

Tapestry is made available under the Apache Software License 2.0. Tapestry is free to download, free to use, free to redistribute and free to modify.

Tapestry 5.0.18

Clojure: The Hundred Year Language

Back in 2003, Paul Graham gave a keynote speech entitled "The Hundred Year Language" (he later expanded this for his book "Hackers and Painters"). He envisioned what an early 22nd century programming language would look like ... what would control the flying cars and robot spaceships.

To my eyes, it looks a lot like Clojure.

He talks about languages that can be highly expressive, yet can be tuned for better performance. He explicitly references Lisp, Clojure's very-close cousin. He's concerned with parallel computation (which is to say, concurrency) ... which is one of Clojure's strong suits.

In any case, check it out ... and see if you are seeing what I'm seeing.

Monday, December 08, 2008

What's on the doorstep?

I haven't even quite moved into my new house, but what was waiting for me on the porch? Programming in Scala. Of course, I seem to be straying towards Clojure in terms of where-to-go-on-the-JVM-after-Java. But we'll see. Certainly reading about any new and powerful language will give you ideas even if you don't adopt it for day-to-day usage.

Sunday, December 07, 2008

ANTLR and code generation

In between packing (I'm moving across town) I'm doing a bit of work for Tapestry 5.1, TAP5-79: Improve Tapestry's property expression language to include OGNL-like features. People really miss being able to do a few cool things in OGNL, such as create lists and maps on the fly ... this is not uncommon when creating a page activation context.

The 5.0 code was based on regular expressions and hand parsing because it only supported a very limited number of options. For 5.1, the grammar will grow considerably, adding options for list and map creation, method invocation (with parameters), and perhaps property projection and list filtering. Hand-tooled parsers aren't going to keep up, so it was time to switch to a more complete solution.

I ended up choosing ANTLR because it seems well supported, has a book and good online documentation, and a set of supporting tools. ANTLR is used elsewhere as well, for example by Hibernate to parse HQL.

There is even decent support for ANTLR with Maven (while Tapestry still builds with Maven, something I hope to address soon). Because of this, I only check my grammar files into SVN, not the generated files; on the continuous integration server, the ANTLR plugin generates the lexer and parser code fresh for each build.

The only real down-side is the runtime dependency ... about 113K and problematic if Tapestry is ever combined with some other tool that has a dependency on a different and incompatible version. Hibernate (for better or worse) uses the ANTLR2 runtime library, which uses different package names.

My first step was to re-create Tapestry 5.0's behavior on top of ANTLR. Because of some complexity in the lexical part of the grammar (that darn ".." operator!) it took quite a bit of head bashing. I did eventually figure it out, and did what any self-respecting coder should do ... leave a simple, useful, documented example for the next poor slob.

Now I'm back into the side of code generation; Tapestry's property expression grammar is converted directly into bytecode; the intermediate language is Javassist, which is a significant subset of Java. So I parse the property expressions into a AST (abstract syntax tree), then generate what looks like Java code from that, which gets compiled in-process and turned directly into instantiable classes.

How would you test something like that? At one time, I would try to unit test that the generated code was correct. Eventually I hit some bugs where my tests passed, but the generated code was incorrect.

With code generation, there is no such thing as a unit test, it's always an integration test. You can try and limit the scope, but there's too many moving parts for a unit test to useful or credible.

Instead, I test my parsing and code generation logic by testing the generated objects' behavior. So I feed in a large number of expressions and objects to have expressions evaluated upon, and check that the results I get by reading and setting property expressions is correct. If I get the right results, I know the generated code is good.