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