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