X
Tech

Writing and processing custom annotations - Part 3

In part three of this custom annotation series, we now turn our attention to developing the "engine" that processes our @Option annotation.
Written by Lee Chuk-Munn, Contributor
In part 1 of this series, we introduced the @Option annotation and described some use cases; in part 2, we learnt how to use the new annotation features in JavaSE 5 (a.k.a. Tiger) to write @Option the annotation. In part 3 this week, we now turn our attention to developing the "engine" that processes our @Option annotation.

Processing the @Option Annotation

Let's learn how to write a custom annotation processor that will generate a Java class to process our @Option annotation. Here are the steps:

  1. Write a class that extends AbstractProcessor. The AbstractProcessor class is in javax.annotation.processing package.

import javax.annotation.processing.*;
public class OptionProcessor extends AbstractProcessor {
   ...
}

  1. Next we have to specify the annotations that our annotation processor class, OptionProcessor, is interested in processing. There are two ways of doing this: either override the getSupportedAnnotationTypes() method from AbstractProcessor or use the @SupportedAnnotationTypes. We will use the latter method to specify the fully qualified path name of our @Option annotation like so (assume that @Option is in options package):

@SupportedAnnotationTypes({"options.Option"})
public class OptionProcessor extends AbstractProcessor {
   ...
}

The SupportedAnnotationTypes annotation is a multi-valued option, so we need a '{}' to specify all the possible annotations that we are going to support. In this particular example, options.Option is the only one.

  1. Now override the process() method. This is the workhorse of the processor; the method has the following signature:

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env)

Here is an explanation of the parameters and return values of process:

  • elements - contains the set of annotations that we are interested in processing. The annotations in the set should correspond with the list of annotations that we specify in the @SupportedAnnotationTypes annotation
  • env - this is the processing environment
  • boolean - a boolean value is to indicate whether we have claimed ownership of the set of annotations passed by the processing environment in elements. "Claiming ownership" means that the set of annotations in elements are ours, and we have processed it. Return true if you claim ownership of them; false otherwise.

Here is what we are going to do in process(); we will use a for loop to go through all the annotations in elements. For every annotation, we will use getElementsAnnotateWith() (in RoundEnvironment) to give us all the annotated statement.

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

   //processingEnv is a predefined member in AbstractProcessor class
   //Messager allows the processor to output messages to the environment
   Messager messager = processingEnv.getMessager();

   //Create a hash table to hold the option switch to option bean mapping
   HashMap<String, String> values = new HashMap<String, String>();

   //Loop through the annotations that we are going to process
   //In this case there should only be one: Option
   for (TypeElement te: elements) {

      //Get the members that are annotated with Option
      for (Element e: env.getElementsAnnotatedWith(te))
         //Process the members. processAnnotation is our own method
         processAnnotation(e, values, messager);
   }


   //If there are any annotations, we will proceed to generate the annotation
   //processor in generateOptionProcessor method
   if (values.size() > 0)
      try {
         //Generate the option process class
         generateOptionProcessor(processingEnv.getFiler(), values);
      } catch (Exception e) {
         messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
      }

   return (true);
}

  1. Let's look at how processAnnotation() method works. For every statement that is annotated, represented by element parameter that is passed in from process(), we retrieve the annotation for that statement. Next we store the option switch against the member name in the value hashmap.

private void processAnnotation(Element element
      , HashMap<String, String> values, Messager msg) {

   //Get the Option annotation on the member
   Option opt = element.getAnnotation(Option.class);

   //Get the class name of the option bean
   className = element.getEnclosingElement().toString();

   //Check if the type in the member is a String. If not we igonre it
   //We are currently only supporting String type
   if (!element.asType().toString().equals(String.class.getName())) {
      msg.printMessage(Diagnostic.Kind.WARNING
            , element.asType() + " not supported. " + opt.name() + " not processed");
      return;
   }

   //Save the option switch and the member's name in a hash set
   //Eg. -filename (option switch) mapped to fileName (member)
   values.put(opt.name(), element.getSimpleName().toString());
}

  1. Once we have collected all the statements that are annotated with Option, we can now proceed to generate the annotation processor class. The generateOptionProcessor() method does just this. It accepts as its parameter an object call Filer. Filer allows you to write out the annotation processor class to a file; this class is then also compiled during the processing.

private void generateOptionProcessor(Filer filer
      , HashMap<String, String> values) throws Exception {
       
   String generatedClassName = className + "Processor";
       
   Writer writer = filer.createSourceFile(generatedClassName);
       
   writer.write("/* Generated on " + new Date() + " */\n");
       
   writer.write("public class " + generatedClassName + " {\n");
       
   writer.write("\tpublic static " + className + " process(String[] args) {\n");
       
   writer.write("\t\t" + className + " options = new " + className + "();\n");
   writer.write("\t\tint idx = 0;\n");
       
   writer.write("\t\twhile (idx < args.length) {\n");
       
   for (String key: values.keySet()) {
      writer.write("\t\t\tif (args[idx].equals(\"" + key + "\")) {\n");
      writer.write("\t\t\t\toptions." + values.get(key) + " = args[++idx];\n");
      writer.write("\t\t\t\tidx++;\n");
      writer.write("\t\t\t\tcontinue;\n");
      writer.write("\t\t\t}\n");
   }
       
   writer.write("\t\t\tSystem.err.println(\"Unknown option: \" + args[idx++]);\n");
       
   writer.write("\t\t}\n");
       
   writer.write("\t\treturn (options);\n");
   writer.write("\t}\n");
       
   writer.write("}");
       
   writer.flush();
   writer.close();
}       

The entire source file for OptionProcessor can be found here. The generated options processor class looks something like this:

/* Generated on Tue Apr 11 17:03:41 SGT 2006 */
public class FileTransferOptionsProcessor {
   public static FileTransferOptions process(String[] args) {
      FileTransferOptions options = new FileTransferOptions();
      int idx = 0;
      while (idx < args.length) {
         if (args[idx].equals("-filename")) {
            options.fileName = args[++idx];
            idx++;
            continue;
         }
         if (args[idx].equals("-server")) {
            options.server = args[++idx];
            idx++;
            continue;
         }
         System.err.println("Unknown option: " + args[idx++]);
      }
      return (options);
   }
}

You can find the full source for OptionProcessor here. In our next and final installment of this series, we will learn how to use our annotation processor.

Lee Chuk-Munn has been programming in the Java language since 1996, when he first joined Sun Microsystems in Hong Kong. He currently works as a senior developer consultant and technology evangelist for Technology Outreach at Sun in Singapore. Chuk's focus is in Java APIs, Java EE, Java SE, and Java ME. Chuk graduated in 1987 from the Royal Melbourne Institute of Technology in Melbourne, Australia, where his favorite subject was compiler theory.

Editorial standards