1.0.3
[scannotation] / src / main / java / org / scannotation / AnnotationDB.java
1 package org.scannotation;
2
3 import javassist.bytecode.AnnotationsAttribute;
4 import javassist.bytecode.ClassFile;
5 import javassist.bytecode.FieldInfo;
6 import javassist.bytecode.MethodInfo;
7 import javassist.bytecode.ParameterAnnotationsAttribute;
8 import javassist.bytecode.annotation.Annotation;
9 import org.scannotation.archiveiterator.Filter;
10 import org.scannotation.archiveiterator.IteratorFactory;
11 import org.scannotation.archiveiterator.StreamIterator;
12
13 import java.io.BufferedInputStream;
14 import java.io.DataInputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.PrintWriter;
18 import java.io.Serializable;
19 import java.net.URL;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26
27 /**
28  * The class allows you to scan an arbitrary set of "archives" for .class files.  These class files
29  * are parsed to see what annotations they use.  Two indexes are created.  The javax, java, sun, com.sun, and javassist
30  * packages will not be scanned by default.
31  * <p/>
32  * One is a map of annotations and what classes
33  * use those annotations.   This could be used, for example, by an EJB deployer to find all the EJBs contained
34  * in the archive
35  * <p/>
36  * Another is a mpa of classes and what annotations those classes use.
37  *
38  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
39  * @version $Revision: 1 $
40  */
41 public class AnnotationDB implements Serializable
42 {
43    protected Map<String, Set<String>> annotationIndex = new HashMap<String, Set<String>>();
44    protected Map<String, Set<String>> implementsIndex = new HashMap<String, Set<String>>();
45    protected Map<String, Set<String>> classIndex = new HashMap<String, Set<String>>();
46
47    protected transient boolean scanClassAnnotations = true;
48    protected transient boolean scanMethodAnnotations = true;
49    protected transient boolean scanParameterAnnotations = true;
50    protected transient boolean scanFieldAnnotations = true;
51    protected transient String[] ignoredPackages = {"javax", "java", "sun", "com.sun", "javassist"};
52    protected transient String[] scanPackages = null;
53
54    public class CrossReferenceException extends Exception
55    {
56       private Set<String> unresolved;
57
58       public CrossReferenceException(Set<String> unresolved)
59       {
60          this.unresolved = unresolved;
61       }
62
63       public Set<String> getUnresolved()
64       {
65          return unresolved;
66       }
67    }
68
69    public String[] getScanPackages()
70    {
71       return scanPackages;
72    }
73
74    /**
75     * Set explicit packages to scan.
76     * Set to null to enable ignore list.
77     *
78     * @param scanPackages packages to scan or null
79     */
80    public void setScanPackages(String[] scanPackages)
81    {
82       this.scanPackages = scanPackages;
83    }
84
85    public String[] getIgnoredPackages()
86    {
87       return ignoredPackages;
88    }
89
90    /**
91     * Override/overwrite any ignored packages
92     *
93     * @param ignoredPackages cannot be null
94     */
95    public void setIgnoredPackages(String[] ignoredPackages)
96    {
97       this.ignoredPackages = ignoredPackages;
98    }
99
100    public void addIgnoredPackages(String... ignored)
101    {
102       String[] tmp = new String[ignoredPackages.length + ignored.length];
103       int i = 0;
104       for (String ign : ignoredPackages) tmp[i++] = ign;
105       for (String ign : ignored) tmp[i++] = ign;
106       this.ignoredPackages = tmp;
107    }
108
109    /**
110     * This method will cross reference annotations in the annotation index with any meta-annotations that they have
111     * and create additional entries as needed.  For example:
112     *
113     * @HttpMethod("GET") public @interface GET {}
114     * <p/>
115     * The HttpMethod index will have additional classes added to it for any classes annotated with annotations that
116     * have the HttpMethod meta-annotation.
117     * <p/>
118     * WARNING: If the annotation class has not already been scaned, this method will load all annotation classes indexed
119     * as a resource so they must be in your classpath
120     */
121    public void crossReferenceMetaAnnotations() throws CrossReferenceException
122    {
123       Set<String> unresolved = new HashSet<String>();
124
125       Set<String> index = new HashSet<String>();
126       index.addAll(annotationIndex.keySet());
127
128       for (String annotation : index)
129       {
130          if (ignoreScan(annotation))
131          {
132             continue;
133          }
134          if (classIndex.containsKey(annotation))
135          {
136             for (String xref : classIndex.get(annotation))
137             {
138                annotationIndex.get(xref).addAll(annotationIndex.get(annotation));
139             }
140             continue;
141          }
142          InputStream bits = Thread.currentThread().getContextClassLoader().getResourceAsStream(annotation.replace('.', '/') + ".class");
143          if (bits == null)
144          {
145             unresolved.add(annotation);
146             continue;
147          }
148          try
149          {
150             scanClass(bits);
151          }
152          catch (IOException e)
153          {
154             unresolved.add(annotation);
155          }
156          for (String xref : classIndex.get(annotation))
157          {
158             annotationIndex.get(xref).addAll(annotationIndex.get(annotation));
159          }
160
161       }
162       if (unresolved.size() > 0) throw new CrossReferenceException(unresolved);
163    }
164
165    /**
166     * Sometimes you want to see if a particular class implements an interface with certain annotations
167     * After you have loaded all your classpaths with the scanArchive() method, call this method to cross reference
168     * a class's implemented interfaces.  The cross references will be added to the annotationIndex and
169     * classIndex indexes
170     *
171     * @param ignoredPackages var arg list of packages to ignore
172     * @throws CrossReferenceException an Exception thrown if referenced interfaces haven't been scanned
173     */
174    public void crossReferenceImplementedInterfaces() throws CrossReferenceException
175    {
176       Set<String> unresolved = new HashSet<String>();
177       for (String clazz : implementsIndex.keySet())
178       {
179          Set<String> intfs = implementsIndex.get(clazz);
180          for (String intf : intfs)
181          {
182             if (ignoreScan(intf)) continue;
183
184             Set<String> xrefAnnotations = classIndex.get(intf);
185             if (xrefAnnotations == null)
186             {
187                unresolved.add(intf);
188             }
189             else
190             {
191                Set<String> classAnnotations = classIndex.get(clazz);
192                if (classAnnotations == null)
193                {
194                   classIndex.put(clazz, xrefAnnotations);
195                }
196                else classAnnotations.addAll(xrefAnnotations);
197                for (String annotation : xrefAnnotations)
198                {
199                   Set<String> classes = annotationIndex.get(annotation);
200                   classes.add(clazz);
201                }
202             }
203          }
204       }
205       if (unresolved.size() > 0) throw new CrossReferenceException(unresolved);
206
207    }
208
209    private boolean ignoreScan(String intf)
210    {
211           if (scanPackages != null)
212           {
213               for (String scan : scanPackages)
214               {
215                  // do not ignore if on packages to scan list
216                  if (intf.startsWith(scan + "."))
217                  {
218                     return false;
219                  }
220               }
221           return true; // didn't match whitelist, ignore
222           }
223       for (String ignored : ignoredPackages)
224       {
225          if (intf.startsWith(ignored + "."))
226          {
227             return true;
228          }
229          else
230          {
231             //System.out.println("NOT IGNORING: " + intf);
232          }
233       }
234       return false;
235    }
236
237    /**
238     * returns a map keyed by the fully qualified string name of a annotation class.  The Set returne is
239     * a list of classes that use that annotation somehow.
240     */
241    public Map<String, Set<String>> getAnnotationIndex()
242    {
243       return annotationIndex;
244    }
245
246    /**
247     * returns a map keyed by the list of classes scanned.  The value set returned is a list of annotations
248     * used by that class.
249     */
250    public Map<String, Set<String>> getClassIndex()
251    {
252       return classIndex;
253    }
254
255
256    /**
257     * Whether or not you want AnnotationDB to scan for class level annotations
258     *
259     * @param scanClassAnnotations
260     */
261    public void setScanClassAnnotations(boolean scanClassAnnotations)
262    {
263       this.scanClassAnnotations = scanClassAnnotations;
264    }
265
266    /**
267     * Wheter or not you want AnnotationDB to scan for method level annotations
268     *
269     * @param scanMethodAnnotations
270     */
271    public void setScanMethodAnnotations(boolean scanMethodAnnotations)
272    {
273       this.scanMethodAnnotations = scanMethodAnnotations;
274    }
275
276    /**
277     * Whether or not you want AnnotationDB to scan for parameter level annotations
278     *
279     * @param scanParameterAnnotations
280     */
281    public void setScanParameterAnnotations(boolean scanParameterAnnotations)
282    {
283       this.scanParameterAnnotations = scanParameterAnnotations;
284    }
285
286    /**
287     * Whether or not you want AnnotationDB to scan for parameter level annotations
288     *
289     * @param scanFieldAnnotations
290     */
291    public void setScanFieldAnnotations(boolean scanFieldAnnotations)
292    {
293       this.scanFieldAnnotations = scanFieldAnnotations;
294    }
295
296
297    /**
298     * Scan a url that represents an "archive"  this is a classpath directory or jar file
299     *
300     * @param urls variable list of URLs to scan as archives
301     * @throws IOException
302     */
303    public void scanArchives(URL... urls) throws IOException
304    {
305       for (URL url : urls)
306       {
307          Filter filter = new Filter()
308          {
309             public boolean accepts(String filename)
310             {
311                if (filename.endsWith(".class"))
312                {
313                   if (filename.startsWith("/") || filename.startsWith("\\"))
314                       filename = filename.substring(1);
315                   if (!ignoreScan(filename.replace('/', '.')))
316                       return true;
317                   //System.out.println("IGNORED: " + filename);
318                }
319                return false;
320             }
321          };
322
323          StreamIterator it = IteratorFactory.create(url, filter);
324
325          InputStream stream;
326          while ((stream = it.next()) != null) scanClass(stream);
327       }
328
329    }
330
331    /**
332     * Parse a .class file for annotations
333     *
334     * @param bits input stream pointing to .class file bits
335     * @throws IOException
336     */
337    public void scanClass(InputStream bits) throws IOException
338    {
339       DataInputStream dstream = new DataInputStream(new BufferedInputStream(bits));
340       ClassFile cf = null;
341       try
342       {
343          cf = new ClassFile(dstream);
344          classIndex.put(cf.getName(), new HashSet<String>());
345          if (scanClassAnnotations)
346             scanClass(cf);
347          if (scanMethodAnnotations || scanParameterAnnotations) scanMethods(cf);
348          if (scanFieldAnnotations) scanFields(cf);
349
350          // create an index of interfaces the class implements
351          if (cf.getInterfaces() != null)
352          {
353             Set<String> intfs = new HashSet<String>();
354             for (String intf : cf.getInterfaces()) intfs.add(intf);
355             implementsIndex.put(cf.getName(), intfs);
356          }
357
358       }
359       finally
360       {
361          dstream.close();
362          bits.close();
363       }
364    }
365
366    protected void scanClass(ClassFile cf)
367    {
368       String className = cf.getName();
369       AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag);
370       AnnotationsAttribute invisible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.invisibleTag);
371
372       if (visible != null) populate(visible.getAnnotations(), className);
373       if (invisible != null) populate(invisible.getAnnotations(), className);
374    }
375
376    /**
377     * Scanns both the method and its parameters for annotations.
378     *
379     * @param cf
380     */
381    protected void scanMethods(ClassFile cf)
382    {
383       List methods = cf.getMethods();
384       if (methods == null) return;
385       for (Object obj : methods)
386       {
387          MethodInfo method = (MethodInfo) obj;
388          if (scanMethodAnnotations)
389          {
390             AnnotationsAttribute visible = (AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.visibleTag);
391             AnnotationsAttribute invisible = (AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.invisibleTag);
392
393             if (visible != null) populate(visible.getAnnotations(), cf.getName());
394             if (invisible != null) populate(invisible.getAnnotations(), cf.getName());
395          }
396          if (scanParameterAnnotations)
397          {
398             ParameterAnnotationsAttribute paramsVisible = (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.visibleTag);
399             ParameterAnnotationsAttribute paramsInvisible = (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.invisibleTag);
400
401             if (paramsVisible != null && paramsVisible.getAnnotations() != null)
402             {
403                for (Annotation[] anns : paramsVisible.getAnnotations())
404                {
405                   populate(anns, cf.getName());
406                }
407             }
408             if (paramsInvisible != null && paramsInvisible.getAnnotations() != null)
409             {
410                for (Annotation[] anns : paramsInvisible.getAnnotations())
411                {
412                   populate(anns, cf.getName());
413                }
414             }
415          }
416       }
417    }
418
419    protected void scanFields(ClassFile cf)
420    {
421       List fields = cf.getFields();
422       if (fields == null) return;
423       for (Object obj : fields)
424       {
425          FieldInfo field = (FieldInfo) obj;
426          AnnotationsAttribute visible = (AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag);
427          AnnotationsAttribute invisible = (AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.invisibleTag);
428
429          if (visible != null) populate(visible.getAnnotations(), cf.getName());
430          if (invisible != null) populate(invisible.getAnnotations(), cf.getName());
431       }
432
433
434    }
435
436    protected void populate(Annotation[] annotations, String className)
437    {
438       if (annotations == null) return;
439       Set<String> classAnnotations = classIndex.get(className);
440       for (Annotation ann : annotations)
441       {
442          Set<String> classes = annotationIndex.get(ann.getTypeName());
443          if (classes == null)
444          {
445             classes = new HashSet<String>();
446             annotationIndex.put(ann.getTypeName(), classes);
447          }
448          classes.add(className);
449          classAnnotations.add(ann.getTypeName());
450       }
451    }
452
453    /**
454     * Prints out annotationIndex
455     *
456     * @param writer
457     */
458    public void outputAnnotationIndex(PrintWriter writer)
459    {
460       for (String ann : annotationIndex.keySet())
461       {
462          writer.print(ann);
463          writer.print(": ");
464          Set<String> classes = annotationIndex.get(ann);
465          Iterator<String> it = classes.iterator();
466          while (it.hasNext())
467          {
468             writer.print(it.next());
469             if (it.hasNext()) writer.print(", ");
470          }
471          writer.println();
472       }
473    }
474
475 }