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