View Javadoc

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