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