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