Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Wednesday, June 11, 2008

Groovy: Leaky Abstractions and Confusing Exceptions

Here's an odd place to get a ClassCastException:

  • org.hibernate.type.StringType.toString(StringType.java:44)
  • org.hibernate.type.NullableType.nullSafeToString(NullableType.java:93)
  • org.hibernate.type.NullableType.nullSafeSet(NullableType.java:140)
  • org.hibernate.type.NullableType.nullSafeSet(NullableType.java:116)
  • org.hibernate.param.NamedParameterSpecification.bind(NamedParameterSpecification.java:38)
  • org.hibernate.loader.hql.QueryLoader.bindParameterValues(QueryLoader.java:488)
  • org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:1554)
  • org.hibernate.loader.Loader.doQuery(Loader.java:661)
  • org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:224)
  • org.hibernate.loader.Loader.doList(Loader.java:2211)
  • org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2095)
  • org.hibernate.loader.Loader.list(Loader.java:2090)
  • org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:375)
  • org.hibernate.hql.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:338)
  • org.hibernate.engine.query.HQLQueryPlan.performList(HQLQueryPlan.java:172)
  • org.hibernate.impl.SessionImpl.list(SessionImpl.java:1121)
  • org.hibernate.impl.QueryImpl.list(QueryImpl.java:79)
  • org.hibernate.impl.AbstractQueryImpl.uniqueResult(AbstractQueryImpl.java:811)
  • sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  • sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
  • sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
  • java.lang.reflect.Method.invoke(Method.java:585)
  • org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:86)
  • groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:230)
  • groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:912)
  • groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:756)
  • org.codehaus.groovy.runtime.InvokerHelper.invokePojoMethod(InvokerHelper.java:766)
  • org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:754)
  • org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodN(ScriptBytecodeAdapter.java:170)
  • org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethod0(ScriptBytecodeAdapter.java:198)
  • com.nfjs.hls.blog.services.AuthenticationManagerImpl.authenticate(AuthenticationManagerImpl.groovy:34)

That StringType object was expecting a string, but got a org.codehaus.groovy.runtime.DefaultGroovyMethods$2. Seems like a little bit of Groovy has leaked unexpectedly into my Java.

The context is a method for authenticating a user's handle and password:

   Blogger authenticate(String handle, String plaintextPassword)
    {
        def authenticationCode = computeAuthenticationCode(handle, plaintextPassword) 

        def query = session.createQuery("from Blogger where handle = :handle and authenticationCode = :code")

        query.setProperties([handle: handle, code: authenticationCode])

        def result = query.uniqueResult()

        if (result)
            asm.get(UserCredentials.class).userId = result.id;

        return result
    }

   def computeAuthenticationCode(handle, plaintextPassword)
    {
        def md = MessageDigest.getInstance("MD5")

        // Salt the digest with the lower-cased handle
        md.update(handle.toLowerCase().bytes)
        md.update(plaintextPassword.bytes)

        md.digest().encodeBase64()
    }

A little hunting around showed that the ClassCastException was related to the authentication code.

Now the encodeBase64() method should be returning a String, one would think. Ah, a little poking around in the Groovy JDK docs and in fact, it returns Writable. After a few experiments, I found that the following change worked and was most pleasing:

   md.digest().encodeBase64() as String

This is the exact kind of error that static typing is designed to catch. Certainly, this would be caught by some tests eventually (I'm working a little fast and loose here), but in the end I had to work with the debugger to chase down what was wrong.

And that brings up a more significant problem: it appears that in Groovy, local variables are not visible in the debugger. I suspect they may be stuck inside the metaClass object or somewhere else I haven't found yet. That is significantly less than ideal. Update: User error; there was a button in the debugger pain that made the local variables visible.

What I'm finding, working with Groovy, is that for the trivial kind of code I'm writing to support my application, any extra bang I get for using Groovy instead of Java is being offset by leaky abstractions, my own lack of familiarity with Groovy, and the very shakey developer tools (even inside IDEA!). One aspect of this is just how trivial the Java code for a Tapestry application is; there's just not much room for Groovy to improve on pure and simple Tapestry Java.

However, the exercise is certainly worth it, because many people will be using Groovy with Tapestry and it should work without any hickups.

5 comments:

Bill Holloway said...

No hiccups hopefully down the road. Hope this can get worked out. You mention that your code so far is somewhat trivial. If these things are popping up in those environs, imagine the trouble sophisticated code could get into.

Guess we'll have to wait at least a bit for working Grapestry 5.

Howard said...

Really, the problem I hit had nothing to do with Tapestry; it was Groovy code calling into Hibernate code. The issue is the distance between the mildly incorrect code and the point at which Hibernate reported the error (or, rather, tripped over its bad assumption about the parameter type). It's not as nice as Tapestry which would either have reported the error earlier and more definitively, or just adapted to the value. The real point is that it took some time and frustration to track down what was going on ... it took me maybe 15 or 20 minutes, it could take someone with less experience and discipline much, much longer (and they might blame Tapestry along the way!)

Leonel said...

md.digest().encodeBase64() as String

Can you explain this construct a little ?

Is that the Java equivalent of a cast, a call to method toString(), or something entirely different ?

Nils said...

In IntelliJ IDEA's Groovy debugger I look at local variables way too often. So it is possible to see them. Or I have hallucinations ;-)

Howard said...

"as String" is a Groovy directive to convert the LHS to the desired type. How this occurs varies, depending on the type of object on the LHS. I could also have used toString().

The "as" keyword is more useful when converting a Closure into an object that implements a specific interface.