|
The apt Tool for Source-Level Annotation ProcessingOne use for annotation is the automatic generation of "side files" that contain additional information about programs. The "Enterprise Edition" of Java is notorious for making programmers fuss with lots of boilerplate code, and an effort is underway to develop a standardized set of annotations to generate most of it automatically. In this section, we demonstrate this technique with a simpler example. We write a program that automatically produces bean info classes. You tag bean properties with an annotation and then run a tool that parses the source file, analyzes the annotations, and writes out the source file of the bean info class. Rather than writing our own parser, we use an annotation processing tool called apt that is part of the SDK. We will first describe how to generate the bean info class, and then we will show you how to use apt. Recall from Chapter 8 that a bean info class describes a bean more precisely than the automatic introspection process can. The bean info class lists all of the properties of the bean. Properties can have optional property editors. The ChartBeanBeanInfo class in Chapter 8 is a typical example. To eliminate the drudgery of writing bean info classes, we supply an @Property annotation. You can tag either the property getter or setter, like this: @Property String getTitle() { return title; } or @Property(editor="TitlePositionEditor") public void setTitlePosition(int p) { titlePosition = p; } Example 13-4 contains the definition of the @Property annotation. Note that the annotation has a retention policy of SOURCE. We analyze the annotation at the source level only. It is not included in class files and not available during reflection. Example 13-4. Property.java1. import java.lang.annotation.*; 2. 3. @Documented 4. @Target(ElementType.METHOD) 5. @Retention(RetentionPolicy.SOURCE) 6. public @interface Property 7. { 8. String editor() default ""; 9. } NOTE
To automatically generate the bean info class of a class with name BeanClass, we carry out the following tasks:
For example, the annotation @Property(editor="TitlePositionEditor") public void setTitlePosition(int p) { titlePosition = p; } in the ChartBean class is translated into public class ChartBeanBeanInfo extends java.beans.SimpleBeanInfo { public java.beans.PropertyDescriptor[] getProperties() { java.beans.PropertyDescriptor titlePositionDescriptor = new java.beans.PropertyDescriptor("titlePosition", ChartBean.class); titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class) . . . return new java.beans.PropertyDescriptor[] { titlePositionDescriptor, . . . } } } (The boilerplate code is printed in gray color.) All this is easy enough to do, provided we can locate all methods that have been tagged with the @Property attribute. Rather than writing our own parser, we use apt, the annotation processing tool, that is a part of the JDK. NOTE
You can find documentation for apt at http://java.sun.com/j2se/5.0/docs/guide/apt/. The tool processes annotations in source files, using annotation factories to locate annotation processors. There is a mechanism for discovering factories automatically, but for simplicity, we explicitly specify the factory on the command line. To invoke the bean info processor, run
The apt program locates the annotations of the source files that are specified on the command line. It then selects the annotation processors that should be applied. Each annotation processor is executed in turn. If an annotation processor creates a new source file, then the process is repeated. If a processing round yields no further source files, then apt invokes the javac compiler on all source files. The parser inside apt analyzes each source file and enumerates the classes, methods, fields, and variables. The API used by apt is called the "mirror API" because it is similar to the reflection API, except that it operates on the source level. Currently, the mirror API is a part of the com.sun hierarchy, and the documentation cautions that the API may change in the future. We do not discuss the API in detail, but we do examine the program in Example 13-5 to give you a flavor of its capabilities. You can find a complete API reference in the apt documentation. The first two methods in the BeanInfoAnnotationFactory class support the factory discovery process (which we are not using). The factory is willing to deal with annotations named Property, and it supports no command-line options. The third method yields the actual processor. The BeanInfoAnnotationProcessor has a single public method, process, that is called for each file. In the process method, we iterate through all classes, ignore classes that are not public, then iterate through all methods and ignore methods that are not annotated with @Property. Here is the outline of the code:
The code for writing the source file is straightforward, just a sequence of out.print statements. Note that we create the output file with a call to env.getFiler().createSourceFile(beanClassName + "BeanInfo"); The env object encapsulates the apt processing environment. The Filer class is responsible for creating new files. The filer keeps track of the newly created source files so that they can be processed in the next processing round. When an annotation processor detects an error, it uses the Messager to communicate with the user. For example, we issue an error message if a method has been annotated with @Property but its name doesn't start with get, set, or is: if (!found) env.getMessager().printError(m.getPosition(), "@Property must be applied to getXxx, setXxx, or isXxx method"); When compiling the BeanInfoAnnotationFactory class, you need to add tools.jar to the class path. That file is contained in the lib subdirectory of your JDK installation:
NOTE
In the companion code for this book, we supply you with an annotated file, ChartBean.java. Run apt -factory BeanInfoAnnotationFactory ChartBean.java and have a look at the automatically generated file ChartBeanBeanInfo.java. This example demonstrates how tools can harvest source file annotations to produce other files. The generated files don't have to be source files. Annotation processors may choose to generate XML descriptors, property files, shell scripts, HTML documentation, and so on. NOTE
Example 13-5. BeanInfoAnnotationFactory.java[View full width] 1. import com.sun.mirror.apt.*; 2. import com.sun.mirror.declaration.*; 3. import com.sun.mirror.type.*; 4. import com.sun.mirror.util.*; 5. 6. import java.beans.*; 7. import java.io.*; 8. import java.util.*; 9. 10. /** 11. This class is used to run an annotation processor that creates a BeanInfo file. 12. */ 13. public class BeanInfoAnnotationFactory implements AnnotationProcessorFactory 14. { 15. public Collection<String> supportedAnnotationTypes() 16. { 17. return Arrays.asList("Property"); 18. } 19. 20. public Collection<String> supportedOptions() 21. { 22. return Arrays.asList(new String[0]); 23. } 24. 25. public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds, 26. AnnotationProcessorEnvironment env) 27. { 28. return new BeanInfoAnnotationProcessor(env); 29. } 30. 31. /** 32. This class is the processor that analyzes @Property annotations. 33. */ 34. private static class BeanInfoAnnotationProcessor implements AnnotationProcessor 35. { 36. BeanInfoAnnotationProcessor(AnnotationProcessorEnvironment env) 37. { 38. this.env = env; 39. } 40. 41. public void process() 42. { 43. for (TypeDeclaration t : env.getSpecifiedTypeDeclarations()) 44. { 45. if (t.getModifiers().contains(Modifier.PUBLIC)) 46. { 47. System.out.println(t); 48. Map<String, Property> props = new TreeMap<String, Property>(); 49. for (MethodDeclaration m : t.getMethods()) 50. { 51. Property p = m.getAnnotation(Property.class); 52. if (p != null) 53. { 54. String mname = m.getSimpleName(); 55. String[] prefixes = { "get", "set", "is" }; 56. boolean found = false; 57. for (int i = 0; !found && i < prefixes.length; i++) 58. if (mname.startsWith(prefixes[i])) 59. { 60. found = true; 61. int start = prefixes[i].length(); 62. String name = Introspector.decapitalize(mname.substring (start)); 63. props.put(name, p); 64. } 65. 66. if (!found) 67. env.getMessager().printError(m.getPosition(), 68. "@Property must be applied to getXxx, setXxx, or isXxx method"); 69. } 70. } 71. 72. try 73. { 74. if (props.size() > 0) 75. writeBeanInfoFile(t.getQualifiedName(), props); 76. } 77. catch (IOException e) 78. { 79. e.printStackTrace(); 80. } 81. } 82. } 83. } 84. 85. /** 86. Writes the source file for the BeanInfo class. 87. @param beanClassName the name of the bean class 88. @param props a map of property names and their annotations 89. */ 90. private void writeBeanInfoFile(String beanClassName, Map<String, Property> props) 91. throws IOException 92. { 93. PrintWriter out = env.getFiler().createSourceFile(beanClassName + "BeanInfo"); 94. int i = beanClassName.lastIndexOf("."); 95. if (i > 0) 96. { 97. out.print("package "); 98. out.println(beanClassName.substring(0, i)); 99. } 100. out.print("public class "); 101. out.print(beanClassName.substring(i + 1)); 102. out.println("BeanInfo extends java.beans.SimpleBeanInfo"); 103. out.println("{"); 104. out.println(" public java.beans.PropertyDescriptor[] getPropertyDescriptors()"); 105. out.println(" {"); 106. out.println(" try"); 107. out.println(" {"); 108. for (Map.Entry<String, Property> e : props.entrySet()) 109. { 110. out.print(" java.beans.PropertyDescriptor "); 111. out.print(e.getKey()); 112. out.println("Descriptor"); 113. out.print(" = new java.beans.PropertyDescriptor(\""); 114. out.print(e.getKey()); 115. out.print("\", "); 116. out.print(beanClassName); 117. out.println(".class);"); 118. String ed = e.getValue().editor().toString(); 119. if (!ed.equals("")) 120. { 121. out.print(" "); 122. out.print(e.getKey()); 123. out.print("Descriptor.setPropertyEditorClass("); 124. out.print(ed); 125. out.println(".class);"); 126. } 127. } 128. out.println(" return new java.beans.PropertyDescriptor[]"); 129. out.print(" {"); 130. boolean first = true; 131. for (String p : props.keySet()) 132. { 133. if (first) first = false; else out.print(","); 134. out.println(); 135. out.print(" "); 136. out.print(p); 137. out.print("Descriptor"); 138. } 139. out.println(); 140. out.println(" };"); 141. out.println(" }"); 142. out.println(" catch (java.beans.IntrospectionException e)"); 143. out.println(" {"); 144. out.println(" e.printStackTrace();"); 145. out.println(" return null;"); 146. out.println(" }"); 147. out.println(" }"); 148. out.println("}"); 149. out.close(); 150. } 151. 152. 153. private AnnotationProcessorEnvironment env; 154. } 155. } |
|