Tuesday, August 28, 2007

Developer-friendly JAXB code generation with JAXB Commons and binding file

In my previous post "Defining service/component interfaces in WSDLs", I mentioned that "Not only are no-argument constructors and all public getter and setter methods generated for complex types, but also other useful methods, such as valued constructors, builder methods, hashCode, equals and toString methods can be implemented using XJC extensions." This post is about how to do this.

Our project uses XFire's WsGenTask Ant task to generate service interfaces, DTO classes and exceptions from WSDL file and the referenced XSD files. WsGenTask delegates the generation of DTO classes to JAXB's XJC compiler. However, these DTO classes generated by JAXB XJC's default are not particularly developer-friendly, in the following aspects:
  1. They have only one default constructor. This is quite inconvenient when you need to instantiate a DTO to hold some values:

    • You need to declare a local variable, which may otherwise be unnecessary.

    • You need to call the constructor to instantiate a new instance, then you need to invoke the setter methods for each property value you would like to it to hold.

    • To re-use the code and get rid of the local variable declaration, you need to create a factory class with one or more overloading create methods for each DTO class, which essentially are value-taking constructors moved to a factory class.

  2. They do not override the hashCode(), equals(Object) and toString() methods of the Object class. DTOs are value objects and no identity. Two instances holding exactly the same data should be considered interchangeable. Thus, overriding hashCode() and equals(Object) is essential for DTO classes. Moreover, not overriding toString() implementation makes it inconvenient in unit testing:

    • You cannot invoke assertEquals(Object, Object) directly. You have to rely on a static method to determine whether two objects are equal in values. And if the assertion fails, you need to invoke another static method to have a useful string representation of both the expected and actual objects.

  3. Default values are not honoured in the generated classes, that means you have to set the value explicitly even if you are using a default value in most cases.

  4. If you are not the owner of the schemas being used, the class and property names generated may not be following the Java's camel-case conventions.
  5. There is no setter method for collections. You need to invoke the getter method to retrieve the collection and invoke the addAll(Collection) method to add all elements of the prepared collection.

  6. If an element has a maxOccurs attribute value greater than one, the getter method generated is not using plural by default.

  7. Date, time and dateTime are generated as XMLGregorianCalendar, which requires constant conversion to and from the java.util.Date and java.util.Calendar values you use in your domain models.

Fortunately, XJC has an extension option that allows third-party extensions. A whole bunch of XJC plugins have been developed to iron out most of these isses in the open source community, and many of them are under the JAXB 2.0 Commons project hosted by java.net.

Many of the these plugins are very useful:
  1. The Value Constructor plugin generates a constructor that takes values for all properties besides the default no-argument constructor.

  2. If you have many properties in a DTO class, or some of the properties are optional, you can use the Fluent API plugin, which generates builder-styled methods, which essentially provides named-parameter constructor, which is not provided by Java.

  3. The jakarta-commons-lang plugin generates overriding hashCode(), equals(Object) and toString() methods using jakarta commons lang's HashCodeBuilder, EqualsBuilder and ToStringBuilder classes, which in turn uses reflection.

  4. The Default Value plugin honours the default values specified in the schemas.

  5. The CamelCase Always plugin generates class and property names following the camel-case convention.

  6. The Collection Setter Injection plugin generates setter methods for collections.


Unfortunately, at the time of this writing, XFire's WsGen does not pass parameters to JAXB's XJC, as tracked by this XFIRE-1038 JIRA. The way to get around it is to call the XJCTask Ant task after the WsGenTask call and overwrite all DTOs generated by WsGenTask.

Note that some of the plugins required JAXB 2.1 to work. If you intend to use JAXB 2.0 in runtime environment, it is fine: you can generate the classes using JAXB 2.1 with the target specified as "2.0" to avoid generating annotations introduced in JAXB 2.1.

To generate a plural form for collections, use a simple JAXB binding file with XJCTask.

If you would like to work with java.util.Date or java.util.Calendar instead of XMLGregorianCalendar, you can provide your own parseMethod and printMethod and specify them in your JAXB binding file used by XJCTask, as detailed by Sun's engineer Kohsuke's blog entry.

Follwing are extracted from the ant build file to illustrate how to generate developer-friendly DTO classes using JAXB:

<!-- This task will autogenerate the code from the XSD specification -->
<taskdef name="xjc" classname="com.sun.tools.xjc.XJCTask" >
<classpath>
<pathelement path="${jaxb1-impl-2.1.3.jar}:${jaxb-api-2.1.3.jar}:${jaxb-impl-2.1.3.jar}:${jaxb-xjc-2.1.3.jar}" />
<pathelement path="${jaxb2-commons-commons-lang-plugin.jar}"/>
<pathelement path="${jaxb2-commons-value-constructor.jar}"/>
<pathelement path="${jaxb2-commons-fluent-api.jar}"/>
<pathelement path="${jaxb2-commons-default-value-plugin.jar}"/>
<pathelement path="${jaxb2-commons-collection-setter-injector.jar}"/>
<pathelement path="${component.classpath}" />
</classpath>
</taskdef>

<!-- This task will autogenerate the code from the WSDL specification -->
<taskdef name="wsgen" classname="org.codehaus.xfire.gen.WsGenTask" >
<classpath>
<pathelement path="${component.classpath}" />
</classpath>
</taskdef>

<!-- Auto generate classes based on the XSD definitions using JAXB bindings -->
<target name="xjc_gen">
<mkdir dir="${component.autogen_src}"/>
<delete>
<fileset dir="${component.autogen_src}"
excludes="**/service/**/*.java"/>
</delete>
<xjc destdir="${component.autogen_src}" target="2.0" extension="true">
<arg value="-Xcommons-lang"/>
<arg value="-Xvalue-constructor"/>
<arg value="-Xfluent-api"/>
<arg value="-Xcollection-setter-injector"/>
<arg value="-Xdefault-value"/>
<schema dir="${component.home}/src/conf/wsdl" includes="*.xsd"/>
<binding file="${component.home}/src/conf/wsdl/simple.xjb"/>
</xjc>
</target>

<!-- Auto generate interfaces and classes based on the WSDL definitions using JAXB bindings -->
<target name="wsdl_gen">
<mkdir dir="${component.autogen_src}"/>
<wsgen outputDirectory="${component.autogen_src}"
wsdl="${component.home}/src/conf/wsdl/service-foo.wsdl"
package="com.blogspot.ozgwei.service.foo"
overwrite="true"
binding="jaxb"
externalBindings="${component.home}/src/conf/wsdl/simple.xjb"
/>
<wsgen outputDirectory="${component.autogen_src}"
wsdl="${component.home}/src/conf/wsdl/service-bar.wsdl"
package="com.blogspot.ozgwei.service.bar"
overwrite="true"
binding="jaxb"
externalBindings="${component.home}/src/conf/wsdl/simple.xjb"
/>
</target>

<!-- Build all user, autogenerated and test code -->
<target name="compile" depends="wsdl_gen, xjc_gen">
...
</target>


The next is the content of the simple.xjb file for JAXB Binding, which use java.util.Calendar:

<!--
This enables the simple binding mode in JAXB.
See http://weblogs.java.net/blog/kohsuke/archive/2006/03/simple_and_bett.html
-->
<jaxb:bindings jaxb:version="2.0" jaxb:extensionBindingPrefixes="xjc"
xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<jaxb:globalBindings>
<xjc:simple/>
<jaxb:javaType name="java.util.Calendar" xmlType="xs:date"
parseMethod="javax.xml.bind.DatatypeConverter.parseDate"
printMethod="javax.xml.bind.DatatypeConverter.printDate"/>
<jaxb:javaType name="java.util.Calendar" xmlType="xs:time"
parseMethod="javax.xml.bind.DatatypeConverter.parseTime"
printMethod="javax.xml.bind.DatatypeConverter.printTime"/>
<jaxb:javaType name="java.util.Calendar" xmlType="xs:dateTime"
parseMethod="javax.xml.bind.DatatypeConverter.parseDateTime"
printMethod="javax.xml.bind.DatatypeConverter.printDateTime"/>
</jaxb:globalBindings>
</jaxb:bindings>


If you prefer to use java.util.Date instead of java.util.Calendar, define the following class and change the binding file accordingly:

package com.blogspot.ozgwei.jaxb

import java.util.Date;
import javax.xml.bind.DatatypeConverter;

public class DateConverter {

public static Date parseDate(String s) {
return DatatypeConverter.parseDate(s).getTime();
}

public static Date parseTime(String s) {
return DatatypeConverter.parseTime(s).getTime();
}

public static Date parseDateTime(String s) {
return DatatypeConverter.parseDateTime(s).getTime();
}

public static String printDate(Date dt) {
Calendar cal = new GregorianCalendar();
cal.setTime(dt);
return DatatypeConverter.printDate(cal);
}

public static String printTime(Date dt) {
Calendar cal = new GregorianCalendar();
cal.setTime(dt);
return DatatypeConverter.printTime(cal);
}

public static String printDateTime(Date dt) {
Calendar cal = new GregorianCalendar();
cal.setTime(dt);
return DatatypeConverter.printDateTime(cal);
}

}


The simple.xjb will be changed to:

<jaxb:bindings jaxb:version="2.0" jaxb:extensionBindingPrefixes="xjc"
xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<jaxb:globalBindings>
<xjc:simple/>
<jaxb:javaType name="java.util.Date" xmlType="xs:date"
parseMethod="com.blogspot.ozgwei.jaxb.DateConverter.parseDate"
printMethod="com.blogspot.ozgwei.jaxb.DateConverter.printDate"/>
<jaxb:javaType name="java.util.Date" xmlType="xs:time"
parseMethod="com.blogspot.ozgwei.jaxb.DateConverter.parseTime"
printMethod="com.blogspot.ozgwei.jaxb.DateConverter.printTime"/>
<jaxb:javaType name="java.util.Date" xmlType="xs:dateTime"
parseMethod="com.blogspot.ozgwei.jaxb.DateConverter.parseDateTime"
printMethod="com.blogspot.ozgwei.jaxb.DateConverter.printDateTime"/>
</jaxb:globalBindings>
</jaxb:bindings>

9 comments:

stoneskin said...

Is there any way of getting JAXB to generate property name constants?

E.g. you have a property

private String myProperty;

And you want to be able to reference this as a constant, i.e. there is a generated constant of

private static final String MYPROPERTY_CONSTANT = "myProperty";

? I could really do with somethign like this as we are retating XPATH expressions back to the request elements.

Alex Wei said...

It should be quite easy to write a XJC plugin to generate constants for property names. You can view the source code of other XJC plugins and quickly make up yours...

stoneskin said...

Thanks mate, I was having a look at the JAXB commons site and coming to the same conclusion.

I was surprised that no-one had created a plugin to output xpath constants though - which are extremely useful when auto-relating errors back to specific request elements.

dmaffitt said...

Have you tried to use xjc from the command line to do this instead of the ant task? I am trying to use the collection-setter-injector but cannot get xjc to recognize the pluggin?

I try "xjc -cp collection-setter-injector.jar -Xcollection-setter-injector -extensioin my.xsd" but no joy.

Any suggestions?

-dave

Luke said...

Simply wonderful Alex! This article is clearly as timely today as 3 years ago when you wrote it. If I would have started with your examples yesterday, I'd have been spared a good deal of troubleshooting with less thorough and effective examples of customized binding. Thank you for taking the time to post this.

Android app development said...

This is one of the Important post.Java is a object Oriented programming language Thanks for share with us.
Android app developers

Idealhomes International said...

Thanks for sharing helpful information, I really like your all post. I will bookmark your blog for future updates.


___________________________
Flats in portugal

Jack Stevens said...

I think mimicking popular posts on other blogs is one of the best ways to get a good idea which will be popular.Such a lovely blog you have shared here with us. Really nice.
-----------------------
Colour consultants Sydney

Samuel Jason said...

My spouse and I absolutely love your blog and find almost all of your post’s to be just what I’m looking for. can you offer guest writers to write content for you? I wouldn’t mind producing a post or elaborating on a number of the subjects you write concerning here. Again, awesome weblog!
Looking for bbw