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: 606536 $ $Date: 2007-12-23 05:27:15 +0100 (Sun, 23 Dec 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 InputStream in = resource.openStream(); 688 try { 689 ClassReader classReader = new ClassReader(in); 690 classReader.accept(new InfoBuildingVisitor(), true); 691 } finally { 692 in.close(); 693 } 694 } else { 695 new Exception("Could not load " + className).printStackTrace(); 696 } 697 } catch (IOException e) { 698 e.printStackTrace(); 699 } 700 701 } 702 703 public class InfoBuildingVisitor extends EmptyVisitor { 704 private Info info; 705 706 public InfoBuildingVisitor() { 707 } 708 709 public InfoBuildingVisitor(Info info) { 710 this.info = info; 711 } 712 713 public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 714 if (name.endsWith("package-info")) { 715 info = new PackageInfo(javaName(name)); 716 } else { 717 ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName)); 718 719 for (String interfce : interfaces) { 720 classInfo.getInterfaces().add(javaName(interfce)); 721 } 722 info = classInfo; 723 classInfos.add(classInfo); 724 } 725 } 726 727 private String javaName(String name) { 728 return (name == null)? null:name.replace('/', '.'); 729 } 730 731 public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 732 AnnotationInfo annotationInfo = new AnnotationInfo(desc); 733 info.getAnnotations().add(annotationInfo); 734 getAnnotationInfos(annotationInfo.getName()).add(info); 735 return new InfoBuildingVisitor(annotationInfo); 736 } 737 738 public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { 739 ClassInfo classInfo = ((ClassInfo) info); 740 FieldInfo fieldInfo = new FieldInfo(classInfo, name, desc); 741 classInfo.getFields().add(fieldInfo); 742 return new InfoBuildingVisitor(fieldInfo); 743 } 744 745 public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 746 ClassInfo classInfo = ((ClassInfo) info); 747 MethodInfo methodInfo = new MethodInfo(classInfo, name, desc); 748 classInfo.getMethods().add(methodInfo); 749 return new InfoBuildingVisitor(methodInfo); 750 } 751 752 public AnnotationVisitor visitParameterAnnotation(int param, String desc, boolean visible) { 753 MethodInfo methodInfo = ((MethodInfo) info); 754 List<AnnotationInfo> annotationInfos = methodInfo.getParameterAnnotations(param); 755 AnnotationInfo annotationInfo = new AnnotationInfo(desc); 756 annotationInfos.add(annotationInfo); 757 return new InfoBuildingVisitor(annotationInfo); 758 } 759 } 760 }