package org.scannotation;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.FieldInfo;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import org.scannotation.archiveiterator.Filter;
import org.scannotation.archiveiterator.IteratorFactory;
import org.scannotation.archiveiterator.StreamIterator;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The class allows you to scan an arbitrary set of "archives" for .class files. These class files
* are parsed to see what annotations they use. Two indexes are created. The javax, java, sun, com.sun, and javassist
* packages will not be scanned by default.
*
* One is a map of annotations and what classes
* use those annotations. This could be used, for example, by an EJB deployer to find all the EJBs contained
* in the archive
*
* Another is a mpa of classes and what annotations those classes use.
*
* @author Bill Burke
* @version $Revision: 1 $
*/
public class AnnotationDB implements Serializable
{
protected Map> annotationIndex = new HashMap>();
protected Map> implementsIndex = new HashMap>();
protected Map> classIndex = new HashMap>();
protected transient boolean scanClassAnnotations = true;
protected transient boolean scanMethodAnnotations = true;
protected transient boolean scanParameterAnnotations = true;
protected transient boolean scanFieldAnnotations = true;
protected transient String[] ignoredPackages = {"javax", "java", "sun", "com.sun", "javassist"};
public class CrossReferenceException extends Exception
{
private Set unresolved;
public CrossReferenceException(Set unresolved)
{
this.unresolved = unresolved;
}
public Set getUnresolved()
{
return unresolved;
}
}
public String[] getIgnoredPackages()
{
return ignoredPackages;
}
/**
* Override/overwrite any ignored packages
*
* @param ignoredPackages cannot be null
*/
public void setIgnoredPackages(String[] ignoredPackages)
{
this.ignoredPackages = ignoredPackages;
}
public void addIgnoredPackages(String... ignored)
{
String[] tmp = new String[ignoredPackages.length + ignored.length];
int i = 0;
for (String ign : ignoredPackages) tmp[i++] = ign;
for (String ign : ignored) tmp[i++] = ign;
}
/**
* This method will cross reference annotations in the annotation index with any meta-annotations that they have
* and create additional entries as needed. For example:
*
* @HttpMethod("GET") public @interface GET {}
*
* The HttpMethod index will have additional classes added to it for any classes annotated with annotations that
* have the HttpMethod meta-annotation.
*
* WARNING: If the annotation class has not already been scaned, this method will load all annotation classes indexed
* as a resource so they must be in your classpath
*/
public void crossReferenceMetaAnnotations() throws CrossReferenceException
{
Set unresolved = new HashSet();
Set index = new HashSet();
index.addAll(annotationIndex.keySet());
for (String annotation : index)
{
if (ignoreScan(annotation))
{
continue;
}
if (classIndex.containsKey(annotation))
{
for (String xref : classIndex.get(annotation))
{
annotationIndex.get(xref).addAll(annotationIndex.get(annotation));
}
continue;
}
InputStream bits = Thread.currentThread().getContextClassLoader().getResourceAsStream(annotation.replace('.', '/') + ".class");
if (bits == null)
{
unresolved.add(annotation);
continue;
}
try
{
scanClass(bits);
}
catch (IOException e)
{
unresolved.add(annotation);
}
for (String xref : classIndex.get(annotation))
{
annotationIndex.get(xref).addAll(annotationIndex.get(annotation));
}
}
if (unresolved.size() > 0) throw new CrossReferenceException(unresolved);
}
/**
* Sometimes you want to see if a particular class implements an interface with certain annotations
* After you have loaded all your classpaths with the scanArchive() method, call this method to cross reference
* a class's implemented interfaces. The cross references will be added to the annotationIndex and
* classIndex indexes
*
* @param ignoredPackages var arg list of packages to ignore
* @throws CrossReferenceException an Exception thrown if referenced interfaces haven't been scanned
*/
public void crossReferenceImplementedInterfaces() throws CrossReferenceException
{
Set unresolved = new HashSet();
for (String clazz : implementsIndex.keySet())
{
Set intfs = implementsIndex.get(clazz);
for (String intf : intfs)
{
if (ignoreScan(intf)) continue;
Set xrefAnnotations = classIndex.get(intf);
if (xrefAnnotations == null)
{
unresolved.add(intf);
}
else
{
Set classAnnotations = classIndex.get(clazz);
if (classAnnotations == null)
{
classIndex.put(clazz, xrefAnnotations);
}
else classAnnotations.addAll(xrefAnnotations);
for (String annotation : xrefAnnotations)
{
Set classes = annotationIndex.get(annotation);
classes.add(clazz);
}
}
}
}
if (unresolved.size() > 0) throw new CrossReferenceException(unresolved);
}
private boolean ignoreScan(String intf)
{
for (String ignored : ignoredPackages)
{
if (intf.startsWith(ignored + "."))
{
return true;
}
else
{
//System.out.println("NOT IGNORING: " + intf);
}
}
return false;
}
/**
* returns a map keyed by the fully qualified string name of a annotation class. The Set returne is
* a list of classes that use that annotation somehow.
*/
public Map> getAnnotationIndex()
{
return annotationIndex;
}
/**
* returns a map keyed by the list of classes scanned. The value set returned is a list of annotations
* used by that class.
*/
public Map> getClassIndex()
{
return classIndex;
}
/**
* Whether or not you want AnnotationDB to scan for class level annotations
*
* @param scanClassAnnotations
*/
public void setScanClassAnnotations(boolean scanClassAnnotations)
{
this.scanClassAnnotations = scanClassAnnotations;
}
/**
* Wheter or not you want AnnotationDB to scan for method level annotations
*
* @param scanMethodAnnotations
*/
public void setScanMethodAnnotations(boolean scanMethodAnnotations)
{
this.scanMethodAnnotations = scanMethodAnnotations;
}
/**
* Whether or not you want AnnotationDB to scan for parameter level annotations
*
* @param scanParameterAnnotations
*/
public void setScanParameterAnnotations(boolean scanParameterAnnotations)
{
this.scanParameterAnnotations = scanParameterAnnotations;
}
/**
* Whether or not you want AnnotationDB to scan for parameter level annotations
*
* @param scanFieldAnnotations
*/
public void setScanFieldAnnotations(boolean scanFieldAnnotations)
{
this.scanFieldAnnotations = scanFieldAnnotations;
}
/**
* Scan a url that represents an "archive" this is a classpath directory or jar file
*
* @param urls variable list of URLs to scan as archives
* @throws IOException
*/
public void scanArchives(URL... urls) throws IOException
{
for (URL url : urls)
{
Filter filter = new Filter()
{
public boolean accepts(String filename)
{
if (filename.endsWith(".class"))
{
if (filename.startsWith("/")) filename = filename.substring(1);
if (!ignoreScan(filename.replace('/', '.'))) return true;
//System.out.println("IGNORED: " + filename);
}
return false;
}
};
StreamIterator it = IteratorFactory.create(url, filter);
InputStream stream;
while ((stream = it.next()) != null) scanClass(stream);
}
}
/**
* Parse a .class file for annotations
*
* @param bits input stream pointing to .class file bits
* @throws IOException
*/
public void scanClass(InputStream bits) throws IOException
{
DataInputStream dstream = new DataInputStream(new BufferedInputStream(bits));
ClassFile cf = null;
try
{
cf = new ClassFile(dstream);
classIndex.put(cf.getName(), new HashSet());
if (scanClassAnnotations) ;
scanClass(cf);
if (scanMethodAnnotations || scanParameterAnnotations) scanMethods(cf);
if (scanFieldAnnotations) scanFields(cf);
// create an index of interfaces the class implements
if (cf.getInterfaces() != null)
{
Set intfs = new HashSet();
for (String intf : cf.getInterfaces()) intfs.add(intf);
implementsIndex.put(cf.getName(), intfs);
}
}
finally
{
dstream.close();
bits.close();
}
}
protected void scanClass(ClassFile cf)
{
String className = cf.getName();
AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag);
AnnotationsAttribute invisible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.invisibleTag);
if (visible != null) populate(visible.getAnnotations(), className);
if (invisible != null) populate(invisible.getAnnotations(), className);
}
/**
* Scanns both the method and its parameters for annotations.
*
* @param cf
*/
protected void scanMethods(ClassFile cf)
{
List methods = cf.getMethods();
if (methods == null) return;
for (Object obj : methods)
{
MethodInfo method = (MethodInfo) obj;
if (scanMethodAnnotations)
{
AnnotationsAttribute visible = (AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.visibleTag);
AnnotationsAttribute invisible = (AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.invisibleTag);
if (visible != null) populate(visible.getAnnotations(), cf.getName());
if (invisible != null) populate(invisible.getAnnotations(), cf.getName());
}
if (scanParameterAnnotations)
{
ParameterAnnotationsAttribute paramsVisible = (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.visibleTag);
ParameterAnnotationsAttribute paramsInvisible = (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.invisibleTag);
if (paramsVisible != null && paramsVisible.getAnnotations() != null)
{
for (Annotation[] anns : paramsVisible.getAnnotations())
{
populate(anns, cf.getName());
}
}
if (paramsInvisible != null && paramsInvisible.getAnnotations() != null)
{
for (Annotation[] anns : paramsInvisible.getAnnotations())
{
populate(anns, cf.getName());
}
}
}
}
}
protected void scanFields(ClassFile cf)
{
List fields = cf.getFields();
if (fields == null) return;
for (Object obj : fields)
{
FieldInfo field = (FieldInfo) obj;
AnnotationsAttribute visible = (AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag);
AnnotationsAttribute invisible = (AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.invisibleTag);
if (visible != null) populate(visible.getAnnotations(), cf.getName());
if (invisible != null) populate(invisible.getAnnotations(), cf.getName());
}
}
protected void populate(Annotation[] annotations, String className)
{
if (annotations == null) return;
Set classAnnotations = classIndex.get(className);
for (Annotation ann : annotations)
{
Set classes = annotationIndex.get(ann.getTypeName());
if (classes == null)
{
classes = new HashSet();
annotationIndex.put(ann.getTypeName(), classes);
}
classes.add(className);
classAnnotations.add(ann.getTypeName());
}
}
/**
* Prints out annotationIndex
*
* @param writer
*/
public void outputAnnotationIndex(PrintWriter writer)
{
for (String ann : annotationIndex.keySet())
{
writer.print(ann);
writer.print(": ");
Set classes = annotationIndex.get(ann);
Iterator it = classes.iterator();
while (it.hasNext())
{
writer.print(it.next());
if (it.hasNext()) writer.print(", ");
}
writer.println();
}
}
}