Monday, September 2, 2013

Creating an XJC Plugin to instantiate fields

The idea to create this plug-in was inspired by a Stackoverflow question and a couple of articles on the web (here and here).
The goal here is to:
  1. See how an XJC Plug-in can be created and how to use it using maven.
  2. Create a plug-in that can instantiate fields in generated Java classes

The Example-Scenario:

The ultimate goal here is to automatically generate Java Classes from an XML Schema, with the fields automatically instantiated. So we will start with the Schema.

The Schema is a relatively simple one that I got from the Stackoverflow post mentioned above. It contains three elements, Book, Chapter and Word.


 
     
         
             
         
     
 
 
     
         
             
         
     
 
 
 
        
            
        
    



The Generated class Book.java should by using this plug-in, contain the following lines:
@XmlElement(name = "Chapter", required = true)
protected Chapter chapter = new Chapter();

In essence, chapter can never be null.

Preparation:

The first thing you need to do is create a maven project. Go to the pom file and add the following dependency:

   com.sun.xml.bind
   jaxb-xjc
   2.2.6

Now you're ready to start. Here's what my pom file looks like:


  
   4.0.0

   de.jaxbnstuff
 xjc-plugin
 0.0.1-SNAPSHOT
 jar

 xjc-plugin
 http://jaxbnstuff.blogspot.com

 

  
   com.sun.xml.bind
   jaxb-xjc
   2.2.6
  

 
 


The Plugin:

Basicall, all you need to do is implement a class that extends the com.sun.tools.xjc.Plugin class. The  com.sun.tools.xjc.Plugin class is an Abstract class that specifies three methods that describe how the plugin is to be used and what it does.

I will create the class Instantiator, that will in essence, implement my entire plugin functionality. Here's what it looks like with the empty auto-generated method stubs:

package de.jaxbnstuff.xjcplugin;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;

import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.outline.Outline;

public class Instantiator extends Plugin{

 @Override
 public String getOptionName() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public String getUsage() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public boolean run(Outline outline, Options opt, ErrorHandler errorHandler)
   throws SAXException {
  // TODO Auto-generated method stub
  return false;
 }

}

Let's go over the methods one by one:

getOptionName():
This method simply returns the "option name" that can be used to turn on this plugin. So, for instance, if the method returns "Xinst-plugin", adding the option -Xinst-plugin to the xjc command will tell xjc to run this plugin. 
I will replace the return statement with
return "Xinst-plugin" 

getUsage():
As the name suggests, this method is used to generate a usage screen and should return a description of the plugin and how to use it.

run():
The run method is where all the magic happens. It is called by XJC if the Plugin's option name is used.

The Implementation:

This what the plugin will do:
  1. First find the types of all the elements defined in the schema. In this case: Book, Chapter and Word
  2. In each Class look for all the fields of types that are defined in the schema. In this case it is only one, namely the field chapter in the class Book
  3. Instantiate that field.
And here are the few lines of code to do it:

@Override
public boolean run(Outline outline, Options opt, ErrorHandler errorHandler)
  throws SAXException {
 
 //1. Store the types of all the generated classes
 List<JType> types = new ArrayList<JType>();
 for (ClassOutline co : outline.getClasses()){
  types.add(co.implClass);
 }
 
 for (ClassOutline co : outline.getClasses()){
  //2. Look through the fields defined in each of the classes
  Map<String, JFieldVar> fields = co.implClass.fields();
  
  for (JFieldVar f : fields.values()){
   if (types.contains(f.type())){ 
    //If the type is defined in the schema
    //3. Instantiate
    f.init(JExpr._new(f.type())); 
   }
  }
 }
 
 return true;
}

Last Step:

One last thing you need to do before the plugin is ready to use:
  1. If not already there, create the directory META-INF under src/main/resources.
  2. In META-INF, create a directory name serices
  3. In services, create a file named com.sun.tools.xjc.Plugin
  4. This file should contain the fully qualified names of all classes that implement Plugin. In this case it is only one line: de.jaxbnstuff.xjcplugin.Instantiator
And there you have it, your plugin is is Done!

Using the Plugin:

Now create a new maven project, and make sure the schema is in the class path.
Open the pom and add the following:
 
  
   
    org.jvnet.jaxb2.maven2
    maven-jaxb2-plugin
    0.8.1
    
     
      
       generate
      
     
    
    
     ${basedir}/src/main/resources/schema
     de.jaxbnstuff.sample
     ${basedir}/target/generated-sources/xjc
     true
     
      -Xinst-plugin
     
     
      
       de.jaxbnstuff
       xjc-plugin
       0.0.1-SNAPSHOT
      
     
    
   
  
 



Here, were are using the maven-jaxb2-plugin to generate the classes into the package and the directory specified from the schema specified.
What this plugin basically does is call XJC. Notice however how the plugin we just created is added under <plugins> and the option to start it is added under <args>.
Now, just run mvn compile, to run xjc with the new plugin.


The Code

If you'd like to have a look at the code, or use my implementation, you can find it at my Repo on GitHub. Also, for another example, check out the next post about instantiating Lists.

Hope you find this post useful, let me know if you have any questions or tips. 

No comments:

Post a Comment