apt-jelly

  soothes irritation and swelling caused by abrasive annotation processing


   

Introduction

This page was intended as a reference guide for developers who are familiar with a templating engine, APT, and the Mirror API and who have grokked the getting started guide and who need to know more detail in order to do some real development. It's expected that developers will need to periodically refer to this guide (and most especially to the directive documentation and the decorated mirror API documentation) to know how to use APT-Jelly in their templates.

The example template snippets of this reference are Freemarker snippets, although each example is equally applicable to Jelly.



Execution Context

When the ProcessorFactory is asked for a Processor, it first establishes an execution context. Most APT-Jelly directives assume that they are being invoked within this execution context so they can reference the AnnotationProcessorEnvironment as needed.

It's important to note that the execution context will direct all output to System.out unless it is redirected somewhere else like a flat file or java source file (see the directive documentation). While it is perfectly acceptable to use any available native directives to redirect output to a file on the local filesystem, it is assumed that most redirection will happen to an apt-generated artifact using the javaSource and/or file directives. This way, the files will be created relative to the directory specified by the "-s" option given to APT.

Data Model

Before invoking any templates, a data model is initialized with the options of the AnnotationProcessorEnvironment being assigned to the variable "aptOptions". Developers can look up any options passed to APT by referencing this variable. For example, to output the value of the "-AMyAPTOption" option, the following snippet can be used:

NOTE: When using the Freemarker processor, there is an option, -AAPTJellyFreemarkerLibraryNS, that specifies the namespace under which the "aptOptions" variable can be put.

      ${aptOptions["-AMyAPTOption"]}
    

The other important feature enabled by the data model is that any variable that is assigned a value that is an instance of com.sun.mirror.declaration.Declaration, com.sun.mirror.type.TypeMirror, and com.sun.mirror.declaration.AnnotationMirror is decorated before being assigned to the variable. See Decorated Mirror API for more details.



APT-Jelly Directives

APT-Jelly includes a set of directives that are used to process a set of java source files. Most of the directives are used to loop through the declarations and and types of the java source files and assign each item in the loop to a variable in the data model. Three notable exceptions to this are the file directive, the javaSource directive, and the annotationValue directive. The first two directives are used to redirect the output to a generated artifact. The third is used to output an annotation if it exists, or none if it doesn't exist.

NOTE: When using the Freemarker processor, there is an option, -AAPTJellyFreemarkerLibraryNS, that specifies the namespace under which the transforms can reside.

This section is intended to give a brief overview of the way the directives are used. For specific details on the use of each directive, refer to the directive documentation.

A good place to start is by generating an artifact with the file directive or the javaSource directive. As should be expected, the file directive generates a file and the javaSource directive generates a java source file. The name of the artifact is specified by the "name" parameter. For a java source artifact, the name should be the fully-qualified name of the java class file for which this is to be the source. For the file directive, the name should be the simple name of the file, but this directive also allows parameters for the character set and the package relative to which this file should be placed. The body of each of these directives is redirected to their respective artifacts.

The java source declarations can be traversed using the declaration loop directives. A declaration loop directive is a directive that iterates through a set of declarations, possibly refering to a "parent" declaration to obtain this set. A parent declaration is a declaration within which a set of other declarations is specified. For example, the parent declaration of a method declaration would be a type declaration (e.g. a class or an interface). A declaration loop directive provides a parameter for specifying the name of the data model variable to which should be assigned a reference to the current declaration in the loop, and a parameter for specifying the name of the data model variable to which should be assigned the current loop index. The set of declarations over which a declaration loop directive iterates can also be filtered by the class name of an annotation using the "annotation" parameter.

You will probably want to start by iterating over the set of all type declarations in your source base with the forAllTypes directive, although another good place to start would be with the forAllPackages directive if you want that level of granularity. Refering to the APT Mirror API documentation for TypeDeclaration, you will see that the set of type declarations is the set of all classes and interfaces (including enums and annotation types). Note also that the documentation for the forAllTypes directive states that interfaces aren't included by default. If you want to include interfaces, you'll want to specify that with the "includeInterfaces" parameter.

Once within a forAllTypes directive, you can use other loop directives to iterate through each of the member declarations of the current type. You'll want to do this through the use of the forAllConstructors, forAllFields, forAllMethods, and forAllNestedTypes directives. You'll note in the documentation for these directives that it is possible to specify the parent declaration, but this is only necessary if these directives aren't nested within a declaration loop directive for a set of type declarations.

When traversing a source base, eventually you will encounter a reference to a type (i.e. TypeMirror) that may or may not be declared. You will want to be aware of the difference between a type declaration and a reference to a type in your template because they are modeled quite differently in the mirror API (and rightly so). While some types may have declarations (e.g. a java class), others may not have declarations (e.g. arrays, primitive types, and the void type). Whether a type has a declaration is important to know because certain operations are only valid on the declarations of those types. For example, you cannot determine whether a type reference is annotated with a specified annotation; only type declarations can be annotated. To determine whether a type has a declaration, use the ifHasDeclaration directive.

A good example of this is when looking at a method declaration. There are APT-Jelly directives that traverse through the constructs of a method declaration. These are forAllParameters, and forAllThrownTypes. There is also a parameter on the forAllMethods directive that allows you to specify the name of a data model variable to which to assign the return type of the method. As an illustration of the difference between a reference to a type and a type declaration, the parameters can be annotated (and thus filtered by a given annotation), but the thrown types of a method cannot. To determine whether a given thrown type is annotated with a specified annotation, a reference to the declaration of the thrown type must be obtained. The recommended way to do this is with the ifHasDeclaration directive.



Decorated Mirror API

When writing a template, you're eventually going to want to do more than just iterate through the declarations of your Java source base. You may want to output the name of a declaration, or perhaps the value of an annotation element. This is done through reference to the properties of a data model variable. Therefore, a knowledge of the model for the objects in the data model is imperative when writing your template.

The Mirror API is the object model that is provided by APT. Each of the declarations and type mirrors in the Mirror API provides a set of relevant properties that can be accessed in the template. However, because the Mirror API has been found to be inadequate or inconvenient to access in the data model, APT-Jelly decorates the Mirror API with other property accessors that make templating more easy.

For example, the Mirror API provides a property on AnnotationMirror that is a map of the element (method) declarations to the values of those elements on the annotation. While this API is accurate and concise, it becomes difficult to reference the annotation value from the template. APT-Jelly decorates AnnotationMirror by adding properties for each of the elements of the annotation so they can be referenced from the template. To illustrate, consider the following two template snippets. Each snippet outputs the value of the "bar" element of the annotation "com.foo.Foo". The first snippet uses the Mirror API as if it were not decorated. The second leverages the API as decorated by APT-Jelly.

      @com.foo.Foo {
      bar = "hello"
      }
      public class MyClass {
      }
    

Example of a class annotated with com.foo.Foo.

      <#list annotation.elementValues.entrySet as entry>
      <#if entry.key.simpleName = "bar">
      ${entry.value}
      </#if>
      </#list>
    

Freemarker template snippet that outputs "hello" without using the decorated API. The context variable "annotation" is assigned the value of the com.foo.Foo AnnotationMirror featured in the example source code above.

      ${annotation.bar}
    

Freemarker template snippet that outputs "hello" using the decorated API. Again, the context variable "annotation" is assigned the value of the com.foo.Foo AnnotationMirror featured in the example source code above.

For more information on the decorated API, refer to the decorated mirror API documentation.



Extending APT-Jelly

If APT-Jelly is missing a feature that you need, consider submitting a patch or making a request on the development mailing list. However, in some cases, it may be more convenient for developers to extend APT-Jelly with their own directives. For example, you may find it necessary to extend the forAllMethods directive to iterate through your own special set of methods that meet certain specific requirements. Fortunately, APT-Jelly boasts a clean, modular, and flexible API and can be very easily extended.

There is a set of directive classes for each templating engine, but all directives share the same common logic in the form of a strategy class. For example, the directive class for the forAllMethods directive to support Freemarker is net.sf.jelly.apt.freemarker.transforms.ForAllMethodsTransform. The class that support the same directive in Jelly is net.sf.jelly.apt.tags.ForAllMethodsTag. But both of these directive classes are nothing more than a thin wrapper around a common strategy class, net.sf.jelly.apt.strategies.MethodDeclarationLoopStrategy.

Whenever you find it necessary to extend an existing APT-Jelly directive take a good look at the existing strategies (and their methods) to see if you can meet your requirements with a simple override. Let's take the above-mentioned example of extending the forAllMethods directive to iterate through a set of methods that meet a specific set of requirements. The logic for the forAllMethods directive is defined by the class net.sf.jelly.apt.strategies.MethodDeclarationLoopStrategy. Looking at the possible methods we could override, we see that this class extends (among other things) net.sf.jelly.apt.strategies.DeclarationLoopStrategy that defines a method, getDeclarations, that gets the declarations to loop through. So, this might be a good place to put our own filtering logic.

Consider the following directive strategy class that extends net.sf.jelly.apt.strategies.MethodDeclarationLoopStrategy and loops all protected methods that are annotated with the "com.foo.Foo" annotation and that return void.

      import net.sf.jelly.apt.strategies.MethodDeclarationLoopStrategy;
      import com.sun.mirror.declaration.MethodDeclaration;
      import com.sun.mirror.declaration.Modifier;
      import com.sun.mirror.type.VoidType;

      import java.util.Collection;
      import java.util.ArrayList;

      import org.apache.commons.jelly.JellyTagException;

      public class ForAllProtectedVoidFooMethodsTag extends MethodDeclarationLoopStrategy {

      @Override
      public Collection<MethodDeclaration> getDeclarations() throws JellyTagException {
      Collection<MethodDeclaration> declarations = super.getDeclarations();

      Collection<MethodDeclaration> specialMethods = new ArrayList<MethodDeclaration>();
      for (MethodDeclaration method : declarations) {
      if ((method.getModifiers().contains(Modifier.PROTECTED))
      && (method.getReturnType() instanceof VoidType)
      && (method.getAnnotation(com.foo.Foo.class) != null)) {
      webMethods.add(method);
      }
      }

      return webMethods;
      }
      }
    

Now most of the work is done. We only have to define the class that defines the directive for our templating engine of choice. The easiest way to do that is to extend the existing class for our templating engine and override the necessary methods to reference our strategy that we created above. For example, we could extend net.sf.jelly.apt.freemarker.transforms.ForAllMethodsTransform and override the newStrategy method to return instead in instance of ForAllProtectedVoidFooMethodsTag.

A few things to note:

  • The above logic could have also been easily done inline with the template. You'll have to make your own design decisions to find that delicate balance between a heavy, difficult-to-read template and a superfluous custom directive library.
  • The filtering of the method declaration in the above example happens after the declarations are filtered by annotation. You may decide to filter before filtering by annotation name by overriding the getAllDeclarationsToConsiderForAnnotationFiltering method.
  • Just in case you don't know, the @java.lang.Override annotation causes a compile failure if any declaration that it annotates doesn't override another declaration. I use this all the time to help me catch API changes at compile-time rather than causing a confusing catastrophic runtime error.

The other aspect of APT-Jelly that you may find useful to extend is the decorated mirror API (*). In this case, the classes you will want to extend are the decorators, net.sf.jelly.apt.decorations.DeclarationDecorator and net.sf.jelly.apt.decorations.TypeMirrorDecorator. As you can see, these decorators are implementations of the visitors for declarations and types in the Mirror API. The work done when visiting a given type or declaration is simply to set a local variable to the decorated instance of the type or declaration.

When extending the decorators, it's only necessary to override the visit method of the type of declaration or type you want to decorate and then call the same method of the super with your own decorated instance (*). Your own decorated instance will need to extend the decorated type or declaration class provided by APT-Jelly in order to expose your custom decorated methods.

After having defined your decoration and extended the decorator, it's necessary to tell APT-Jelly what decorator to use when decorating. This can be done either by a system property or passed in as an "-A" APT option on the command line. (Note that the system property is difficult to set from the command line.) This name of this system property (or APT option) is "net.sf.jelly.apt.decorations.DeclarationDecorator" for a declaration decorator and "net.sf.jelly.apt.decorations.TypeMirrorDecorator" for type decorator. The values of these properties should be the fully-qualified class name of your custom decorator. You can set a system property either programmatically or you can specify these properties with a -A option (e.g. -Anet.sf.jelly.apt.decorations.DeclarationDecorator=happy.HappyDeclarationDecorator) passed to the APT-Jelly processor factory.

As an example, let's extend APT-Jelly by adding another property called "happy" to any class declaration. (Apologies for such a contrived example. All the useful decorations I could think of have already been applied.) We'll say that a class declaration is "happy" if its simple name starts with "Happy".

First, we start with extending the decorated class declaration:

      package happy;

      public class HappilyDecoratedClassDeclaration extends net.sf.jelly.apt.decorations.declaration.DecoratedClassDeclaration {

      public HappilyDecoratedClassDeclaration(com.sun.mirror.declaration.ClassDeclaration delegate) {
      super(delegate);
      }

      public boolean isHappy() {
      return getSimpleName().startsWith("Happy");
      }
      }
    

Next, we extend net.sf.jelly.apt.decorations.DeclarationDecorator to correctly visit a class declaration:

      package happy;

      public class HappyDeclarationDecorator extends net.sf.jelly.apt.decorations.DeclarationDecorator {

      public void visitClassDeclaration(com.sun.mirror.declaration.ClassDeclaration declaration) {
      super.visitClassDeclaration(new HappilyDecoratedClassDeclaration(declaration));
      }

      }
    

As long as we make sure that there is a sytem property (or APT option) named "net.sf.jelly.apt.decorations.DeclarationDecorator" that has the value "happy.HappyDeclarationDecorator" when we invoke apt (either with a -A option or programmatically), then we should be able to access our decorations from the template, e.g.:

      <#if classDeclaration.happy>
      <#-- do some happy processing -->
      </#if>
    

*NOTE: The strict rigidity of the visitor/decorator pattern causes some irritating inefficiencies when extending the model. If you decide that extending the decorated API is necessary, be aware that when extending the decorations of a type, the subtypes of that type will NOT also be decorated. Using the example above, while we extending a class declaration with the "happy" property, we are NOT extending an enum declaration. So, even though an enum declaration extends a class declaration, no enum declarations will be decorated with the "happy" property. Now just think about how irritating it would be if we wanted to decorate *all* declarations with the "happy" property. We would have to extend every subclass of DecoratedDeclaration. So extend the decorations at your own risk.



Further Information

If you've scoured the directive documentation and the decorated mirror API documentation and still can't find what you're looking for, you may want to try the apt-jelly mailing lists. And then, if all else fails, use the source, Luke. You may want to especially take a look at the unit tests as a good source for examples on how to use APT-Jelly.


page design by Phlash