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