Thursday, August 25, 2011

Adding Common Methods to JAXB-Generated Classes (Separate Class/Groovy Categories)

I have previously discussed using JAXB2 Basic Plugins to add common methods to JAXB-generated Java classes. In this blog post, I look at another approach for performing the same behaviors on JAXB-generated objects as these classes might normally provide for themselves. In this case, I look at writing a separate class to perform the same functionality that classes normally define for themselves in the common and overridden methods equals(Object), hashCode(), and toString().

The next code listing shows a JAXB-generated class (see my previous post for the XSD used to generate this class) that has been generated with the default settings of the JAXB 2 RI xjc binding compiler without use of JAXB2 Basic Plugins. In other words, there is no "common" method defined for this class such as toString(), hashCode(), or equals(Object).

If one does not have access to the source schema or for some reason cannot regenerate the JAXB classes using a framework like JAXB2 Basic Plugins to automatically add these common methods to the generated classes, another approach that can be used is to have a totally distinct class or piece of code perform the same functionality on these classes. The following is a simple Java class that performs equals, hashCode, and toString functionality for a JAXB-generated class that does not have its own overridden implementations of these.

MovieTypeCommons.java
package dustin.examples;

import java.util.Objects;

/**
 * <p>Class that compares a JAXB-generated MovieType class to add common methods
 * {@code toString()}, {@code equals(Object)}, and {@code hashCode()} that a
 * class normally overrides from {@link java.lang.Object}, but which are not
 * overridden in the JAXB-generated class.</p>
 * 
 * <p>This class requires Java 7 for compiling as it uses the new
 * {@link java.lang.Objects} class.</p>
 * 
 * <p>This class could be used as-is in Java or Groovy to provide surrogate
 * "common" methods for instances of {@link dustin.examples.MovieType} or
 * could be used as a Groovy Category.</p>
 */
public class MovieTypeCommons
{
   /**
    * Performs equality comparison on two provided instances of the
    * {@link dustin.examples.MovieType} class.
    * 
    * @param movie1 First instance to be compared.
    * @param movie2 Second instance to be compared.
    * @return {@code true} if the two provided instances are equal or
    *    {@code false} if the two instances are not equal or if either provided
    *    parameter is null.
    */
   public static boolean doEquals(final MovieType movie1, final MovieType movie2)
   {
      if (movie1 == null || movie2 == null)
      {
         return false;
      }
      if (movie1.equals(movie2))
      {
         return true;
      }
      if (   !Objects.equals(movie1.title, movie2.title)
          || !Objects.equals(movie1.year, movie2.year)
          || movie1.genre != movie2.genre)
      {
         return false;
      }
      return true;
   }


   /**
    * Provide hash code for provided instance of {@link dustin.examples.MovieType}.
    * 
    * @param movie Instance for which hash code is desired.
    * @return The hash code for the provided object; will be null if provided
    *    MovieType is null.
    */
   public static int doHashCode(final MovieType movie)
   {
      return  movie != null
            ? Objects.hash(movie.title, movie.year, movie.genre)
            : null;
   }


   /**
    * Provide String representation for provided instance of
    * {@link dustin.examples.MovieType}.
    * 
    * @param movie Movie Instance for which String representation is desired.
    * @return String representation of provided instance; will be "null" if
    *    provided MovieType is null.
    */
   public static String doToString(final MovieType movie)
   {
      return  movie != null
            ? movie.title + " [" + movie.genre + "/" + movie.year + "]"
            : "null";
   }
}

The above code accepts instances of the JAXB-generated class MovieType and performs the common functionality on them. It is worth a quick diversion here to point out that handiness of Java 7's sparkling new Objects class in implementing these methods in a null-safe fashion.

The next code listing shows how a simple Java client can use this commons-providing class to handle the JAXB-generated MovieType class.

Main2.java
package dustin.examples;

import java.io.File;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;

import static java.lang.System.out;
import static java.lang.System.err;

/**
 * Main class for demonstrating use of common methods on JAXB objects via a
 * separate and distinct class.
 */
public class Main2
{
   public Movies getContentsOfMoviesFile(final String xmlFileName)
   {
      Movies movies = null;
      try
      {
         final JAXBContext jc = JAXBContext.newInstance("dustin.examples");
         final Unmarshaller u = jc.createUnmarshaller();
         movies = (Movies) u.unmarshal(new File(xmlFileName));
      }
      catch (JAXBException jaxbEx)
      {
         err.println(jaxbEx.toString());
      }
      catch (ClassCastException castEx)
      {
         err.println("Unable to get Movies object out of file " + xmlFileName +
                 " - " + castEx.toString());
      }
      return movies;
   }
 

   /**
    * Simple example of using separate and distinct class to perform common
    * functionality on JAXB-generated classes that cannot provide these functions
    * for themselves.
    * 
    * @param arguments Command line arguments; none expected.
    */
   public static void main(String[] arguments)
   {
      final Main2 me = new Main2();
      final Movies movies1 = me.getContentsOfMoviesFile("movies1.xml");
      final Movies movies2 = me.getContentsOfMoviesFile("movies1.xml");
      for (final MovieType movie : movies1.getMovie())
      {
         boolean matchFound = false;
         for (final MovieType otherMovie : movies2.getMovie())
         {
            if (MovieTypeCommons.doEquals(movie, otherMovie))
            {
               matchFound = true;
               break;
            }
         }
         if (matchFound)
         {
            out.println("Match FOUND for " + MovieTypeCommons.doToString(movie));
         }
         else
         {
            out.println("NO match found for " + MovieTypeCommons.doToString(movie));
         }
      }
   }
}

When the above class's main method is executed, we see both MovieTypeCommons.doEquals(MovieType,MovieType) and MovieTypeCommons.doToString(MovieType) in action and working.


As the example above indicates, using a separate and distinct class to perform common functionality is a viable solution for working with JAXB-generated classes that don't define these themselves. However, there are some disadvantages. First, the code must be generated separately and cannot be done in a simple step as was done with JAXB2 Basic Plugins. Every time something changes, the multiple steps are required. The second problem is related in that most changes to the JAXB-generated classes require corresponding changes to the separate class that works on their instances. Even with these disadvantages in mind, it still may be a practical solution in some cases.

We can use the class defined above (MovieTypeCommons) even easier in Groovy. In fact, in Groovy, its use can be made to look very similar to what the calling code would look like if we were operating on the JAXB-generated classes directly. This can be implemented using Groovy's Objective-C-inspired Category support. As the documentation states, a Groovy category satisfies the "many situations where you might find that it would be useful if a class not under your control had additional methods that you define."

Before demonstrating use of Groovy Categories, I first "port" the Main2 simple Jav client from above to a fairly close Groovy equivalent.

compareMovieTypes.groovy
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;

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

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
}

This first Groovy script does NOT use Categories and calls the separate and distinct class MovieTypeCommons just as its Java equivalent did with the static methods access. Categories make this code a little cleaner and make it look as if the methods were being called directly on the instances of MovieType rather than on the separate class. The next Groovy script shows Groovy code using Categories (note the use keyword) to make it appear as if the methods were being called on the MovieType instance rather than on the separate MovieTypeCommons static methods.

compareMovieTypes2.groovy
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;

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

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;
}

In the last Groovy code listing, the code appears to call doEquals(MovieType) and doToString(MovieType> on the MovieType instance itself! This looks more like what we'd normally see when a class implements its own "equals" and "toString" methods.


Conclusion

This post has shown how a separate and distinct Java class might be used to implement common functionality for JAXB classes that don't have these common methods themselves. Groovy Categories were demonstrated as an easy approach to making it appear as if Groovy scripts were calling the common functionality methods on the instances of the JAXB-generated classes themselves. Along the way, Java 7's Objects class was used to simplify implementation of the common functionality.

No comments: