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