View Javadoc

1   // ========================================================================
2   // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.annotations;
15  
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.lang.reflect.Array;
19  import java.lang.reflect.Field;
20  import java.lang.reflect.Method;
21  import java.net.URI;
22  import java.net.URL;
23  import java.net.URLClassLoader;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.jar.JarEntry;
31  import java.util.regex.Pattern;
32  
33  import org.eclipse.jetty.util.Loader;
34  import org.eclipse.jetty.util.log.Log;
35  import org.eclipse.jetty.util.resource.Resource;
36  import org.eclipse.jetty.webapp.JarScanner;
37  import org.objectweb.asm.AnnotationVisitor;
38  import org.objectweb.asm.ClassReader;
39  import org.objectweb.asm.FieldVisitor;
40  import org.objectweb.asm.MethodVisitor;
41  import org.objectweb.asm.Type;
42  import org.objectweb.asm.commons.EmptyVisitor;
43  
44  
45  /**
46   * AnnotationFinder
47   *
48   *
49   * Scans class sources using asm to find annotations.
50   *
51   *
52   */
53  public class AnnotationFinder
54  {
55      private Map<String,ParsedClass> parsedClasses = new HashMap<String, ParsedClass>();
56     
57      
58      public static String normalize (String name)
59      {
60          if (name==null)
61              return null;
62          
63          if (name.startsWith("L") && name.endsWith(";"))
64              name = name.substring(1, name.length()-1);
65          
66          if (name.endsWith(".class"))
67              name = name.substring(0, name.length()-".class".length());
68          
69         name = name.replace('$', '.');
70          
71          return name.replace('/', '.');
72      }
73      
74      public static Class convertType (org.objectweb.asm.Type t)
75      throws Exception
76      {
77          if (t == null)
78              return (Class)null;
79          
80          switch (t.getSort())
81          {
82              case Type.BOOLEAN:
83              {
84                  return Boolean.TYPE;
85              }
86              case Type.ARRAY:
87              {
88                  Class clazz = convertType(t.getElementType());
89                  return Array.newInstance(clazz, 0).getClass();
90              }
91              case Type.BYTE:
92              {
93                  return Byte.TYPE;
94              }
95              case Type.CHAR:
96              {
97                  return Character.TYPE;
98              }
99              case Type.DOUBLE:
100             {
101                 return Double.TYPE;
102             }
103             case Type.FLOAT:
104             {
105                 return Float.TYPE;
106             }
107             case Type.INT:
108             {
109                 return Integer.TYPE;
110             }
111             case Type.LONG:
112             {
113                 return Long.TYPE;
114             }
115             case Type.OBJECT:
116             {
117                 return (Loader.loadClass(null, t.getClassName()));
118             }
119             case Type.SHORT:
120             {
121                 return Short.TYPE;
122             }
123             case Type.VOID:
124             {
125                 return null;
126             }
127             default:
128                 return null;
129         }
130         
131     }
132     
133     public static Class[] convertTypes (Type[] types)
134     throws Exception
135     {
136         if (types==null)
137             return new Class[0];
138         
139         Class[] classArray = new Class[types.length];
140         
141         for (int i=0; i<types.length; i++)
142         {
143             classArray[i] = convertType(types[i]);
144         }
145         return classArray;
146     }
147   
148     
149     /**
150      * AnnotatedStructure
151      *
152      * Annotations on an object such as a class, field or method.
153      */
154     public static class AnnotatedStructure  extends EmptyVisitor
155     {
156         Map<String, Map<String, Object>> annotations = new HashMap<String, Map<String,Object>>();
157         
158         
159         public AnnotationVisitor addAnnotation (final String name)
160         {
161             final HashMap<String,Object> annotationValues = new HashMap<String,Object>();
162             this.annotations.put(normalize(name), annotationValues);
163             return new AnnotationVisitor()
164             {
165                 public void visit(String name, Object value)
166                 {
167                     annotationValues.put(name, value);
168                 }
169 
170                 public AnnotationVisitor visitAnnotation(String name, String desc)
171                 {
172                     return null; //ignore nested annotations
173                 }
174 
175                 public AnnotationVisitor visitArray(String arg0)
176                 {
177                     return null;//ignore array valued annotations
178                 }
179 
180                 public void visitEnd()
181                 {     
182                 }
183 
184                 public void visitEnum(String name, String desc, String value)
185                 {
186                 }
187             };
188         } 
189         
190         public Map<String, Map<String, Object>> getAnnotations ()
191         {
192             return annotations;
193         }
194         
195         
196         public String toString()
197         {
198             StringBuffer strbuff = new StringBuffer();
199             
200             for (Map.Entry<String, Map<String,Object>> e: annotations.entrySet())
201             {
202                 strbuff.append(e.getKey()+"\n");
203                 for (Map.Entry<String,Object> v: e.getValue().entrySet())
204                 {
205                     strbuff.append("\t"+v.getKey()+"="+v.getValue()+", ");
206                 }
207             }
208             return strbuff.toString();
209         }
210     }
211     
212     
213     /**
214      * ParsedClass
215      *
216      * A class that contains annotations.
217      */
218     public static class ParsedClass extends AnnotatedStructure
219     {
220         String className;  
221         String superClassName;
222         Class clazz;
223         List<ParsedMethod> methods = new ArrayList<ParsedMethod>();
224         List<ParsedField> fields = new ArrayList<ParsedField>();
225         
226       
227         public ParsedClass (String className, String superClassName)
228         {
229             this.className = normalize(className);
230             this.superClassName = normalize(superClassName);
231         }
232         
233         public String getClassName()
234         {
235             return this.className;
236         }
237         
238         public String getSuperClassName ()
239         {
240             return this.superClassName;
241         }
242         
243         public Class toClass ()
244         throws ClassNotFoundException
245         {
246             if (clazz==null)
247                 clazz = Loader.loadClass(null, className);
248             return clazz;
249         }
250         
251         public List<ParsedMethod> getMethods ()
252         {
253             return methods;
254         }
255         
256         public ParsedMethod getMethod(String name, String paramString)
257         {
258             Iterator<ParsedMethod> itor = methods.iterator();
259             ParsedMethod method = null;
260             while (itor.hasNext() && method==null)
261             {
262                 ParsedMethod m = itor.next();
263                 if (m.matches(name, paramString))
264                     method = m;
265             }
266             
267             return method;
268         }
269         
270         public void addMethod (ParsedMethod m)
271         {
272             if (getMethod(m.methodName, m.paramString)!= null)
273                 return;
274             methods.add(m);
275         }
276         
277         public List<ParsedField> getFields()
278         {
279             return fields;
280         }
281         
282         public ParsedField getField(String name)
283         {
284             Iterator<ParsedField> itor = fields.iterator();
285             ParsedField field = null;
286             while (itor.hasNext() && field==null)
287             {
288                 ParsedField f = itor.next();
289                 if (f.matches(name))
290                     field=f;
291             }
292             return field;
293         }
294         
295         public void addField (ParsedField f)
296         {
297             if (getField(f.fieldName) != null)
298                 return;
299             fields.add(f);
300         }
301         
302         public String toString ()
303         {
304             StringBuffer strbuff = new StringBuffer();
305             strbuff.append(this.className+"\n");
306             strbuff.append("Class annotations\n"+super.toString());
307             strbuff.append("\n");
308             strbuff.append("Method annotations\n");
309             for (ParsedMethod p:methods)
310                 strbuff.append(p+"\n");
311             strbuff.append("\n");
312             strbuff.append("Field annotations\n");
313             for (ParsedField f:fields)
314                 strbuff.append(f+"\n");   
315             strbuff.append("\n");
316             return strbuff.toString();
317         }
318     }
319     
320     
321     /**
322      * ParsedMethod
323      *
324      * A class method that can contain annotations.
325      */
326     public static class ParsedMethod extends AnnotatedStructure
327     {
328         ParsedClass pclass;
329         String methodName;
330         String paramString;
331         Method method;
332         
333        
334         public ParsedMethod(ParsedClass pclass, String name, String paramString)
335         {
336             this.pclass=pclass;
337             this.methodName=name;
338             this.paramString=paramString;
339         }
340         
341         
342         
343         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
344         {
345             this.pclass.addMethod(this);
346             return addAnnotation(desc);
347         }
348         
349         public Method toMethod ()
350         throws Exception
351         {
352             if (method == null)
353             {
354                 Type[] types = null;
355                 if (paramString!=null)
356                     types = Type.getArgumentTypes(paramString);
357 
358                 Class[] args = convertTypes(types);       
359                 method = pclass.toClass().getDeclaredMethod(methodName, args);
360             }
361             
362             return method;
363         }
364         
365         public boolean matches (String name, String paramString)
366         {
367             if (!methodName.equals(name))
368                 return false;
369             
370             if (this.paramString!=null && this.paramString.equals(paramString))
371                 return true;
372             
373             return (this.paramString == paramString);
374         }
375         
376         public String toString ()
377         {
378             return pclass.getClassName()+"."+methodName+"\n\t"+super.toString();
379         }
380     }
381     
382     /**
383      * ParsedField
384      *
385      * A class field that can contain annotations. Also implements the 
386      * asm visitor for Annotations.
387      */
388     public static class ParsedField extends AnnotatedStructure
389     {
390         ParsedClass pclass;
391         String fieldName;
392         Field field;
393       
394         public ParsedField (ParsedClass pclass, String name)
395         {
396             this.pclass=pclass;
397             this.fieldName=name;
398         }  
399         
400         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
401         { 
402             this.pclass.addField(this);
403             return addAnnotation(desc);
404         }
405         
406         public Field toField ()
407         throws Exception
408         {
409             if (field==null)
410             {
411                 field=this.pclass.toClass().getDeclaredField(fieldName);
412             }
413             return field;
414         }
415         
416         
417         public boolean matches (String name)
418         {
419             if (fieldName.equals(name))
420                 return true;
421             
422             return false;
423         }
424         
425         public String toString ()
426         {
427             return pclass.getClassName()+"."+fieldName+"\n\t"+super.toString();
428         }
429     }
430     
431     
432     
433     /**
434      * MyClassVisitor
435      *
436      * ASM visitor for a class.
437      */
438     public class MyClassVisitor extends EmptyVisitor
439     {
440         ParsedClass pclass;
441       
442 
443         public void visit (int version,
444                 int access,
445                 String name,
446                 String signature,
447                 String superName,
448                 String[] interfaces)
449         {     
450             pclass = new ParsedClass(name, superName);
451         }
452 
453         public AnnotationVisitor visitAnnotation (String desc, boolean visible)
454         {  
455             if (!parsedClasses.containsKey(pclass.getClassName()))
456                 parsedClasses.put(pclass.getClassName(), pclass);
457 
458             return pclass.addAnnotation(desc);
459         }
460 
461         public MethodVisitor visitMethod (int access,
462                 String name,
463                 String desc,
464                 String signature,
465                 String[] exceptions)
466         {   
467             if (!parsedClasses.values().contains(pclass))
468                 parsedClasses.put(pclass.getClassName(),pclass);
469 
470             ParsedMethod method = pclass.getMethod(name, desc);
471             if (method==null)
472                 method = new ParsedMethod(pclass, name, desc);
473             return method;
474         }
475 
476         public FieldVisitor visitField (int access,
477                 String name,
478                 String desc,
479                 String signature,
480                 Object value)
481         {
482             if (!parsedClasses.values().contains(pclass))
483                 parsedClasses.put(pclass.getClassName(),pclass);
484             
485             ParsedField field = pclass.getField(name);
486             if (field==null)
487                 field = new ParsedField(pclass, name);
488             return field;
489         }
490     }
491     
492  
493    
494     
495     
496     
497     public void find (String className, ClassNameResolver resolver) 
498     throws Exception
499     {
500         if (className == null)
501             return;
502         
503         if (!resolver.isExcluded(className))
504         {
505             if ((parsedClasses.get(className) == null) || (resolver.shouldOverride(className)))
506             {
507                 parsedClasses.remove(className);
508                 className = className.replace('.', '/')+".class";
509                 URL resource = Loader.getResource(this.getClass(), className, false);
510                 if (resource!= null)
511                     scanClass(resource.openStream());
512             }
513         }
514     }
515     
516     public void find (String[] classNames, ClassNameResolver resolver)
517     throws Exception
518     {
519         if (classNames == null)
520             return;
521         
522         find(Arrays.asList(classNames), resolver); 
523     }
524     
525     public void find (List<String> classNames, ClassNameResolver resolver)
526     throws Exception
527     {
528         for (String s:classNames)
529         {
530             if (!resolver.isExcluded(s))
531             {
532                 if ((parsedClasses.get(s) == null) || (resolver.shouldOverride(s)))
533                 {                
534                     parsedClasses.remove(s);
535                     s = s.replace('.', '/')+".class";
536                     URL resource = Loader.getResource(this.getClass(), s, false);
537                     if (resource!= null)
538                         scanClass(resource.openStream());
539                 }
540             }
541         }
542     }
543     
544     public void find (Resource dir, ClassNameResolver resolver)
545     throws Exception
546     {
547         if (!dir.isDirectory() || !dir.exists())
548             return;
549         
550         
551         String[] files=dir.list();
552         for (int f=0;files!=null && f<files.length;f++)
553         {
554             try 
555             {
556                 Resource res = dir.addPath(files[f]);
557                 if (res.isDirectory())
558                     find(res, resolver);
559                 String name = res.getName();
560                 if (name.endsWith(".class"))
561                 {
562                     if (!resolver.isExcluded(name))
563                     {
564                         if ((parsedClasses.get(name) == null) || (resolver.shouldOverride(name)))
565                         {
566                             parsedClasses.remove(name);
567                             scanClass(res.getURL().openStream());
568                         }
569                     }
570                 }
571             }
572             catch (Exception ex)
573             {
574                 Log.warn(Log.EXCEPTION,ex);
575             }
576         }
577     }
578     
579     
580     /**
581      * Find annotations on classes in the supplied classloader. 
582      * Only class files in jar files will be scanned.
583      * @param loader
584      * @param visitParents
585      * @param jarNamePattern
586      * @param nullInclusive
587      * @param resolver
588      * @throws Exception
589      */
590     public void find (ClassLoader loader, boolean visitParents, boolean nullInclusive, final ClassNameResolver resolver)
591     throws Exception
592     {
593         if (loader==null)
594             return;
595         
596         if (!(loader instanceof URLClassLoader))
597             return; //can't extract classes?
598        
599         JarScanner scanner = new JarScanner()
600         {
601             public void processEntry(URI jarUri, JarEntry entry)
602             {   
603                 try
604                 {
605                     String name = entry.getName();
606                     if (name.toLowerCase().endsWith(".class"))
607                     {
608                         String shortName =  name.replace('/', '.').substring(0,name.length()-6);
609                         if (!resolver.isExcluded(shortName))
610                         {
611                             if ((parsedClasses.get(shortName) == null) || (resolver.shouldOverride(shortName)))
612                             {
613                                 parsedClasses.remove(shortName);
614                                 Resource clazz = Resource.newResource("jar:"+jarUri+"!/"+name);                     
615                                 scanClass(clazz.getInputStream());
616                             }
617                         }
618                     }
619                 }
620                 catch (Exception e)
621                 {
622                     Log.warn("Problem processing jar entry "+entry, e);
623                 }
624             }
625             
626         };
627 
628         scanner.scan(null, loader, nullInclusive, visitParents);
629     }
630     
631     
632     /**
633      * Find annotations in classes in the supplied url of jar files.
634      * @param uris
635      * @param resolver
636      * @throws Exception
637      */
638     public void find (URI[] uris, final ClassNameResolver resolver)
639     throws Exception
640     {
641         if (uris==null)
642             return;
643         
644         JarScanner scanner = new JarScanner()
645         {
646             public void processEntry(URI jarUri, JarEntry entry)
647             {   
648                 try
649                 {
650                     String name = entry.getName();
651                     if (name.toLowerCase().endsWith(".class"))
652                     {
653                         String shortName =  name.replace('/', '.').substring(0,name.length()-6);
654                         if (!resolver.isExcluded(shortName))
655                         {
656                             if ((parsedClasses.get(shortName) == null) || (resolver.shouldOverride(shortName)))
657                             {
658                                 parsedClasses.remove(shortName);
659                                 Resource clazz = Resource.newResource("jar:"+jarUri+"!/"+name);                     
660                                 scanClass(clazz.getInputStream());
661                             }
662                         }
663                     }
664                 }
665                 catch (Exception e)
666                 {
667                     Log.warn("Problem processing jar entry "+entry, e);
668                 }
669             }
670             
671         };        
672         scanner.scan(null, uris, true);
673     }
674     
675     
676     /** Exclude class by name
677      * Instances of {@link AnnotationFinder} can implement this method to exclude
678      * classes by name.
679      * @param name
680      * @return
681      */
682     protected boolean excludeClass (String name)
683     {
684         return false;
685     }
686     
687 
688 
689     public List<Class<?>> getClassesForAnnotation(Class<?> annotationClass)
690     throws Exception
691     {
692         List<Class<?>> classes = new ArrayList<Class<?>>();
693         for (Map.Entry<String, ParsedClass> e: parsedClasses.entrySet())
694         {
695             ParsedClass pc = e.getValue();
696             Map<String, Map<String,Object>> annotations = pc.getAnnotations();
697             for (String key:annotations.keySet())
698             {
699                 if (key.equals(annotationClass.getName()))
700                 {
701                     classes.add(pc.toClass());
702                 }
703             }
704         }           
705         return classes;
706 
707     }
708 
709 
710 
711     public List<Method>  getMethodsForAnnotation (Class<?> annotationClass)
712     throws Exception
713     {
714 
715         List<Method> methods = new ArrayList<Method>();
716 
717         for (Map.Entry<String, ParsedClass> e: parsedClasses.entrySet())
718         {
719             ParsedClass pc = e.getValue();
720 
721             List<ParsedMethod> pmethods = pc.getMethods();
722             for (ParsedMethod p:pmethods)
723             {
724                 for (String key:p.getAnnotations().keySet())
725                 {
726                     if (key.equals(annotationClass.getName()))
727                     {
728                         methods.add(p.toMethod());
729                     }
730                 }
731             }
732         }           
733         return methods;
734 
735     }
736 
737 
738     public List<Field> getFieldsForAnnotation (Class<?> annotation)
739     throws Exception
740     {
741 
742         List<Field> fields = new ArrayList<Field>();
743         for (Map.Entry<String, ParsedClass> e: parsedClasses.entrySet())
744         {
745             ParsedClass pc = e.getValue();
746 
747             List<ParsedField> pfields = pc.getFields();
748             for (ParsedField f:pfields)
749             {
750                 for (String key:f.getAnnotations().keySet())
751                 {
752                     if (key.equals(annotation.getName()))
753                     {
754                         fields.add(f.toField());
755                     }
756                 }
757             }
758         }           
759         return fields;
760     }
761 
762 
763     public String toString ()
764     {
765         StringBuffer strbuff = new StringBuffer();
766         for (Map.Entry<String, ParsedClass> e:parsedClasses.entrySet())
767         {
768             strbuff.append(e.getValue());
769             strbuff.append("\n");
770         }
771         return strbuff.toString();
772     }
773     
774 
775     private void scanClass (InputStream is)
776     throws IOException
777     {
778         ClassReader reader = new ClassReader(is);
779         reader.accept(new MyClassVisitor(), ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
780     }
781 }