1   package com.atlassian.plugins.rest.module.scanner;
2   
3   import org.apache.commons.lang.StringUtils;
4   import org.apache.commons.lang.Validate;
5   import org.objectweb.asm.AnnotationVisitor;
6   import org.objectweb.asm.Attribute;
7   import org.objectweb.asm.ClassReader;
8   import org.objectweb.asm.ClassVisitor;
9   import org.objectweb.asm.FieldVisitor;
10  import org.objectweb.asm.Label;
11  import org.objectweb.asm.MethodVisitor;
12  import org.objectweb.asm.Opcodes;
13  import org.osgi.framework.Bundle;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.UnsupportedEncodingException;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.net.URLDecoder;
24  import java.util.Enumeration;
25  import java.util.HashSet;
26  import java.util.Set;
27  import java.util.jar.JarEntry;
28  import java.util.jar.JarFile;
29  
30  /**
31   * <p>Search for Java classes in the specified OSGi bundle that are annotated or have at least one method annotated with one or more of a set of annotations.</p>
32   * <p>This implementation is <em>inspired</em> by the {@link com.sun.jersey.server.impl.container.config.AnnotatedClassScanner} in Jersey 1.0.1.</p>
33   * @see com.sun.jersey.server.impl.container.config.AnnotatedClassScanner
34   */
35  public final class AnnotatedClassScanner
36  {
37      private final static Logger LOGGER = LoggerFactory.getLogger(AnnotatedClassScanner.class);
38  
39      private final Bundle bundle;
40      private final Set<String> annotations;
41  
42      public AnnotatedClassScanner(Bundle bundle, Class<?>... annotations)
43      {
44          Validate.notNull(bundle);
45          Validate.notEmpty(annotations, "You gotta scan for something!");
46  
47          this.bundle = bundle;
48          this.annotations = getAnnotationSet(annotations);
49      }
50  
51      public Set<Class<?>> scan(String... basePackages)
52      {
53          final File bundleFile = getBundleFile(bundle);
54          if (!bundleFile.isFile() || !bundleFile.exists())
55          {
56              throw new RuntimeException("Could not identify Bundle at location <" + bundle.getLocation() + ">");
57          }
58          return indexJar(bundleFile, preparePackages(basePackages));
59      }
60  
61      File getBundleFile(Bundle bundle)
62      {
63          final String bundleLocation = bundle.getLocation();
64          final File bundleFile;
65          if (bundleLocation.startsWith("file:"))
66          {
67              try
68              {
69                  bundleFile = new File(URLDecoder.decode(new URL(bundleLocation).getFile(), "UTF-8"));
70              }
71              catch (MalformedURLException e)
72              {
73                  throw new RuntimeException("Could not parse Bundle location as URL <" + bundleLocation + ">", e);
74              }
75              catch (UnsupportedEncodingException e)
76              {
77                  throw new IllegalStateException("Obviously something is wrong with your JVM... It doesn't support UTF-8 !?!", e);
78              }
79          }
80          else
81          {
82              bundleFile = new File(bundleLocation);
83          }
84          return bundleFile;
85      }
86  
87      private Set<String> preparePackages(String... packages)
88      {
89          final Set<String> packageNames = new HashSet<String>();
90          for (String packageName : packages)
91          {
92              final String newPackageName = StringUtils.replaceChars(packageName, '.', '/');
93  
94              // make sure we have a trailing / to not confuse packages such as com.mycompany.package
95              // and com.mycompany.package1 which would both start with com/mycompany/package once transformed
96              if (!newPackageName.endsWith("/"))
97              {
98                  packageNames.add(newPackageName + '/');
99              }
100             else
101             {
102                 packageNames.add(newPackageName);
103             }
104         }
105 
106         return packageNames;
107     }
108 
109     private Set<String> getAnnotationSet(Class... annotations)
110     {
111         final Set<String> formatedAnnotations = new HashSet<String>();
112         for (Class cls : annotations)
113         {
114             formatedAnnotations.add("L" + cls.getName().replaceAll("\\.", "/") + ";");
115         }
116         return formatedAnnotations;
117     }
118 
119     private Set<Class<?>> indexJar(File file, Set<String> packageNames)
120     {
121         // Make sure the set doesn't allow <code>null</code>
122         final Set<Class<?>> classes = new HashSet<Class<?>>()
123         {
124             @Override
125             public boolean add(Class<?> c)
126             {
127                 return c != null && super.add(c);
128             }
129         };
130 
131         JarFile jar = null;
132         try
133         {
134             jar = new JarFile(file);
135             for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements();)
136             {
137                 final JarEntry jarEntry = entries.nextElement();
138                 if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".class"))
139                 {
140                     if (packageNames.isEmpty())
141                     {
142                         classes.add(analyzeClassFile(jar, jarEntry));
143                     }
144                     else
145                     {
146                         for (String packageName : packageNames)
147                         {
148                             if (jarEntry.getName().startsWith(packageName))
149                             {
150                                 classes.add(analyzeClassFile(jar, jarEntry));
151                                 break;
152                             }
153                         }
154                     }
155                 }
156             }
157         }
158         catch (Exception e)
159         {
160             LOGGER.error("Exception while processing file, " + file, e);
161         }
162         finally
163         {
164             try
165             {
166                 if (jar != null)
167                 {
168                     jar.close();
169                 }
170             }
171             catch (IOException ex)
172             {
173                 LOGGER.error("Error closing jar file, {}", jar.getName());
174             }
175         }
176         return classes;
177     }
178 
179     private Class analyzeClassFile(JarFile jarFile, JarEntry entry)
180     {
181         final AnnotatedClassVisitor visitor = new AnnotatedClassVisitor(annotations);
182         getClassReader(jarFile, entry).accept(visitor, 0);
183         return visitor.hasAnnotation() ? getClassForName(visitor.className) : null;
184     }
185 
186     private ClassReader getClassReader(JarFile jarFile, JarEntry entry)
187     {
188         InputStream is = null;
189         try
190         {
191             is = jarFile.getInputStream(entry);
192             return new ClassReader(is);
193         }
194         catch (IOException ex)
195         {
196             throw new RuntimeException("Error accessing input stream of the jar file, " + jarFile.getName() + ", entry, " + entry.getName(), ex);
197         }
198         finally
199         {
200             try
201             {
202                 if (is != null)
203                 {
204                     is.close();
205                 }
206             }
207             catch (IOException ex)
208             {
209                 LOGGER.error("Error closing input stream of the jar file, {}, entry, {}, closed.", jarFile.getName(), entry.getName());
210             }
211         }
212     }
213 
214     private Class getClassForName(String className)
215     {
216         try
217         {
218             return bundle.loadClass(className.replaceAll("/", "."));
219         }
220         catch (ClassNotFoundException ex)
221         {
222             throw new RuntimeException("A class file of the class name, " + className + " is identified but the class could not be loaded", ex);
223         }
224     }
225 
226     private static final class AnnotatedClassVisitor implements ClassVisitor
227     {
228 
229         private final Set<String> annotations;
230 
231         /**
232          * The name of the visited class.
233          */
234         private String className;
235 
236         /**
237          * True if the class has the correct scope
238          */
239         private boolean isScoped;
240         /**
241          * True if the class has the correct declared annotations
242          */
243         private boolean isAnnotated;
244 
245         public AnnotatedClassVisitor(Set<String> annotations)
246         {
247             this.annotations = annotations;
248         }
249 
250         public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
251         {
252             className = name;
253             isScoped = (access & Opcodes.ACC_PUBLIC) != 0;
254             isAnnotated = false;
255         }
256 
257         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
258         {
259             isAnnotated |= annotations.contains(desc);
260             return null;
261         }
262 
263         public void visitInnerClass(String name, String outerName, String innerName, int access)
264         {
265             // If the name of the class that was visited is equal to the name of this visited inner class then
266             // this access field needs to be used for checking the scope of the inner class
267             if (className.equals(name))
268             {
269                 isScoped = (access & Opcodes.ACC_PUBLIC) != 0;
270                 // Inner classes need to be statically scoped
271                 isScoped &= (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC;
272             }
273         }
274 
275         boolean hasAnnotation()
276         {
277             return isScoped && isAnnotated;
278         }
279 
280         public void visitEnd()
281         {
282         }
283 
284 
285         public void visitOuterClass(String string, String string0, String string1)
286         {
287         }
288 
289         public FieldVisitor visitField(int i, String string, String string0, String string1, Object object)
290         {
291             return null;
292         }
293 
294         public void visitSource(String string, String string0)
295         {
296         }
297 
298         public void visitAttribute(Attribute attribute)
299         {
300         }
301 
302         public MethodVisitor visitMethod(int i, String string, String string0, String string1, String[] string2)
303         {
304             if (isAnnotated)
305             {
306                 // the class has already been found annotated, no need to visit methods
307                 return null;
308             }
309 
310             return new MethodVisitor()
311             {
312                 public AnnotationVisitor visitAnnotationDefault()
313                 {
314                     return null;
315                 }
316 
317                 public AnnotationVisitor visitAnnotation(String desc, boolean visible)
318                 {
319                     isAnnotated |= annotations.contains(desc);
320                     return null;
321                 }
322 
323                 public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible)
324                 {
325                     return null;
326                 }
327 
328                 public void visitAttribute(Attribute attr)
329                 {
330                 }
331 
332                 public void visitCode()
333                 {
334                 }
335 
336                 public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack)
337                 {
338                 }
339 
340                 public void visitInsn(int opcode)
341                 {
342                 }
343 
344                 public void visitIntInsn(int opcode, int operand)
345                 {
346                 }
347 
348                 public void visitVarInsn(int opcode, int var)
349                 {
350                 }
351 
352                 public void visitTypeInsn(int opcode, String type)
353                 {
354                 }
355 
356                 public void visitFieldInsn(int opcode, String owner, String name, String desc)
357                 {
358                 }
359 
360                 public void visitMethodInsn(int opcode, String owner, String name, String desc)
361                 {
362                 }
363 
364                 public void visitJumpInsn(int opcode, Label label)
365                 {
366                 }
367 
368                 public void visitLabel(Label label)
369                 {
370                 }
371 
372                 public void visitLdcInsn(Object cst)
373                 {
374                 }
375 
376                 public void visitIincInsn(int var, int increment)
377                 {
378                 }
379 
380                 public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels)
381                 {
382                 }
383 
384                 public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
385                 {
386                 }
387 
388                 public void visitMultiANewArrayInsn(String desc, int dims)
389                 {
390                 }
391 
392                 public void visitTryCatchBlock(Label start, Label end, Label handler, String type)
393                 {
394                 }
395 
396                 public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index)
397                 {
398                 }
399 
400                 public void visitLineNumber(int line, Label start)
401                 {
402                 }
403 
404                 public void visitMaxs(int maxStack, int maxLocals)
405                 {
406                 }
407 
408                 public void visitEnd()
409                 {
410                 }
411             };
412         }
413     }
414 }