Saturday, August 27, 2011

Adding Common Methods to JAXB-Generated Classes (Extension)

I introduced the issue of JAXB-generated classes not having explicitly overridden equals(Object), hashCode(), or toString() methods in the blog post Adding Common Methods to JAXB-Generated Java Classes (JAXB2 Basics Plugins). In that post, I solved that problem using JAXB2 Basic Plugins to instruct the JAXB 2 RI xjc binding compiler to create these methods when creating Java classes based on a source XSD file. In two posts that followed, I showed how to solve this issue using a separate and distinct class (including with Groovy Categories) and how to solve this issue using Groovy's method interception. In this post, I look at yet another approach: extending the JAXB-generated classes and providing these common methods at that level.

The JAXB-generated MovieType class defined in my earlier post does not have the desired common methods. However, the next code listing shows how it can be extended to provide these methods in the child class on behalf of the parent class. This new class contains no attributes of its own but instead bases its common methods on the attributes inherited from its parent class.

MovieTypeExtended.java
package dustin.examples;

import java.util.Objects;

/**
 * This class extends {@link dustin.examples.MovieType} to add explicitly
 * overridden <code>toString()</code>, <code>equals(Object)</code>, and
 * <code>hashCode()</code> methods.
 */
public class MovieTypeExtended extends MovieType
{
   public MovieTypeExtended(final MovieType parentSource)
   {
      this.title = parentSource.title;
      this.year = parentSource.year;
      this.genre = parentSource.genre;
   }

   @Override
   public String toString()
   {
      return this.title + " [" + this.year + "/" + this.genre + "]";
   }

   @Override
   public boolean equals(final Object obj)
   {
      if (obj == null)
      {
         return false;
      }
      if (getClass() != obj.getClass())
      {
         return false;
      }
      final MovieTypeExtended other = (MovieTypeExtended) obj;
      return    Objects.equals(this.title, other.title)
             && Objects.equals(this.year, other.year)
             && Objects.equals(this.genre, other.genre);
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(this.title, this.year, this.genre);
   }
}

The above class is fairly simple thanks to use of JDK 7's new Objects class. The client or test code that needs to use the common functionality of MovieType can get this behavior from this extended class. Example code is shown next.

Main3.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
 * extended classes that provide those common methods for the JAXB-generated
 * parents.
 */
public class Main3
{
   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 Main3 me = new Main3();
      final Movies movies1 = me.getContentsOfMoviesFile("movies1.xml");
      final Movies movies2 = me.getContentsOfMoviesFile("movies1.xml");
      for (final MovieType movieSrc : movies1.getMovie())
      {
         final MovieTypeExtended movie = new MovieTypeExtended(movieSrc);
         boolean matchFound = false;
         for (final MovieType otherMovieSrc : movies2.getMovie())
         {
            final MovieTypeExtended otherMovie = new MovieTypeExtended(otherMovieSrc);
            if (movie.equals(otherMovie))
            {
               matchFound = true;
               break;
            }
         }
         if (matchFound)
         {
            out.println("Match FOUND for " + movie);
         }
         else
         {
            out.println("NO match found for " + movie);
         }
      }
   }
}

The extended class does not have to be written in Java. The next example is an class that extends MovieType and is implemented in Groovy.

MovieTypeGroovyExtended.groovy
package dustin.examples

/**
 * Using @Canonical or @EqualsAndHashCode or @ToString does not work here because
 * attributes of interest are not actually declared at this class's level.
 */
class MovieTypeGroovyExtended extends MovieType
{
   public MovieTypeGroovyExtended(MovieType parentSource)
   {
      this.title = parentSource.title
      this.year = parentSource.year
      this.genre = parentSource.genre
   }

   @Override
   public boolean equals(Object object)
   {
      if (object == null)
      {
         return false
      }
      if (this.getClass() != object.getClass())
      {
         return false
      }
      def other = (MovieTypeGroovyExtended) object
      return Objects.equals(this.title, other.title) &&
             Objects.equals(this.year, other.year) &&
             Objects.equals(this.genre, other.genre)
   }

   @Override
   public String toString()
   {
      return "${this.title} [${this.year}/${this.genre}]"
   }
}

The above example is fairly similar to the Java version, but with a little more concise Groovy syntax. It would have been really slick to be able to use the Groovy 1.8 provided annotations @ToString, @EqualsAndHashCode, and @Canonical annotations I covered in my posts (here and here), but these won't work here because they work against the attributes of a particular class rather than the attributes of its parent class. They support calling their parent's version of the same methods, but that is not helpful when the parent class doesn't implement those methods.

This is the fourth approach I have shown for dealing with lack of common methods in JAXB-generated classes. This is also my least favorite approach of the four because it relies on implementation inheritance of a concrete class by an otherwise "empty" class solely providing "common" methods. It seems odd to implement an "empty" class to perform functions on its parent class. It is easier to accept it for tests and simple uses, but I prefer the three other approaches (build JAXB classes with common methods via JAXB2 Plugins, use separate class to perform functionality, and Groovy method interception) over this approach for general use. The other approaches all feel and look "closer" to the actual JAXB-generated classes than this one from client code/script perspective.

No comments: