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.MalformedURLException;
035 import java.net.URLConnection;
036 import java.net.JarURLConnection;
037 import java.util.ArrayList;
038 import java.util.Arrays;
039 import java.util.Collection;
040 import java.util.Collections;
041 import java.util.Enumeration;
042 import java.util.HashMap;
043 import java.util.List;
044 import java.util.Map;
045 import java.util.jar.JarEntry;
046 import java.util.jar.JarInputStream;
047
048 /**
049 * ClassFinder searches the classpath of the specified classloader for
050 * packages, classes, constructors, methods, or fields with specific annotations.
051 *
052 * For security reasons ASM is used to find the annotations. Classes are not
053 * loaded unless they match the requirements of a called findAnnotated* method.
054 * Once loaded, these classes are cached.
055 *
056 * The getClassesNotLoaded() method can be used immediately after any find*
057 * method to get a list of classes which matched the find requirements (i.e.
058 * contained the annotation), but were unable to be loaded.
059 *
060 * @author David Blevins
061 * @version $Rev: 483277 $ $Date: 2006-12-06 23:53:30 +0100 (mer., 06 déc. 2006) $
062 */
063 public class ClassFinder {
064 private final Map<String, List<Info>> annotated = new HashMap();
065 private final List<ClassInfo> classInfos = new ArrayList();
066
067 private final ClassLoader classLoader;
068 private final List<String> classesNotLoaded = new ArrayList();
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
083 * @throws Exception
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
093 * @param excludeParent
094 * @throws Exception
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
105 * @param exclude
106 * @throws Exception
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(new URL[]{url}));
114 }
115
116 public ClassFinder(ClassLoader classLoader, Collection<URL> urls) {
117 this.classLoader = classLoader;
118
119 List<String> classNames = new ArrayList();
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();
152 List<Package> packages = new ArrayList();
153 for (Class clazz : classes) {
154 if (!packages.contains(clazz.getPackage())){
155 infos.add(new PackageInfo(clazz.getPackage()));
156 packages.add(clazz.getPackage());
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 /**
184 * Returns a list of classes that could not be loaded in last invoked findAnnotated* method.
185 * <p/>
186 * The list will only contain entries of classes whose byte code matched the requirements
187 * of last invoked find* method, but were unable to be loaded and included in the results.
188 * <p/>
189 * The list returned is unmodifiable and the results of this method will change
190 * after each invocation of a findAnnotated* method.
191 * <p/>
192 * This method is not thread safe.
193 */
194 public List<String> getClassesNotLoaded() {
195 return Collections.unmodifiableList(classesNotLoaded);
196 }
197
198 public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation) {
199 classesNotLoaded.clear();
200 List<Package> packages = new ArrayList<Package>();
201 List<Info> infos = getAnnotationInfos(annotation.getName());
202 for (Info info : infos) {
203 if (info instanceof PackageInfo) {
204 PackageInfo packageInfo = (PackageInfo) info;
205 try {
206 Package pkg = packageInfo.get();
207 // double check via proper reflection
208 if (pkg.isAnnotationPresent(annotation)) {
209 packages.add(pkg);
210 }
211 } catch (ClassNotFoundException e) {
212 classesNotLoaded.add(packageInfo.getName());
213 }
214 }
215 }
216 return packages;
217 }
218
219 public List<Class> findAnnotatedClasses(Class<? extends Annotation> annotation) {
220 classesNotLoaded.clear();
221 List<Class> classes = new ArrayList<Class>();
222 List<Info> infos = getAnnotationInfos(annotation.getName());
223 for (Info info : infos) {
224 if (info instanceof ClassInfo) {
225 ClassInfo classInfo = (ClassInfo) info;
226 try {
227 Class clazz = classInfo.get();
228 // double check via proper reflection
229 if (clazz.isAnnotationPresent(annotation)) {
230 classes.add(clazz);
231 }
232 } catch (ClassNotFoundException e) {
233 classesNotLoaded.add(classInfo.getName());
234 }
235 }
236 }
237 return classes;
238 }
239
240 public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation) {
241 classesNotLoaded.clear();
242 List<ClassInfo> seen = new ArrayList<ClassInfo>();
243 List<Method> methods = new ArrayList<Method>();
244 List<Info> infos = getAnnotationInfos(annotation.getName());
245 for (Info info : infos) {
246 if (info instanceof MethodInfo && !info.getName().equals("<init>")) {
247 MethodInfo methodInfo = (MethodInfo) info;
248 ClassInfo classInfo = methodInfo.getDeclaringClass();
249
250 if (seen.contains(classInfo)) continue;
251
252 seen.add(classInfo);
253
254 try {
255 Class clazz = classInfo.get();
256 for (Method method : clazz.getDeclaredMethods()) {
257 if (method.isAnnotationPresent(annotation)) {
258 methods.add(method);
259 }
260 }
261 } catch (ClassNotFoundException e) {
262 classesNotLoaded.add(classInfo.getName());
263 }
264 }
265 }
266 return methods;
267 }
268
269 public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation) {
270 classesNotLoaded.clear();
271 List<ClassInfo> seen = new ArrayList<ClassInfo>();
272 List<Constructor> constructors = new ArrayList<Constructor>();
273 List<Info> infos = getAnnotationInfos(annotation.getName());
274 for (Info info : infos) {
275 if (info instanceof MethodInfo && info.getName().equals("<init>")) {
276 MethodInfo methodInfo = (MethodInfo) info;
277 ClassInfo classInfo = methodInfo.getDeclaringClass();
278
279 if (seen.contains(classInfo)) continue;
280
281 seen.add(classInfo);
282
283 try {
284 Class clazz = classInfo.get();
285 for (Constructor constructor : clazz.getConstructors()) {
286 if (constructor.isAnnotationPresent(annotation)) {
287 constructors.add(constructor);
288 }
289 }
290 } catch (ClassNotFoundException e) {
291 classesNotLoaded.add(classInfo.getName());
292 }
293 }
294 }
295 return constructors;
296 }
297
298 public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation) {
299 classesNotLoaded.clear();
300 List<ClassInfo> seen = new ArrayList<ClassInfo>();
301 List<Field> fields = new ArrayList<Field>();
302 List<Info> infos = getAnnotationInfos(annotation.getName());
303 for (Info info : infos) {
304 if (info instanceof FieldInfo) {
305 FieldInfo fieldInfo = (FieldInfo) info;
306 ClassInfo classInfo = fieldInfo.getDeclaringClass();
307
308 if (seen.contains(classInfo)) continue;
309
310 seen.add(classInfo);
311
312 try {
313 Class clazz = classInfo.get();
314 for (Field field : clazz.getDeclaredFields()) {
315 if (field.isAnnotationPresent(annotation)) {
316 fields.add(field);
317 }
318 }
319 } catch (ClassNotFoundException e) {
320 classesNotLoaded.add(classInfo.getName());
321 }
322 }
323 }
324 return fields;
325 }
326
327 public List<Class> findClassesInPackage(String packageName, boolean recursive) {
328 classesNotLoaded.clear();
329 List<Class> classes = new ArrayList();
330 for (ClassInfo classInfo : classInfos) {
331 try {
332 if (recursive && classInfo.getPackageName().startsWith(packageName)){
333 classes.add(classInfo.get());
334 } else if (classInfo.getPackageName().equals(packageName)){
335 classes.add(classInfo.get());
336 }
337 } catch (ClassNotFoundException e) {
338 classesNotLoaded.add(classInfo.getName());
339 }
340 }
341 return classes;
342 }
343
344 private static Collection<URL> getUrls(ClassLoader classLoader, boolean excludeParent) throws IOException {
345 return getUrls(classLoader, excludeParent? classLoader.getParent() : null);
346 }
347
348 private static Collection<URL> getUrls(ClassLoader classLoader, ClassLoader excludeParent) throws IOException {
349 Map<String, URL> urls = toMap(classLoader.getResources("META-INF"));
350
351 if (excludeParent != null) {
352 Map<String, URL> parentUrls = toMap(excludeParent.getResources("META-INF"));
353 for (String url : parentUrls.keySet()) {
354 urls.remove(url);
355 }
356 }
357
358 return urls.values();
359 }
360
361 private static Map<String, URL> toMap(Enumeration<URL> enumeration) {
362 Map<String, URL> urls = new HashMap();
363 while (enumeration.hasMoreElements()) {
364 URL url = enumeration.nextElement();
365 urls.put(url.toExternalForm(), url);
366 }
367 return urls;
368 }
369
370 private List<String> file(URL location) {
371 List<String> classNames = new ArrayList();
372 File dir = new File(location.getPath());
373 if (dir.getName().equals("META-INF")) {
374 dir = dir.getParentFile(); // Scrape "META-INF" off
375 }
376 if (dir.isDirectory()) {
377 scanDir(dir, classNames, "");
378 }
379 return classNames;
380 }
381
382 private void scanDir(File dir, List<String> classNames, String packageName) {
383 File[] files = dir.listFiles();
384 for (File file : files) {
385 if (file.isDirectory()) {
386 scanDir(file, classNames, packageName + file.getName() + ".");
387 } else if (file.getName().endsWith(".class")) {
388 String name = file.getName();
389 name = name.replaceFirst(".class$", "");
390 classNames.add(packageName + name);
391 }
392 }
393 }
394
395 private List<String> jar(URL location) throws IOException {
396 String jarPath = location.getFile();
397 if (jarPath.indexOf("!") > -1){
398 jarPath = jarPath.substring(0, jarPath.indexOf("!"));
399 }
400 URL url = new URL(jarPath);
401 InputStream in = url.openStream();
402 try {
403 JarInputStream jarStream = new JarInputStream(in);
404 return jar(jarStream);
405 } finally {
406 in.close();
407 }
408 }
409
410 private List<String> jar(JarInputStream jarStream) throws IOException {
411 List<String> classNames = new ArrayList();
412
413 JarEntry entry;
414 while ((entry = jarStream.getNextJarEntry()) != null) {
415 if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
416 continue;
417 }
418 String className = entry.getName();
419 className = className.replaceFirst(".class$", "");
420 className = className.replace('/', '.');
421 classNames.add(className);
422 }
423
424 return classNames;
425 }
426
427 public class Annotatable {
428 private final List<AnnotationInfo> annotations = new ArrayList();
429
430 public Annotatable(AnnotatedElement element) {
431 for (Annotation annotation : element.getAnnotations()) {
432 annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
433 }
434 }
435
436 public Annotatable() {
437 }
438
439 public List<AnnotationInfo> getAnnotations() {
440 return annotations;
441 }
442
443 }
444
445 public static interface Info {
446 String getName();
447
448 List<AnnotationInfo> getAnnotations();
449 }
450
451 public class PackageInfo extends Annotatable implements Info {
452 private final String name;
453 private final ClassInfo info;
454 private final Package pkg;
455
456 public PackageInfo(Package pkg){
457 super(pkg);
458 this.pkg = pkg;
459 this.name = pkg.getName();
460 this.info = null;
461 }
462
463 public PackageInfo(String name) {
464 info = new ClassInfo(name, null);
465 this.name = name;
466 this.pkg = null;
467 }
468
469 public String getName() {
470 return name;
471 }
472
473 public Package get() throws ClassNotFoundException {
474 return (pkg != null)?pkg:info.get().getPackage();
475 }
476 }
477
478 public class ClassInfo extends Annotatable implements Info {
479 private final String name;
480 private final List<MethodInfo> methods = new ArrayList();
481 private final List<MethodInfo> constructors = new ArrayList();
482 private final String superType;
483 private final List<String> interfaces = new ArrayList();
484 private final List<FieldInfo> fields = new ArrayList();
485 private Class<?> clazz;
486 private ClassNotFoundException notFound;
487
488 public ClassInfo(Class clazz) {
489 super(clazz);
490 this.clazz = clazz;
491 this.name = clazz.getName();
492 Class superclass = clazz.getSuperclass();
493 this.superType = superclass != null ? superclass.getName(): null;
494 }
495
496 public ClassInfo(String name, String superType) {
497 this.name = name;
498 this.superType = superType;
499 }
500
501 public String getPackageName(){
502 return name.substring(name.lastIndexOf(".")+1, name.length());
503 }
504
505 public List<MethodInfo> getConstructors() {
506 return constructors;
507 }
508
509 public List<String> getInterfaces() {
510 return interfaces;
511 }
512
513 public List<FieldInfo> getFields() {
514 return fields;
515 }
516
517 public List<MethodInfo> getMethods() {
518 return methods;
519 }
520
521 public String getName() {
522 return name;
523 }
524
525 public String getSuperType() {
526 return superType;
527 }
528
529 public Class get() throws ClassNotFoundException {
530 if (clazz != null) return clazz;
531 if (notFound != null) throw notFound;
532 try {
533 this.clazz = classLoader.loadClass(name);
534 return clazz;
535 } catch (ClassNotFoundException notFound) {
536 classesNotLoaded.add(name);
537 this.notFound = notFound;
538 throw notFound;
539 }
540 }
541
542 public String toString() {
543 return name;
544 }
545 }
546
547 public class MethodInfo extends Annotatable implements Info {
548 private final ClassInfo declaringClass;
549 private final String returnType;
550 private final String name;
551 private final List<List<AnnotationInfo>> parameterAnnotations = new ArrayList();
552
553 public MethodInfo(ClassInfo info, Constructor constructor){
554 super(constructor);
555 this.declaringClass = info;
556 this.name = "<init>";
557 this.returnType = Void.TYPE.getName();
558 }
559
560 public MethodInfo(ClassInfo info, Method method){
561 super(method);
562 this.declaringClass = info;
563 this.name = method.getName();
564 this.returnType = method.getReturnType().getName();
565 }
566
567 public MethodInfo(ClassInfo declarignClass, String name, String returnType) {
568 this.declaringClass = declarignClass;
569 this.name = name;
570 this.returnType = returnType;
571 }
572
573 public List<List<AnnotationInfo>> getParameterAnnotations() {
574 return parameterAnnotations;
575 }
576
577 public List<AnnotationInfo> getParameterAnnotations(int index) {
578 if (index >= parameterAnnotations.size()) {
579 for (int i = parameterAnnotations.size(); i <= index; i++) {
580 List<AnnotationInfo> annotationInfos = new ArrayList<AnnotationInfo>();
581 parameterAnnotations.add(i, annotationInfos);
582 }
583 }
584 return parameterAnnotations.get(index);
585 }
586
587 public String getName() {
588 return name;
589 }
590
591 public ClassInfo getDeclaringClass() {
592 return declaringClass;
593 }
594
595 public String getReturnType() {
596 return returnType;
597 }
598
599 public String toString() {
600 return declaringClass + "@" + name;
601 }
602 }
603
604 public class FieldInfo extends Annotatable implements Info {
605 private final String name;
606 private final String type;
607 private final ClassInfo declaringClass;
608
609 public FieldInfo(ClassInfo info, Field field){
610 super(field);
611 this.declaringClass = info;
612 this.name = field.getName();
613 this.type = field.getType().getName();
614 }
615
616 public FieldInfo(ClassInfo declaringClass, String name, String type) {
617 this.declaringClass = declaringClass;
618 this.name = name;
619 this.type = type;
620 }
621
622 public String getName() {
623 return name;
624 }
625
626 public ClassInfo getDeclaringClass() {
627 return declaringClass;
628 }
629
630 public String getType() {
631 return type;
632 }
633
634 public String toString() {
635 return declaringClass + "#" + name;
636 }
637 }
638
639 public class AnnotationInfo extends Annotatable implements Info {
640 private final String name;
641
642 public AnnotationInfo(Annotation annotation){
643 this(annotation.getClass().getName());
644 }
645
646 public AnnotationInfo(Class<? extends Annotation> annotation) {
647 this.name = annotation.getName().intern();
648 }
649
650 public AnnotationInfo(String name) {
651 name = name.replaceAll("^L|;$", "");
652 name = name.replace('/', '.');
653 this.name = name.intern();
654 }
655
656 public String getName() {
657 return name;
658 }
659
660 public String toString() {
661 return name.toString();
662 }
663 }
664
665 private List<Info> getAnnotationInfos(String name) {
666 List<Info> infos = annotated.get(name);
667 if (infos == null) {
668 infos = new ArrayList();
669 annotated.put(name, infos);
670 }
671 return infos;
672 }
673
674 private void readClassDef(String className) {
675 if (!className.endsWith(".class")) {
676 className = className.replace('.', '/') + ".class";
677 }
678 ClassReader classReader = null;
679 try {
680 URL resource = classLoader.getResource(className);
681 classReader = new ClassReader(resource.openStream());
682 } catch (IOException e) {
683 e.printStackTrace();
684 }
685
686 // classReader.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), true);
687 classReader.accept(new InfoBuildingVisitor(), true);
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 }