001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.xbean.finder;
018
019 import org.objectweb.asm.AnnotationVisitor;
020 import org.objectweb.asm.ClassReader;
021 import org.objectweb.asm.FieldVisitor;
022 import org.objectweb.asm.MethodVisitor;
023 import org.objectweb.asm.commons.EmptyVisitor;
024
025 import java.io.File;
026 import java.io.IOException;
027 import java.io.InputStream;
028 import java.lang.annotation.Annotation;
029 import java.lang.reflect.Constructor;
030 import java.lang.reflect.Field;
031 import java.lang.reflect.Method;
032 import java.lang.reflect.AnnotatedElement;
033 import java.net.URL;
034 import java.net.JarURLConnection;
035 import java.net.URLDecoder;
036 import java.util.ArrayList;
037 import java.util.Arrays;
038 import java.util.Collection;
039 import java.util.Collections;
040 import java.util.Enumeration;
041 import java.util.HashMap;
042 import java.util.List;
043 import java.util.Map;
044 import java.util.jar.JarEntry;
045 import java.util.jar.JarInputStream;
046
047 /**
048 * ClassFinder searches the classpath of the specified classloader for
049 * packages, classes, constructors, methods, or fields with specific annotations.
050 *
051 * For security reasons ASM is used to find the annotations. Classes are not
052 * loaded unless they match the requirements of a called findAnnotated* method.
053 * Once loaded, these classes are cached.
054 *
055 * The getClassesNotLoaded() method can be used immediately after any find*
056 * method to get a list of classes which matched the find requirements (i.e.
057 * contained the annotation), but were unable to be loaded.
058 *
059 * @author David Blevins
060 * @version $Rev: 661180 $ $Date: 2008-05-29 02:08:46 +0000 (Thu, 29 May 2008) $
061 */
062 public class ClassFinder {
063 private final Map<String, List<Info>> annotated = new HashMap<String, List<Info>>();
064 private final List<ClassInfo> classInfos = new ArrayList<ClassInfo>();
065
066 private final ClassLoader classLoader;
067 private final List<String> classesNotLoaded = new ArrayList<String>();
068 private final int ASM_FLAGS = ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES;
069
070 /**
071 * Creates a ClassFinder that will search the urls in the specified classloader
072 * excluding the urls in the classloader's parent.
073 *
074 * To include the parent classloader, use:
075 *
076 * new ClassFinder(classLoader, false);
077 *
078 * To exclude the parent's parent, use:
079 *
080 * new ClassFinder(classLoader, classLoader.getParent().getParent());
081 *
082 * @param classLoader source of classes to scan
083 * @throws Exception if something goes wrong
084 */
085 public ClassFinder(ClassLoader classLoader) throws Exception {
086 this(classLoader, true);
087 }
088
089 /**
090 * Creates a ClassFinder that will search the urls in the specified classloader.
091 *
092 * @param classLoader source of classes to scan
093 * @param excludeParent Allegedly excludes classes from parent classloader, whatever that might mean
094 * @throws Exception if something goes wrong.
095 */
096 public ClassFinder(ClassLoader classLoader, boolean excludeParent) throws Exception {
097 this(classLoader, getUrls(classLoader, excludeParent));
098 }
099
100 /**
101 * Creates a ClassFinder that will search the urls in the specified classloader excluding
102 * the urls in the 'exclude' classloader.
103 *
104 * @param classLoader source of classes to scan
105 * @param exclude source of classes to exclude from scanning
106 * @throws Exception if something goes wrong
107 */
108 public ClassFinder(ClassLoader classLoader, ClassLoader exclude) throws Exception {
109 this(classLoader, getUrls(classLoader, exclude));
110 }
111
112 public ClassFinder(ClassLoader classLoader, URL url) {
113 this(classLoader, Arrays.asList(url));
114 }
115
116 public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
117 this.classLoader = classLoader;
118
119 List<String> classNames = new ArrayList<String>();
120 for (URL location : urls) {
121 try {
122 if (location.getProtocol().equals("jar")) {
123 classNames.addAll(jar(location));
124 } else if (location.getProtocol().equals("file")) {
125 try {
126 // See if it's actually a jar
127 URL jarUrl = new URL("jar", "", location.toExternalForm() + "!/");
128 JarURLConnection juc = (JarURLConnection) jarUrl.openConnection();
129 juc.getJarFile();
130 classNames.addAll(jar(jarUrl));
131 } catch (IOException e) {
132 classNames.addAll(file(location));
133 }
134 }
135 } catch (Exception e) {
136 e.printStackTrace();
137 }
138 }
139
140 for (String className : classNames) {
141 readClassDef(className);
142 }
143 }
144
145 public ClassFinder(Class... classes){
146 this(Arrays.asList(classes));
147 }
148
149 public ClassFinder(List<Class> classes){
150 this.classLoader = null;
151 List<Info> infos = new ArrayList<Info>();
152 List<Package> packages = new ArrayList<Package>();
153 for (Class clazz : classes) {
154
155 Package aPackage = clazz.getPackage();
156 if (aPackage != null && !packages.contains(aPackage)){
157 infos.add(new PackageInfo(aPackage));
158 packages.add(aPackage);
159 }
160
161 ClassInfo classInfo = new ClassInfo(clazz);
162 infos.add(classInfo);
163 classInfos.add(classInfo);
164 for (Method method : clazz.getDeclaredMethods()) {
165 infos.add(new MethodInfo(classInfo, method));
166 }
167
168 for (Constructor constructor : clazz.getConstructors()) {
169 infos.add(new MethodInfo(classInfo, constructor));
170 }
171
172 for (Field field : clazz.getDeclaredFields()) {
173 infos.add(new FieldInfo(classInfo, field));
174 }
175 }
176
177 for (Info info : infos) {
178 for (AnnotationInfo annotation : info.getAnnotations()) {
179 List<Info> annotationInfos = getAnnotationInfos(annotation.getName());
180 annotationInfos.add(info);
181 }
182 }
183 }
184
185 public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
186 List<Info> infos = annotated.get(annotation.getName());
187 return infos != null && !infos.isEmpty();
188 }
189
190 /**
191 * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
192 * <p/>
193 * The list will only contain entries of classes whose byte code matched the requirements
194 * of last invoked find* method, but were unable to be loaded and included in the results.
195 * <p/>
196 * The list returned is unmodifiable. Once obtained, the returned list will be a live view of the
197 * results from the last findAnnotated* method call.
198 * <p/>
199 * This method is not thread safe.
200 * @return an unmodifiable live view of classes that could not be loaded in previous findAnnotated* call.
201 */
202 public List<String> getClassesNotLoaded() {
203 return Collections.unmodifiableList(classesNotLoaded);
204 }
205
206 public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
207 classesNotLoaded.clear();
208 List<Package> packages = new ArrayList<Package>();
209 List<Info> infos = getAnnotationInfos(annotation.getName());
210 for (Info info : infos) {
211 if (info instanceof PackageInfo) {
212 PackageInfo packageInfo = (PackageInfo) info;
213 try {
214 Package pkg = packageInfo.get();
215 // double check via proper reflection
216 if (pkg.isAnnotationPresent(annotation)) {
217 packages.add(pkg);
218 }
219 } catch (ClassNotFoundException e) {
220 classesNotLoaded.add(packageInfo.getName());
221 }
222 }
223 }
224 return packages;
225 }
226
227 public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) {
228 classesNotLoaded.clear();
229 List<Class> classes = new ArrayList<Class>();
230 List<Info> infos = getAnnotationInfos(annotation.getName());
231 for (Info info : infos) {
232 if (info instanceof ClassInfo) {
233 ClassInfo classInfo = (ClassInfo) info;
234 try {
235 Class clazz = classInfo.get();
236 // double check via proper reflection
237 if (clazz.isAnnotationPresent(annotation)) {
238 classes.add(clazz);
239 }
240 } catch (ClassNotFoundException e) {
241 classesNotLoaded.add(classInfo.getName());
242 }
243 }
244 }
245 return classes;
246 }
247
248 public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
249 classesNotLoaded.clear();
250 List<ClassInfo> seen = new ArrayList<ClassInfo>();
251 List<Method> methods = new ArrayList<Method>();
252 List<Info> infos = getAnnotationInfos(annotation.getName());
253 for (Info info : infos) {
254 if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
255 MethodInfo methodInfo = (MethodInfo) info;
256 ClassInfo classInfo = methodInfo.getDeclaringClass();
257
258 if (seen.contains(classInfo)) continue;
259
260 seen.add(classInfo);
261
262 try {
263 Class clazz = classInfo.get();
264 for (Method method : clazz.getDeclaredMethods()) {
265 if (method.isAnnotationPresent(annotation)) {
266 methods.add(method);
267 }
268 }
269 } catch (ClassNotFoundException e) {
270 classesNotLoaded.add(classInfo.getName());
271 }
272 }
273 }
274 return methods;
275 }
276
277 public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
278 classesNotLoaded.clear();
279 List<ClassInfo> seen = new ArrayList<ClassInfo>();
280 List<Constructor> constructors = new ArrayList<Constructor>();
281 List<Info> infos = getAnnotationInfos(annotation.getName());
282 for (Info info : infos) {
283 if (info instanceof MethodInfo && info.getName().equals("<init>")) {
284 MethodInfo methodInfo = (MethodInfo) info;
285 ClassInfo classInfo = methodInfo.getDeclaringClass();
286
287 if (seen.contains(classInfo)) continue;
288
289 seen.add(classInfo);
290
291 try {
292 Class clazz = classInfo.get();
293 for (Constructor constructor : clazz.getConstructors()) {
294 if (constructor.isAnnotationPresent(annotation)) {
295 constructors.add(constructor);
296 }
297 }
298 } catch (ClassNotFoundException e) {
299 classesNotLoaded.add(classInfo.getName());
300 }
301 }
302 }
303 return constructors;
304 }
305
306 public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
307 classesNotLoaded.clear();
308 List<ClassInfo> seen = new ArrayList<ClassInfo>();
309 List<Field> fields = new ArrayList<Field>();
310 List<Info> infos = getAnnotationInfos(annotation.getName());
311 for (Info info : infos) {
312 if (info instanceof FieldInfo) {
313 FieldInfo fieldInfo = (FieldInfo) info;
314 ClassInfo classInfo = fieldInfo.getDeclaringClass();
315
316 if (seen.contains(classInfo)) continue;
317
318 seen.add(classInfo);
319
320 try {
321 Class clazz = classInfo.get();
322 for (Field field : clazz.getDeclaredFields()) {
323 if (field.isAnnotationPresent(annotation)) {
324 fields.add(field);
325 }
326 }
327 } catch (ClassNotFoundException e) {
328 classesNotLoaded.add(classInfo.getName());
329 }
330 }
331 }
332 return fields;
333 }
334
335 public List<Class> findClassesInPackage(String packageName, boolean recursive) {
336 classesNotLoaded.clear();
337 List<Class> classes = new ArrayList<Class>();
338 for (ClassInfo classInfo : classInfos) {
339 try {
340 if (recursive && classInfo.getPackageName().startsWith(packageName)){
341 classes.add(classInfo.get());
342 } else if (classInfo.getPackageName().equals(packageName)){
343 classes.add(classInfo.get());
344 }
345 } catch (ClassNotFoundException e) {
346 classesNotLoaded.add(classInfo.getName());
347 }
348 }
349 return classes;
350 }
351
352 private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
353 return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
354 }
355
356 private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
357 UrlSet urlSet = new UrlSet(classLoader);
358 if (excludeParent != null){
359 urlSet = urlSet.exclude(excludeParent);
360 }
361 return urlSet.getUrls();
362 }
363
364 private List<String> file(URL location) {
365 List<String> classNames = new ArrayList<String>();
366 File dir = new File(URLDecoder.decode(location.getPath()));
367 if (dir.getName().equals("META-INF")) {
368 dir = dir.getParentFile(); // Scrape "META-INF" off
369 }
370 if (dir.isDirectory()) {
371 scanDir(dir, classNames, "");
372 }
373 return classNames;
374 }
375
376 private void scanDir(File dir, List<String> classNames, String packageName) {
377 File[] files = dir.listFiles();
378 for (File file : files) {
379 if (file.isDirectory()) {
380 scanDir(file, classNames, packageName + file.getName() + ".");
381 } else if (file.getName().endsWith(".class")) {
382 String name = file.getName();
383 name = name.replaceFirst(".class$", "");
384 classNames.add(packageName + name);
385 }
386 }
387 }
388
389 private List<String> jar(URL location) throws IOException {
390 String jarPath = location.getFile();
391 if (jarPath.indexOf("!") > -1){
392 jarPath = jarPath.substring(0, jarPath.indexOf("!"));
393 }
394 URL url = new URL(jarPath);
395 InputStream in = url.openStream();
396 try {
397 JarInputStream jarStream = new JarInputStream(in);
398 return jar(jarStream);
399 } finally {
400 in.close();
401 }
402 }
403
404 private List<String> jar(JarInputStream jarStream) throws IOException {
405 List<String> classNames = new ArrayList<String>();
406
407 JarEntry entry;
408 while ((entry = jarStream.getNextJarEntry()) != null) {
409 if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
410 continue;
411 }
412 String className = entry.getName();
413 className = className.replaceFirst(".class$", "");
414 className = className.replace('/', '.');
415 classNames.add(className);
416 }
417
418 return classNames;
419 }
420
421 public class Annotatable {
422 private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();
423
424 public Annotatable(AnnotatedElement element) {
425 for (Annotation annotation : element.getAnnotations()) {
426 annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
427 }
428 }
429
430 public Annotatable() {
431 }
432
433 public List<AnnotationInfo> getAnnotations() {
434 return annotations;
435 }
436
437 }
438
439 public static interface Info {
440 String getName();
441
442 List<AnnotationInfo> getAnnotations();
443 }
444
445 public class PackageInfo extends Annotatable implements Info {
446 private final String name;
447 private final ClassInfo info;
448 private final Package pkg;
449
450 public PackageInfo(Package pkg){
451 super(pkg);
452 this.pkg = pkg;
453 this.name = pkg.getName();
454 this.info = null;
455 }
456
457 public PackageInfo(String name) {
458 info = new ClassInfo(name, null);
459 this.name = name;
460 this.pkg = null;
461 }
462
463 public String getName() {
464 return name;
465 }
466
467 public Package get() throws ClassNotFoundException {
468 return (pkg != null)?pkg:info.get().getPackage();
469 }
470 }
471
472 public class ClassInfo extends Annotatable implements Info {
473 private final String name;
474 private final List<MethodInfo> methods = new ArrayList<MethodInfo>();
475 private final List<MethodInfo> constructors = new ArrayList<MethodInfo>();
476 private final String superType;
477 private final List<String> interfaces = new ArrayList<String>();
478 private final List<FieldInfo> fields = new ArrayList<FieldInfo>();
479 private Class<?> clazz;
480 private ClassNotFoundException notFound;
481
482 public ClassInfo(Class clazz) {
483 super(clazz);
484 this.clazz = clazz;
485 this.name = clazz.getName();
486 Class superclass = clazz.getSuperclass();
487 this.superType = superclass != null ? superclass.getName(): null;
488 }
489
490 public ClassInfo(String name, String superType) {
491 this.name = name;
492 this.superType = superType;
493 }
494
495 public String getPackageName(){
496 return name.substring(name.lastIndexOf(".")+1, name.length());
497 }
498
499 public List<MethodInfo> getConstructors() {
500 return constructors;
501 }
502
503 public List<String> getInterfaces() {
504 return interfaces;
505 }
506
507 public List<FieldInfo> getFields() {
508 return fields;
509 }
510
511 public List<MethodInfo> getMethods() {
512 return methods;
513 }
514
515 public String getName() {
516 return name;
517 }
518
519 public String getSuperType() {
520 return superType;
521 }
522
523 public Class get() throws ClassNotFoundException {
524 if (clazz != null) return clazz;
525 if (notFound != null) throw notFound;
526 try {
527 this.clazz = classLoader.loadClass(name);
528 return clazz;
529 } catch (ClassNotFoundException notFound) {
530 classesNotLoaded.add(name);
531 this.notFound = notFound;
532 throw notFound;
533 }
534 }
535
536 public String toString() {
537 return name;
538 }
539 }
540
541 public class MethodInfo extends Annotatable implements Info {
542 private final ClassInfo declaringClass;
543 private final String returnType;
544 private final String name;
545 private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList<List<AnnotationInfo>>();
546
547 public MethodInfo(ClassInfo info, Constructor constructor){
548 super(constructor);
549 this.declaringClass = info;
550 this.name = "<init>";
551 this.returnType = Void.TYPE.getName();
552 }
553
554 public MethodInfo(ClassInfo info, Method method){
555 super(method);
556 this.declaringClass = info;
557 this.name = method.getName();
558 this.returnType = method.getReturnType().getName();
559 }
560
561 public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
562 this.declaringClass = declarignClass;
563 this.name = name;
564 this.returnType = returnType;
565 }
566
567 public List<List<AnnotationInfo>> getParameterAnnotations() {
568 return parameterAnnotations;
569 }
570
571 public List<AnnotationInfo> getParameterAnnotations(int index) {
572 if (index >= parameterAnnotations.size()) {
573 for (int i = parameterAnnotations.size(); i <= index; i++) {
574 List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
575 parameterAnnotations.add(i, annotationInfos);
576 }
577 }
578 return parameterAnnotations.get(index);
579 }
580
581 public String getName() {
582 return name;
583 }
584
585 public ClassInfo getDeclaringClass() {
586 return declaringClass;
587 }
588
589 public String getReturnType() {
590 return returnType;
591 }
592
593 public String toString() {
594 return declaringClass + "@" + name;
595 }
596 }
597
598 public class FieldInfo extends Annotatable implements Info {
599 private final String name;
600 private final String type;
601 private final ClassInfo declaringClass;
602
603 public FieldInfo(ClassInfo info, Field field){
604 super(field);
605 this.declaringClass = info;
606 this.name = field.getName();
607 this.type = field.getType().getName();
608 }
609
610 public FieldInfo(ClassInfo declaringClass, String name, String type) {
611 this.declaringClass = declaringClass;
612 this.name = name;
613 this.type = type;
614 }
615
616 public String getName() {
617 return name;
618 }
619
620 public ClassInfo getDeclaringClass() {
621 return declaringClass;
622 }
623
624 public String getType() {
625 return type;
626 }
627
628 public String toString() {
629 return declaringClass + "#" + name;
630 }
631 }
632
633 public class AnnotationInfo extends Annotatable implements Info {
634 private final String name;
635
636 public AnnotationInfo(Annotation annotation){
637 this(annotation.getClass().getName());
638 }
639
640 public AnnotationInfo(Class<? extends Annotation> annotation) {
641 this.name = annotation.getName().intern();
642 }
643
644 public AnnotationInfo(String name) {
645 name = name.replaceAll("^L|;$", "");
646 name = name.replace('/', '.');
647 this.name = name.intern();
648 }
649
650 public String getName() {
651 return name;
652 }
653
654 public String toString() {
655 return name;
656 }
657 }
658
659 private List<Info> getAnnotationInfos(String name) {
660 List<Info> infos = annotated.get(name);
661 if (infos == null) {
662 infos = new ArrayList<Info>();
663 annotated.put(name, infos);
664 }
665 return infos;
666 }
667
668 private void readClassDef(String className) {
669 if (!className.endsWith(".class")) {
670 className = className.replace('.', '/') + ".class";
671 }
672 try {
673 URL resource = classLoader.getResource(className);
674 if (resource != null) {
675 InputStream in = resource.openStream();
676 try {
677 ClassReader classReader = new ClassReader(in);
678 classReader.accept(new InfoBuildingVisitor(), ASM_FLAGS);
679 } finally {
680 in.close();
681 }
682 } else {
683 new Exception("Could not load " + className).printStackTrace();
684 }
685 } catch (IOException e) {
686 e.printStackTrace();
687 }
688
689 }
690
691 public class InfoBuildingVisitor extends EmptyVisitor {
692 private Info info;
693
694 public InfoBuildingVisitor() {
695 }
696
697 public InfoBuildingVisitor(Info info) {
698 this.info = info;
699 }
700
701 public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
702 if (name.endsWith("package-info")) {
703 info = new PackageInfo(javaName(name));
704 } else {
705 ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName));
706
707 for (String interfce : interfaces) {
708 classInfo.getInterfaces().add(javaName(interfce));
709 }
710 info = classInfo;
711 classInfos.add(classInfo);
712 }
713 }
714
715 private String javaName(String name) {
716 return (name == null)? null:name.replace('/', '.');
717 }
718
719 public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
720 AnnotationInfo annotationInfo = new AnnotationInfo(desc);
721 info.getAnnotations().add(annotationInfo);
722 getAnnotationInfos(annotationInfo.getName()).add(info);
723 return new InfoBuildingVisitor(annotationInfo);
724 }
725
726 public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
727 ClassInfo classInfo = ((ClassInfo) info);
728 FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc);
729 classInfo.getFields().add(fieldInfo);
730 return new InfoBuildingVisitor(fieldInfo);
731 }
732
733 public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
734 ClassInfo classInfo = ((ClassInfo) info);
735 MethodInfo methodInfo = new MethodInfo(classInfo, name, desc);
736 classInfo.getMethods().add(methodInfo);
737 return new InfoBuildingVisitor(methodInfo);
738 }
739
740 public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) {
741 MethodInfo methodInfo = ((MethodInfo) info);
742 List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param);
743 AnnotationInfo annotationInfo = new AnnotationInfo(desc);
744 annotationInfos.add(annotationInfo);
745 return new InfoBuildingVisitor(annotationInfo);
746 }
747 }
748 }