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