Friday, August 26, 2011

Adding Common Methods to JAXB-Generated Classes (Groovy invokeMethod)

In two previous posts [Adding Common Methods to JAXB-Generated Java Classes (JAXB2 Basics Plugins) and Adding Common Methods to JAXB-Generated Classes (Separate Class/Groovy Categories)], I looked at using JAXB2 Basic Plugins and a separate Class (including via Groovy Categories) respectively to deal with the issue of JAXB-generated classes without common methods toString(), equals(Object), and hashCode(). In this post, I look at a third approach to providing functionality of these common methods for JAXB-generated classes. The approach covered in this post is to use Groovy's dynamic method interception via invokeMethod.

Groovy's invokeMethod allows Groovy code to intercept a method call and either do something different than the intercepted method, do something before invoking the intended method, or do something after invoking the intended method. This, of course, sounds a lot like aspect-oriented programming (AOP). This approach works particularly well for adding functionality of toString(), equals(Object), and hashCode() to classes that don't already override these methods (such as the JAXB-generated classes). We can intercept calls to the versions of these methods provided by the super Object class and instead implement versions specific to the JAXB-generated classes.

The next Groovy code listing shows Groovy code that intercepts calls to common methods on the JAXB-generated MovieType class (see the previous post for this class's source code).

compareMovieTypes4.groovy
import dustin.examples.MovieGenre
import dustin.examples.MovieType
import dustin.examples.MovieTypeCommons
import dustin.examples.Movies
import javax.xml.bind.Unmarshaller
import javax.xml.bind.JAXBContext
import javax.xml.bind.JAXBException

MovieType.metaClass.invokeMethod =
{ String name, args ->
   if (name == 'equals')
   {
      return MovieTypeCommons.doEquals(delegate, args[0])
   }
   else if (name == 'hashCode')
   {
      return MovieTypeCommons.doHashCode(delegate)
   }
   else if (name == 'toString')
   {
      return MovieTypeCommons.doToString(delegate)
   }
   else
   {
      def methodHandle = MovieType.metaClass.getMetaMethod(name, args)
      if (methodHandle != null)
      {
         return methodHandle.invoke(delegate, args)
      }
   }
}

def movies1 = getContentsOfMoviesFile("movies1.xml")
def movies2 = getContentsOfMoviesFile("movies1.xml")
movies1.movie.each
{ movie ->
   boolean matchFound = false
   movies2.movie.each
   { otherMovie ->
      if (movie == otherMovie)
      {
         matchFound = true
      }
   }
   if (matchFound)
   {
      println "Match FOUND for ${movie.toString()}"
   }
   else
   {
      println "NO match found for ${movie.toString()}"
   }
} 

def Movies getContentsOfMoviesFile(String xmlFileName)
{
   def jc = JAXBContext.newInstance("dustin.examples")
   def u = jc.createUnmarshaller()
   def movies = (Movies) u.unmarshal(new File(xmlFileName))
   return movies;
}

The simple Groovy script shown above uses the syntax MovieType.metaClass.invokeMethod = { to declare a closure that intercepts all method calls on the MovieType class. In this simple script, any method with the name "toString", "equals", or "hashCode" is intercepted and each method with one of these names has its implementation replaced with a call to the MovieTypeCommons class introduced in this earlier post. Any intercepted method that does not have a name matching "toString", "equals", or "hashCode" is not treated and the code simply turns around and invokes that intended method without change. The gist of all this is that only methods with the three mentioned names are changed in implementation (in this case to override the nearly useless Object-provided implementation with something useful) and all other methods are left well enough alone.

The Groovy metaClass for MovieType is used to invoke invokeMethod to intercept method calls. This same metaClass is also used to get a handle on the method that was intercepted so that this handle's invoke method can be used to invoke the originally intended method in the manner that it was originally called.

After the closure for MovieType is defined, the script goes on to use the conveniences of this method interception to invoke equals (via Groovy's == operator) and toString() on the MovieType class. The good news here is that this script does determine that JAXB objects with the same content are equal and does provide a nice String representation of them thanks to the intercepted and alternatively handled methods.


There are different flavors of method interception in Groovy. The GroovyInterceptable interface is defined in its Javadoc documentation as a "marker interface used to notify that all methods should be intercepted through the invokeMethod mechanism of GroovyObject." Making a Groovy class implement this interface provides similar method interception as the approach I demonstrated earlier in this post using metaClass.invokeMethod.

If the code needing to use JAXB-generated classes is Groovy, this is an elegant way to "add" methods to those classes for common functionality such as equality checking, hash code generation, and String representation. Of course, this approach can be generalized to cover any such situations when existing classes (even Java classes like JAXB-generated MovieType) need to support methods they don't statically support.

No comments: