Sunday, September 8, 2013

Creating an XJC Plugin to instantiate Lists

In my previous post, I described how you can create an XJC Plugin and how to use it from maven. I will briefly go over the steps one more time, before explaining how to find and instantiate lists.

Creating the Plugin:

The steps are:

  1. Create a new Maven project, and add the jaxb-xjc dependency to your pom.
  2. Create a Class that extends com.sun.tools.xjc.Plugin.
  3. Implement the method getOptionName() to return the Option name you want used to turn on the plugin.
  4. Implement the logic of the plugin in the run() method.
  5. Go to (or create) the folder src/main/resources/META-INF/services in your project.
  6. In folder services, create a file named "com.sun.tools.xjc.Plugin"
  7. In that file, add the fully qualified name of your Plugin class.
For the detailed steps check my previous post.


The Implementation

Override the run() method with the following:


 @Override
 public boolean run(Outline outline, Options opt, ErrorHandler errorHandler)
   throws SAXException {
    
    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()){
      
      JClass fClass = (JClass) f.type();
      
      if (fClass.getTypeParameters()!=null && 
        fClass.getTypeParameters().size()==1){
       
       // f.type() is a list
       JType inner = fClass.getTypeParameters().get(0);
       
       
       f.init(JExpr._new(co.parent().getCodeModel()
         .ref(ArrayList.class).narrow(inner)));
       
       replaceGetterNoInst(co, f); 
      }
      
     }
    }
  return true;
 }

In the run() method, we iterate over the fields in a class, and check if they are parameterized or generic. If they have exactly one type parameter, we conclude its a list.

Once a list is found, we can instantiate the field with the init() method. The rest of the expression is:

  • JExpr is a factory class that helps generate several JExpressions
  • _new() creates the new operator
  • ref() returns a JType for the given class
  • narrow() sets the parameter type.
One last thing to remember, the Plugin actually runs after XJC is done generating the classes. This means that the getter method for the field just initialized already exists. And as you probably know, the getter method for a List always checks if the field is null and if it is, it instantiates it. 

Now this of course has no negative effect what so ever, however it is unnecessary. I therefore added the method replaceGetterNoInst(), which you've probably noticed in the code  above.

What it does is find the getter for the list and remove it. Then add a new one that does not check if the List is null. Here it is:
/**
  * Replaces the getter of field f, in classOutline co with one that does not
  * check if the field is null.
  * 
  * @param co the ClassOutline of the class in which the getter is replaced
  * @param f the Field for which the getter is replaced
  */
 private void replaceGetterNoInst(ClassOutline co, JFieldVar f) {
  //Create the method name
  String get = "get";
  String name  = f.name().substring(0, 1).toUpperCase() 
    + f.name().substring(1);
  String methodName = get+name;
  
  //Find and remove Old Getter!
  JMethod oldGetter = co.implClass.getMethod(methodName, new JType[0]);
  co.implClass.methods().remove(oldGetter);
  
  //Create New Getter
  JMethod getter = co.implClass.method(JMod.PUBLIC, f.type(), methodName);
      
  getter.body()._return(JExpr.ref(f.name()));
 }

Done!

So there you have it. If you'd like to take a look at the source code for the Plugin, check out my GitHub Repo at https://github.com/walmir/xjc-inst

If you've got any questions feel free to comment here. If you find bugs or have improvement ideas/requests please submit them using the issue tracker at github.

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.