XJC (the XML Schema-to-Java conversion tool supplied with Java JDKs) does a fairly good job of creating readable and useful Java classes from XSDs, but when you have a large set of schemas and want to make widespread and repeatable changes to the generated objects, you're limited to what comes with XJC -- or at least that's what I thought.

It turns out that if you create a class that extends com.sun.tools.internal.xjc.Plugin and create a java.util.ServiceLoader entry for your class, XJC will find your extension and incorporate it into the class generation process.

This tutorial is a good place to start, and details of the code generation API can be found here. The source code for the plugin, a demo XSD-to-Java ant project, and a JUnit project (all for Eclipse Kepler) are included in the attached zip file.

The Problem

For this example, I will show how to implement copy constructors that handle inheritance and simple vs complex type assignments for all generated classes. Note that this example code may not be production-ready, so make sure you test it thouroughly.

In the demo Person.xsd schema attached, the main players are Person, BaseAddress, USAddress, and GlobalAddress. Notice that USAddress and GlobalAddress both extend BaseAddress:

<> name="BaseAddress">
 <>>
 <> name="Addr1" type="string">>
 <> minoccurs="0" name="Addr2" type="string">>
 <> name="City" type="string">>
 >
>
 
<> name="USAddress">
 <>>
 <> base="tns:BaseAddress">
 <>>
 <> name="State" type="string">>
 	<> name="Zip" type="string">>
 >
 >
 >
>
 
<> name="GlobalAddress">
 <>>
 <> base="tns:BaseAddress">
 <>>
 <> name="PostalCode" type="string">>
 <> name="Country" type="string">>
 >
 >
 >
>

and that Person has a mix of simple (age) and complex (Name, HomeAddress, ShippingAddress) elements.

<> name="Person">
 <>>
 <> name="Name" type="tns:Name">>
 <> minoccurs="0" name="age" type="int">>
 <> name="HomeAddress" type="tns:BaseAddress">>
 <> minoccurs="0" name="ShippingAddress" type="tns:BaseAddress">>
 >
>

By default, the version of XJC I used generated the following properties for the Person class, and did not generate any constructors:

@XmlElement(name = "Name", required = true)
protected Name name;
protected Integer age;
@XmlElement(name = "HomeAddress", required = true)
protected BaseAddress homeAddress;
@XmlElement(name = "ShippingAddress")
protected BaseAddress shippingAddress;

The Design

The copy constructors should generate assignments that take in to account the following rules. In the list of rules, 'this' class refers to the class being modified, and 'parameter class' refers to the runtime class of the parameter sent into the constructor. I'll also make use of 'field class' and 'parameter field class' to refer to the class of a member of 'this' class or the 'parameter class', respectively.

The custom plugin code that generates the constructors follows this logic ('add' means add to the generated code in the constructor):

  1. Always add a default (no parameter) constructor that calls super().
  2. Always add a constructor that takes an instance of 'this' class as its sole parameter.
  3. If 'this' class extends one that is included in this XJC build, add a call to super(x), where x is the parameter sent in to the constructor.
  4. Add an 'if' statement to check if the parameter is null. If it is, add an immediate return statement.
  5. Loop through the non-static and non-final fields in 'this' class:
  6. If this.field.class is not one that is included in this XJC build, (java.lang.String, for instance), add a direct assignment: this.field = parameter.field;
  7. If this.field.class is one that is included in this XJC build, add a check to make sure it's not null.
  8. If the parameter.field value is not null, add an 'if' statement to check if this.field.class is identical to the runtime class of parameter.field.
  9. If the classes are identical, add a 'new' statement to the 'then' clause: this.field = new This(parameter);
  10. If the classes are not identical, we have a subclass and must add code to the 'else' clause to get the single argument constructor Parameter.class(Parameter.class) and assign it to this.field: this.field = parameter.field.getClass().getConstructor(parameter.field.getClass()).newInstance(parameter);
  11. If the dynamic constuctor fails, throw an IllegalArgumentException.

The Results

When I added the copy-constructor plugin, XJC also generated the following constructors for the BaseAddress and GlobalAddress classes; you can see the super(x) call in action:

public BaseAddress(BaseAddress x) {
 if (x == null) {
 return ;
 }
 addr1 = x.addr1;
 addr2 = x.addr2;
 city = x.city;
 }
 
public GlobalAddress(GlobalAddress x) {
 super(x);
 if (x == null) {
 return ;
 }
 postalCode = x.postalCode;
 country = x.country;
 }

Notice that the simple types are directly assigned and that each class is responsible for initializing its own fields. Since GlobalAddress extends BaseAddress, rule #3 applies and the super(x) call was added. Rule #6 applies to all of the fields in both BaseAddress and GlobalAddress, so all of the assignments are simple.

The constructors in the Person class, however, are a bit more involved:

 public Person() {
 super();
 }
 
 public Person(Person x) {
 if (x == null) {
 return ;
 }
 if (x.name!= null) {
 if (Name.class.equals(x.name.getClass())) {
 name = new Name(x.name);
 } else {
 try {
 name = x.name.getClass().
 getConstructor(x.name.getClass()).newInstance(x.name);
 } catch (java.lang.Exception exception) {
 throw new java.lang.IllegalArgumentException(exception);
 }
 }
 }
 age = x.age;
 if (x.homeAddress!= null) {
 if (BaseAddress.class.equals(x.homeAddress.getClass())) {
 homeAddress = new BaseAddress(x.homeAddress);
 } else {
 try {
 homeAddress = x.homeAddress.getClass().
 getConstructor(x.homeAddress.getClass()).
 newInstance(x.homeAddress);
 } catch (java.lang.Exception exception) {
 throw new java.lang.IllegalArgumentException(exception);
 }
 }
 }
 if (x.shippingAddress!= null) {
 if (BaseAddress.class.equals(x.shippingAddress.getClass())) {
 shippingAddress = new BaseAddress(x.shippingAddress);
 } else {
 try {
 shippingAddress = x.shippingAddress.getClass().
 getConstructor(x.shippingAddress.getClass()).
 newInstance(x.shippingAddress);
 } catch (java.lang.Exception exception) {
 throw new java.lang.IllegalArgumentException(exception);
 }
 }
 }
 }

For the Person class, rule #3 does not apply, so there is no 'super(x)' call in constructor. For the 'age' field, the assignment generated by rule #6 will be executed. For the objects that were generated in the XJC run, rules #7-#11 apply. For the Name field, the 'then' clause generated by rule #9 will be executed since Name does not extend an XJC-generated object. For both BaseAddress fields, the 'else' clause generated by rule #10 will be generated to create a new USAddress or GlobalAddress depending on the runtime type of the given parameter.

Going Farther with XJC Plugins

In the next blog post, I'll show how to use custom external binding tags in conjunction with an XJC plugin to apply customizations with near-surgical precision. Stay tuned!

Thanks to Barry Speas for reviewing early revisions of this code and making some great suggestions.