Total Pageviews

Monday, September 25, 2006

Annotation processing with APT-Jelly


There's not much documentation about APT-Jelly out there, although the docs on the homepage are very good I decided to write a short tutorial. APT-Jelly seems to me a little bit underrated and maybe this blog entry helps it to gain more attention .

Tutorial

Given the case we want to generate local and remote interfaces for our EJB3 business class implementation. Java 5 introduces a cool tool called APT (Annotation Processing Tool) which helps the developer in generating Java source files from annotations. Unfortunately it is (just my opinion) some kind of hard work to generate those classes. You have to implement an annotation processor and walk through all of the relevant types and methods that are annotated to generate the source files. At the latest now file templates (for the Java source files) seem to be the right choice in combination with a powerful templating engine like Velocity or Freemarker.
This is the time when APT-Jelly steps in.
APT-Jelly is an engine for generating artifacts (e.g. source code,
config files) from Java source code. APT-Jelly provides a
template-oriented approach to artifact generation by providing an interface for Sun's
Annotation Processing Tool (APT) to a templating engine. Currently, APT-Jelly has direct support for both Jakarta
Commons Jelly
and Freemarker, and indirect support for Velocity
(through the merging capabilities of Jelly).
This tutorial focusses on writing Freemarker templates with APT-Jelly.
Two types of annotations are relevant for our approach: A Type annotation and a Method annotation. Let's start with writing the type annotation.
The type annotation declares, that our business class might expose its methods to either a remote or a local interface or maybe to both interfaces. It might also be helpful that the generated interface extends other interfaces. So here is the "BusinessInterface" annotation declaring the above mentioned elements:
Annotation source file: BusinessInterface.java
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(value = RetentionPolicy.SOURCE)
@Target(value = ElementType.TYPE)
public @interface BusinessInterface {

String local() default ""; // FQCN of the local interface String localExtends() default ""; // FQCN of the interface the local interface should extend String remote() default ""; // FQCN of the remote interface String remoteExtends() default ""; // FQCN of the interface the remote interface should extend
}
The 2nd annotation we are going to implement is a method annotation. We would like to declare, whether the annotated method should be a member of the local or the remote interface or maybe both. So here is the "InterfaceMethod" annotation.
Annotation source file: InterfaceMethod.java
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(value = RetentionPolicy.SOURCE)
@Target(value = ElementType.METHOD)
public @interface InterfaceMethod {

boolean viewTypeLocal() default true; boolean viewTypeRemote() default true;
}
Now let's create a Freemarker template in conjunction with APT-Jelly and get the whole thing running:

The template: BusinessInterface.fmt
<@forAllTypes devar="type">
<@ifHasAnnotation var="ann" annotation="BusinessInterface">
<#if (ann.remote?length > 0)>
<@javaSource name="${ann.remote}">
<#assign interfaceName = ann.remote?substring(ann.remote?last_index_of(".") + 1)>
package ${type.package};
@javax.ejb.Remote
public interface ${interfaceName} <#if (ann.remoteExtends?length > 0)>extends ${ann.remoteExtends} {

<@forAllMethods var="method" annotation="InterfaceMethod" annotationVar="annotatedMethod" returnTypeVar="returnType">
<#if (annotatedMethod.viewTypeRemote = true)>
${method.returnType} ${method.simpleName}(<@forAllParameters var="param" indexVar="paramIndex"><#if paramIndex = 0>${param.type} ${param.simpleName}<#else>, ${param.type} ${param.simpleName})<@forAllThrownTypes var="exception" indexVar="exceptionIndex"><#if exceptionIndex = 0> throws <#else>, ${exception};



}


<#if (ann.local?length > 0)>
<@javaSource name="${ann.local}">
<#assign interfaceName = ann.local?substring(ann.local?last_index_of(".") + 1)>
package ${type.package};
@javax.ejb.Local
public interface ${interfaceName} <#if (ann.localExtends?length > 0)>extends ${ann.localExtends} {

<@forAllMethods var="method" annotation="InterfaceMethod" annotationVar="annotatedMethod" returnTypeVar="returnType">
<#if (annotatedMethod.viewTypeLocal = true)>
${method.returnType} ${method.simpleName}(<@forAllParameters var="param" indexVar="paramIndex"><#if paramIndex = 0>${param.type} ${param.simpleName}<#else>, ${param.type} ${param.simpleName})<@forAllThrownTypes var="exception" indexVar="exceptionIndex"><#if exceptionIndex = 0> throws <#else>, ${exception};



}



If you are familiar with Freemarker most of the stuff should be easy to understand. The interesting stuff are the custom directives of APT-Jelly. Those directives start with a "@". The Freemarker built-in functions start with a "#".
The lines:
<@forAllTypes var="type">
<@ifHasAnnotation var="ann" annotation="BusinessInterface">

iterates through all types (classes) with a type annotation called "BusinessInterface" we have developed earlier in this tutorial.
<#if (ann.remote?length > 0)>
Now it's interesting to see, whether we have to generate local, remote or both interfaces. We start by looking up the remote element. If the element's value length is greater 0 we assume that we have the FQCN of the remote interface and we can start generating a Java source file with this name:
<@javaSource name="${ann.remote}">
I leave out the generation of the class header (interface xxx extends yyy, etc..) and go straight on to generating the methods:
<@forAllMethods var="method" annotation="InterfaceMethod" annotationVar="annotatedMethod" returnTypeVar="returnType">
We have to iterate through all methods that are annotated with the annotation "InterfaceMethod" we developed earlier. Now we have to construct the interface methods.
<#if (annotatedMethod.viewTypeRemote = true)>

${method.returnType} ${method.simpleName}(
<@forAllParameters
var="param" indexVar="paramIndex"><#if paramIndex =
0>${param.type} ${param.simpleName} <#else>, ${param.type}
${param.simpleName}
)
<@forAllThrownTypes
var="exception" indexVar="exceptionIndex"><#if exceptionIndex =
0> throws <#else>,
${exception}
;


This expression looks very confusing (especially unformatted in this blog entry), but it is pretty simple. We determine, if the annotated method's viewType is remote (referencing the "annotationVar" variable in the @forAllMethods directive) and in this case we insert all the relevant parameters with the APT-Jelly @forAllParameters directive.
The last thing to do is adding the throws clause in case the method throws an exception (@forAllThrownTypes).
The second part of the template repeats the generating stuff for the local interfaces. The solution is not perfect, but it works and shows some of the power of APT-Jelly.

The last thing to do now is execute APT and generate the sources. There are at least 4 jar archives which must be added to the classpath when calling apt:
  1. apt-jelly-core
  2. apt-jelly-freemarker
  3. freemarker
and the jar containing the annotations we have developed for generating the business interfaces (ejb3-annotations-1.0-SNAPSHOT.jar). Assuming all the jars live in the directory e:/temp and the sources of the ejb business implementation live under the directory "ejb/", we can generate the interfaces for the business implementations with the following apt call:
apt -cp e:/temp/apt-jelly-core-2.0.1.jar;e:/temp/apt-jelly-freemarker-2.0.1.jar;
e:/temp/freemarker-2.3.8.jar;e:/temp/ejb3-annotations-1.0-SNAPSHOT.jar -s target/gen -factory net.sf.jelly.apt.freemarker.FreemarkerProcessorFactory -Atemplate=BusinessInterface.fmt ejb/*.java

This will generate the business interfaces in the specified target directory (target/gen).
So let's have a look at some sample Java code using the above developed annotations. Assuming the following business class implementation:
package ejb;

import java.util.HashMap;

@BusinessInterface(remote = "ejb.InterfaceTestRemote",
remoteExtends = "ejb.InterfaceBase",
local = "ejb.InterfaceTestLocal")
public class InterfaceTestImplementation {
@InterfaceMethod
public void testDummy() throws Exception {

}

@InterfaceMethod
public String testDummy2(String dummyString) throws IllegalStateException, IllegalArgumentException {
return "";
}

@InterfaceMethod(viewTypeLocal = false)
public String testDummy3(String dummy2String, HashMap map) {
return "";
}

@InterfaceMethod(viewTypeLocal = false, viewTypeRemote = true)
public void testDummy4() {

}

@InterfaceMethod(viewTypeLocal = true, viewTypeRemote = true)
public void testDummy5() {
}

@InterfaceMethod(viewTypeLocal = false, viewTypeRemote = false)
public void testDummy6() {
}

public void testDummy7() {
}

@InterfaceMethod
public void testDummy8() {
}
}

We declare some of the methods as interface methods, some of it should appear in the remote and some of it should appear in the local interface. There are also methods which should not become a member of the generated interfaces (testDummy6 and testDummy7).
Running apt with the above mentioned command would generate the following two Java business interfaces:
InterfaceTestRemote.java
package ejb;

@Remote
public interface InterfaceTestRemote extends ejb.InterfaceBase {
void testDummy() throws java.lang.Exception;
java.lang.String testDummy2(java.lang.String dummyString) throws java.lang.IllegalStateException, java.lang.IllegalArgumentException;
java.lang.String testDummy3(java.lang.String dummy2String, java.util.HashMap map);
void testDummy4();
void testDummy5();
void testDummy8();
}

InterfaceTestLocal.java
package ejb;

@Local
public interface InterfaceTestLocal {
void testDummy() throws java.lang.Exception;
java.lang.String testDummy2(java.lang.String dummyString) throws java.lang.IllegalStateException, java.lang.IllegalArgumentException;
void testDummy5();
void testDummy8();
}

The generated remote interface extends an already existing base interface called ejb.InterfaceBase. In a next trail I will explain how to generate those base interface as well (as proposed in the book "Enterprise Java Beans") and how to use macros (some sort of methods) in a Freemarker template.
APT-Jelly really seems to simplify a lot!