Pages

Saturday, October 11, 2008

XDoclet2 Custom Plugin ( Actionscript3 (AS3) model generator from java value objects ) and Velocity Tutorial

I work on flex and it was necessary for me to generate the flex side model classes in sync with the java side value objects. I Googled but was not able to find one that is as simple as the one written by Joe Berkovitz of Allurent. It was pretty old code and lacked configuration options for generating AS classes. I decided to write my own plugin for xdoclet2 that can be used to generate the flex code. And, here given below is a tutorial for learning xdoclet2. Since, the template engine it uses is velocity, this post also serves as a tutorial for velocity. Finally, I provide you with a complete working edition of the xdoclet2 plugin, and a sample to use the same.

Please look at the end of this tutorial to find links to the source, plugin jar and sample files.

System Configuration
1. xdoclet2 v1.0.4
2. velocity 1.5
3. JDK 5 update 15
4. ant 1.7.1

XDoclet2

XDoclet2 allows you to read the annotations on the java source files, and generate either XML or Java or any other file. It uses velocity template engine for any file type generation, and Jelly template engine for XML generation.

Step 1:

Download the xdoclet2 plugin distribution from http://sourceforge.net/projects/xdoclet-plugins/
Extract the zip or the tar.gz to an comfortable location.

Step 2:

Write the sample java class with the annotations that needs to be supported by the plugin. In my case I wrote Model.java. A sample snippet is given below:

package com.ssb.sample;
import java.math.BigDecimal;
import java.math.BigInteger;
/**
*
* @author Shreyas
* @as3.class name="Model" generate="true" generate-bindable-metadata="true" generate-remote-class-metadata="true"
*/
public class Model {
    private int i;
    private double d;
    private long l;
    /**
     * @as3.field type="Number" import="false" generate-bindable-field-metadata="true"
     */
    private BigDecimal bdecimal;
    /**
     * @as3.field type="mx.flex.BigInt" import="true"
     */
    private BigInteger binteger;

    /**
     * @as3.field type="mx.flex.BigInt" generate="false"
     */
    private transient BigInteger doNotGeneratebinteger;
    .
    .
    .
Step 3:

Define Tag validators. This is helpful for validating if the annotations provided are as anticipated. Tag validators extends DocletTag as shown below. The interface itself is annotated using the qtags.

package com.ssb.plugin.as3.qtags;
import com.thoughtworks.qdox.model.DocletTag;
/**
*
* @qtags.location class
* @qtags.once
*
*/
public interface As3ClassTag extends DocletTag{
    /**
     * @qtags.required
     */
    String getName_();
    /**
     * @qtags.default true
     */
    String isGenerate();
    /**
     * @qtags.default true
     */
    String isGenerateBindableMetadata();
    /**
     * @qtags.default false
     */
    String isGenerateRemoteClassMetadata();
}

There are some points to be noted:
a. There is a one to one matching between the name of the interface - As3ClassTag -  and the annotation on the sample class -as3.class-. The First letter is capitalized, the '.' is removed, the next letter is also capitalized, a 'Tag' is appended.
b. The methods in the interface uses the standard java bean convention. For each attribute in the tag, 'get' or 'is' is prefixed to the attribute name with the first letter capitalized.
c. 'name' is a special attribute, a '_' should be used at the end of the getter to avoid internal naming collision.
d. The attribute when contains '-', the methods in the interface nelects it and follows the first letter capitalization rule.
e. Do not use camel casing in the sample annotation. Use only small letters or hyphens to separate for ease of work.

Definitions of tags:
a. qtags.location Whether the tag applies to class level or field level or method level, repectively, we have class, field, method as its values.
b. qtags.once The tag can be used only once in teh source file.
c. qtags.required The field is required.
d. qtags.default The default value of the field.

Step 4:

Generate the tag validator implementation and tag library. The tag library will be used by our plugin. A ant script is used to generate these items. The target to generate is given below. Please look at source code to get the entire build script.

<target name="gen.qtags.impl">
      <property name="xdoclet.qtags.namespace" value="as3"/>

      <xdoclet>
           <fileset dir="src">
             <include name="**/*.java"/>
           </fileset>
           <component
              classname="org.xdoclet.plugin.qtags.impl.QTagImplPlugin"
              destdir="${basedir}/src"
           />   
           <component
            classname="org.xdoclet.plugin.qtags.impl.QTagLibraryPlugin"
                destdir="${basedir}/src"
                packagereplace="com.ssb.plugin.${xdoclet.qtags.namespace}.qtags"
           />
      </xdoclet>
</target>

Step 5:

Write the xdoclet2 plugin. As3Plugin extends QDoxPlugin.

package com.ssb.plugin.as3;
import java.util.*;
import org.apache.log4j.Logger;
import org.generama.*
import com.ssb.plugin.as3.qtags.TagLibrary;
import com.thoughtworks.qdox.model.*;

public class As3Plugin extends QDoxPlugin{
    private Map<String, String> typeMap = new HashMap<String, String>();
    public As3Plugin(VelocityTemplateEngine templateEngine,
            QDoxCapableMetadataProvider metadataProvider,
            WriterMapper writerMapper) {
        //Call the superclass constructor.
        super(templateEngine, metadataProvider, writerMapper);
        ..
        ..
        ..

In the above snippet, the constructor takes VelocityTemplateEngine as argument. Do not bother how it gets that. The xdoclet is responsible for injecting it to the constructor. First, call the super class constructor.

        //Replace .java with .as extensions
        setFileregex(".java");
        setFilereplace(".as");
        //Set Multiple file output to true.
        setMultioutput(true);
        //Instantiate the generated tag library.
        new TagLibrary(metadataProvider);
        //Initialize the the type map
        initTypeMap();
The comments are pretty clear for the above code snippet. This ends the constructor.

The initTypeMap method is shown below.

    /**
     * Initializes the type map.
     */
    protected void initTypeMap() {
        typeMap.put("int", "Number");
        typeMap.put("double", "Number");
        typeMap.put("long", "Number");
        typeMap.put("java.lang.Short", "Number");
        typeMap.put("java.lang.Byte", "Number");
        ..
        ..
        ..
    }

    /**
     * Over ridden method, determines whether the given java class should be converted to as3 or not.
     *
     * @param metadata A java Class.
     * @return
     */
     public boolean shouldGenerate(Object metadata) {
            JavaClass javaClass = (JavaClass) metadata;
            boolean ignore = "false".equalsIgnoreCase(javaClass.getNamedParameter("as3.class","generate"));
            if (!ignore)
              return true;
            else
              return false;
     }
The above method decides whether AS files should be generated or not for the annotated java file. This is called by xdoclet and the Object argument is a JavaClass type. The getNamedParameter takes the annotation name and the attribute whose value has to be retrieved. If, it is false then do not generate the AS file.

    protected void populateContextMap(Map map) {
        super.populateContextMap(map);
        map.put("tagUtil", new TagUtil());
    }

The above code initializes the context map by adding tagUtil, so that it can be accessed by velocity template.

Now for each of the annotation and its attribute write a public method to get its value. This is present in TagUtil.java. Remember, it is public. Protected will not work, as this will be later used by the Velocity. Here below are sample for two of them:

    /**
     * Gets the As3 Class name from the annotation as3.class, attribute 'name'
     *
     * @param metadata The java class.
     * @return
     */
    public String getAs3Name(Object metadata){
        JavaClass javaClass = (JavaClass) metadata;
        return javaClass.getNamedParameter("as3.class","name");
    }
    /**
     * Gets the Field type from the annotation as3.field, attribute 'type'
     *
     * @param metadata The java Field.
     * @return
     */
    public String getTypeName(Object metadata){
        JavaField javaField = (JavaField)metadata;
        return getTagValue("as3.field", "type", javaField);
    }   
    private String getTagValue(String tagName,String tagAttribute, JavaField field){       
        return field.getNamedParameter(tagName, tagAttribute);
    }
This is it!! Really, this is all it takes to write a XDoclet related code for the plugin. Now, we have to integrate it with velocity to generate the output AS files.

Step 6:

Write the Velocity template file for Code generation.

Velocity

Step 1: Create a As3Plugin.vm file at com.ssb.plugin.as3 package, same package as the plugin. The file name must identical to the plugin name.

Step 2: Write the velocity code in the vm file. It is shown below as snippets.

// ${dontedit}
#set( $class = $metadata )
#set( $truevalue = "true")

The ${} are variables that can hold values in the VTL(Velocity template language). #set is used to assign values. In working with xdoclet, the $metadata is injected from xdoclet and holds the class name. $truevalue is assigned string 'true';

package $plugin.getDestinationPackage($class);
{

#foreach($field in $class.getFields())
#set($import = $plugin.shouldImport($field))
  #if($import != "false")
    import $import;
  #end
#end
#if($tagUtil.isGenerateBindableMetadata($class) == $truevalue)
    [Bindable]
#end
..
..

Other variables that are available to the velocity are $plugin and $tagUtil. $plugin holds the instance of the As3Plugin class. Velocity can be used to invoke java methods through this variable, passing any arguments if required. The getDestinationPackage method is present in the base class of the As3Plugin. It gives the package of the java class. The package of the AS class will be same as this one.
#foreach is a directive to loop in velocity. Velocity supports #if#elseif#else#end directive also. Any method you see being invoked on $plugin is actually being invoked on As3Plugin java side object. So, shouldImport calls the As3Plugin classes shouldImport method. Similarly, isGenerateBindableMetadata calls the TagUtil classes isGenerateBindableMetadata method.

Velocity supports writing methods, by defining macro's etc, but that is not needed for writing this template file. That's all is velocity!! Its the most basic stuff in velocity that is being used here. Please refer velocity user manual for more detailed information( http://velocity.apache.org/engine/devel/user-guide.html ).

Every thing needed for our plugin is now in place. Just compile the classes, include all the resources and build the plugin jar. The build script is provided in the source.

To use the plugin, write a ant build script as given below:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="Test AS3 Plugin Jar" default="main">

    <property file="build.properties"/>
    <path id="xdoclet.task.classpath">
        <!-- xdoclet2 runtime dependencies -->

          <fileset dir="${xdoclet.plugin.install.dir}/lib">
            <include name="**/*.jar"/>
          </fileset>
        <pathelement location="${basedir}/lib/as3-plugin.jar"/>

    </path>

    <!-- Define xdoclet task -->
     <taskdef
        name="xdoclet" classname="org.xdoclet.ant.XDocletTask"
         classpathref="xdoclet.task.classpath"
     />
    <target name="main" depends="generate"/>
    <target name="generate">
        <xdoclet>
        <fileset dir="${basedir}/src">
             <include name="**/*.java"/>          
         </fileset>
        <component classname="com.ssb.plugin.as3.As3Plugin"
                   destdir="${basedir}/src"/>
        </xdoclet>
     </target>
</project>

build.properties contain:
xdoclet.plugin.install.dir = E:/DevToolsInstalled/xdoclet-plugins-dist-1.0.4

This completes the tutorial for writing xdoclet2 plugin using velocity template engine. This generates non java files, .as, as output.

Resources:

Go to: https://github.com/shreyaspurohit/AS3Generator

1. Download the latest plugin jar from dist folder (as3-plugin.jar) to use the lib.

2. Download the latest source of the plugin by cloning git (or download zip). Edit the build.properties to provide the xdoclet2 installation directory.
Run ant on build.xml to generate the as3-plugin.jar in the base directory.

3. Download the latest samples from sample folder by cloning git (or download zip). Edit the build.properties to provide the xdoclet2 installation directory.
Run ant on build.xml to generate Model.as, Model2.as at com.ssb.sample.

4. Download the latest java doc from folder api on git by cloning or downloading zip.

Please read my next post to understand the capabilities of the plugin.

No comments:

Post a Comment