001/* 002 * Copyright (C) 2012 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 */ 014 015package com.google.common.reflect; 016 017import static com.google.common.base.Preconditions.checkArgument; 018import static com.google.common.base.Preconditions.checkNotNull; 019 020import com.google.common.annotations.Beta; 021import com.google.common.annotations.VisibleForTesting; 022import com.google.common.base.CharMatcher; 023import com.google.common.base.Predicate; 024import com.google.common.base.Splitter; 025import com.google.common.collect.FluentIterable; 026import com.google.common.collect.ImmutableMap; 027import com.google.common.collect.ImmutableSet; 028import com.google.common.collect.Maps; 029import com.google.common.collect.MultimapBuilder; 030import com.google.common.collect.SetMultimap; 031import com.google.common.collect.Sets; 032import com.google.common.io.ByteSource; 033import com.google.common.io.CharSource; 034import com.google.common.io.Resources; 035import java.io.File; 036import java.io.IOException; 037import java.net.MalformedURLException; 038import java.net.URISyntaxException; 039import java.net.URL; 040import java.net.URLClassLoader; 041import java.nio.charset.Charset; 042import java.util.Enumeration; 043import java.util.HashSet; 044import java.util.LinkedHashMap; 045import java.util.Map; 046import java.util.NoSuchElementException; 047import java.util.Set; 048import java.util.jar.Attributes; 049import java.util.jar.JarEntry; 050import java.util.jar.JarFile; 051import java.util.jar.Manifest; 052import java.util.logging.Logger; 053import javax.annotation.Nullable; 054 055/** 056 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources. 057 * 058 * <p><b>Warning:</b> Currently only {@link URLClassLoader} and only {@code file://} urls are 059 * supported. 060 * 061 * <p>In the case of directory classloaders, symlinks are supported but cycles are not traversed. 062 * This guarantees discovery of each <em>unique</em> loadable resource. However, not all possible 063 * aliases for resources on cyclic paths will be listed. 064 * 065 * @author Ben Yu 066 * @since 14.0 067 */ 068@Beta 069public final class ClassPath { 070 private static final Logger logger = Logger.getLogger(ClassPath.class.getName()); 071 072 private static final Predicate<ClassInfo> IS_TOP_LEVEL = 073 new Predicate<ClassInfo>() { 074 @Override 075 public boolean apply(ClassInfo info) { 076 return info.className.indexOf('$') == -1; 077 } 078 }; 079 080 /** Separator for the Class-Path manifest attribute value in jar files. */ 081 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR = 082 Splitter.on(" ").omitEmptyStrings(); 083 084 private static final String CLASS_FILE_NAME_EXTENSION = ".class"; 085 086 private final ImmutableSet<ResourceInfo> resources; 087 088 private ClassPath(ImmutableSet<ResourceInfo> resources) { 089 this.resources = resources; 090 } 091 092 /** 093 * Returns a {@code ClassPath} representing all classes and resources loadable from {@code 094 * classloader} and its parent class loaders. 095 * 096 * <p><b>Warning:</b> Currently only {@link URLClassLoader} and only {@code file://} urls are 097 * supported. 098 * 099 * @throws IOException if the attempt to read class path resources (jar files or directories) 100 * failed. 101 */ 102 public static ClassPath from(ClassLoader classloader) throws IOException { 103 DefaultScanner scanner = new DefaultScanner(); 104 scanner.scan(classloader); 105 return new ClassPath(scanner.getResources()); 106 } 107 108 /** 109 * Returns all resources loadable from the current class path, including the class files of all 110 * loadable classes but excluding the "META-INF/MANIFEST.MF" file. 111 */ 112 public ImmutableSet<ResourceInfo> getResources() { 113 return resources; 114 } 115 116 /** 117 * Returns all classes loadable from the current class path. 118 * 119 * @since 16.0 120 */ 121 public ImmutableSet<ClassInfo> getAllClasses() { 122 return FluentIterable.from(resources).filter(ClassInfo.class).toSet(); 123 } 124 125 /** Returns all top level classes loadable from the current class path. */ 126 public ImmutableSet<ClassInfo> getTopLevelClasses() { 127 return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet(); 128 } 129 130 /** Returns all top level classes whose package name is {@code packageName}. */ 131 public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) { 132 checkNotNull(packageName); 133 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 134 for (ClassInfo classInfo : getTopLevelClasses()) { 135 if (classInfo.getPackageName().equals(packageName)) { 136 builder.add(classInfo); 137 } 138 } 139 return builder.build(); 140 } 141 142 /** 143 * Returns all top level classes whose package name is {@code packageName} or starts with 144 * {@code packageName} followed by a '.'. 145 */ 146 public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) { 147 checkNotNull(packageName); 148 String packagePrefix = packageName + '.'; 149 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 150 for (ClassInfo classInfo : getTopLevelClasses()) { 151 if (classInfo.getName().startsWith(packagePrefix)) { 152 builder.add(classInfo); 153 } 154 } 155 return builder.build(); 156 } 157 158 /** 159 * Represents a class path resource that can be either a class file or any other resource file 160 * loadable from the class path. 161 * 162 * @since 14.0 163 */ 164 @Beta 165 public static class ResourceInfo { 166 private final String resourceName; 167 168 final ClassLoader loader; 169 170 static ResourceInfo of(String resourceName, ClassLoader loader) { 171 if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) { 172 return new ClassInfo(resourceName, loader); 173 } else { 174 return new ResourceInfo(resourceName, loader); 175 } 176 } 177 178 ResourceInfo(String resourceName, ClassLoader loader) { 179 this.resourceName = checkNotNull(resourceName); 180 this.loader = checkNotNull(loader); 181 } 182 183 /** 184 * Returns the url identifying the resource. 185 * 186 * <p>See {@link ClassLoader#getResource} 187 * 188 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 189 * despite physically existing in the class path. 190 */ 191 public final URL url() { 192 URL url = loader.getResource(resourceName); 193 if (url == null) { 194 throw new NoSuchElementException(resourceName); 195 } 196 return url; 197 } 198 199 /** 200 * Returns a {@link ByteSource} view of the resource from which its bytes can be read. 201 * 202 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 203 * despite physically existing in the class path. 204 * @since 20.0 205 */ 206 public final ByteSource asByteSource() { 207 return Resources.asByteSource(url()); 208 } 209 210 /** 211 * Returns a {@link CharSource} view of the resource from which its bytes can be read as 212 * characters decoded with the given {@code charset}. 213 * 214 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 215 * despite physically existing in the class path. 216 * @since 20.0 217 */ 218 public final CharSource asCharSource(Charset charset) { 219 return Resources.asCharSource(url(), charset); 220 } 221 222 /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */ 223 public final String getResourceName() { 224 return resourceName; 225 } 226 227 @Override 228 public int hashCode() { 229 return resourceName.hashCode(); 230 } 231 232 @Override 233 public boolean equals(Object obj) { 234 if (obj instanceof ResourceInfo) { 235 ResourceInfo that = (ResourceInfo) obj; 236 return resourceName.equals(that.resourceName) && loader == that.loader; 237 } 238 return false; 239 } 240 241 // Do not change this arbitrarily. We rely on it for sorting ResourceInfo. 242 @Override 243 public String toString() { 244 return resourceName; 245 } 246 } 247 248 /** 249 * Represents a class that can be loaded through {@link #load}. 250 * 251 * @since 14.0 252 */ 253 @Beta 254 public static final class ClassInfo extends ResourceInfo { 255 private final String className; 256 257 ClassInfo(String resourceName, ClassLoader loader) { 258 super(resourceName, loader); 259 this.className = getClassName(resourceName); 260 } 261 262 /** 263 * Returns the package name of the class, without attempting to load the class. 264 * 265 * <p>Behaves identically to {@link Package#getName()} but does not require the class (or 266 * package) to be loaded. 267 */ 268 public String getPackageName() { 269 return Reflection.getPackageName(className); 270 } 271 272 /** 273 * Returns the simple name of the underlying class as given in the source code. 274 * 275 * <p>Behaves identically to {@link Class#getSimpleName()} but does not require the class to be 276 * loaded. 277 */ 278 public String getSimpleName() { 279 int lastDollarSign = className.lastIndexOf('$'); 280 if (lastDollarSign != -1) { 281 String innerClassName = className.substring(lastDollarSign + 1); 282 // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are 283 // entirely numeric whereas local classes have the user supplied name as a suffix 284 return CharMatcher.digit().trimLeadingFrom(innerClassName); 285 } 286 String packageName = getPackageName(); 287 if (packageName.isEmpty()) { 288 return className; 289 } 290 291 // Since this is a top level class, its simple name is always the part after package name. 292 return className.substring(packageName.length() + 1); 293 } 294 295 /** 296 * Returns the fully qualified name of the class. 297 * 298 * <p>Behaves identically to {@link Class#getName()} but does not require the class to be 299 * loaded. 300 */ 301 public String getName() { 302 return className; 303 } 304 305 /** 306 * Loads (but doesn't link or initialize) the class. 307 * 308 * @throws LinkageError when there were errors in loading classes that this class depends on. 309 * For example, {@link NoClassDefFoundError}. 310 */ 311 public Class<?> load() { 312 try { 313 return loader.loadClass(className); 314 } catch (ClassNotFoundException e) { 315 // Shouldn't happen, since the class name is read from the class path. 316 throw new IllegalStateException(e); 317 } 318 } 319 320 @Override 321 public String toString() { 322 return className; 323 } 324 } 325 326 /** 327 * Abstract class that scans through the class path represented by a {@link ClassLoader} and calls 328 * {@link #scanDirectory} and {@link #scanJarFile} for directories and jar files on the class path 329 * respectively. 330 */ 331 abstract static class Scanner { 332 333 // We only scan each file once independent of the classloader that resource might be associated 334 // with. 335 private final Set<File> scannedUris = Sets.newHashSet(); 336 337 public final void scan(ClassLoader classloader) throws IOException { 338 for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) { 339 scan(entry.getKey(), entry.getValue()); 340 } 341 } 342 343 /** Called when a directory is scanned for resource files. */ 344 protected abstract void scanDirectory(ClassLoader loader, File directory) throws IOException; 345 346 /** Called when a jar file is scanned for resource entries. */ 347 protected abstract void scanJarFile(ClassLoader loader, JarFile file) throws IOException; 348 349 @VisibleForTesting 350 final void scan(File file, ClassLoader classloader) throws IOException { 351 if (scannedUris.add(file.getCanonicalFile())) { 352 scanFrom(file, classloader); 353 } 354 } 355 356 private void scanFrom(File file, ClassLoader classloader) throws IOException { 357 try { 358 if (!file.exists()) { 359 return; 360 } 361 } catch (SecurityException e) { 362 logger.warning("Cannot access " + file + ": " + e); 363 // TODO(emcmanus): consider whether to log other failure cases too. 364 return; 365 } 366 if (file.isDirectory()) { 367 scanDirectory(classloader, file); 368 } else { 369 scanJar(file, classloader); 370 } 371 } 372 373 private void scanJar(File file, ClassLoader classloader) throws IOException { 374 JarFile jarFile; 375 try { 376 jarFile = new JarFile(file); 377 } catch (IOException e) { 378 // Not a jar file 379 return; 380 } 381 try { 382 for (File path : getClassPathFromManifest(file, jarFile.getManifest())) { 383 scan(path, classloader); 384 } 385 scanJarFile(classloader, jarFile); 386 } finally { 387 try { 388 jarFile.close(); 389 } catch (IOException ignored) { 390 } 391 } 392 } 393 394 /** 395 * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according 396 * to 397 * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes"> 398 * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no 399 * manifest, and an empty set will be returned. 400 */ 401 @VisibleForTesting 402 static ImmutableSet<File> getClassPathFromManifest(File jarFile, @Nullable Manifest manifest) { 403 if (manifest == null) { 404 return ImmutableSet.of(); 405 } 406 ImmutableSet.Builder<File> builder = ImmutableSet.builder(); 407 String classpathAttribute = 408 manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString()); 409 if (classpathAttribute != null) { 410 for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) { 411 URL url; 412 try { 413 url = getClassPathEntry(jarFile, path); 414 } catch (MalformedURLException e) { 415 // Ignore bad entry 416 logger.warning("Invalid Class-Path entry: " + path); 417 continue; 418 } 419 if (url.getProtocol().equals("file")) { 420 builder.add(toFile(url)); 421 } 422 } 423 } 424 return builder.build(); 425 } 426 427 @VisibleForTesting 428 static ImmutableMap<File, ClassLoader> getClassPathEntries(ClassLoader classloader) { 429 LinkedHashMap<File, ClassLoader> entries = Maps.newLinkedHashMap(); 430 // Search parent first, since it's the order ClassLoader#loadClass() uses. 431 ClassLoader parent = classloader.getParent(); 432 if (parent != null) { 433 entries.putAll(getClassPathEntries(parent)); 434 } 435 if (classloader instanceof URLClassLoader) { 436 URLClassLoader urlClassLoader = (URLClassLoader) classloader; 437 for (URL entry : urlClassLoader.getURLs()) { 438 if (entry.getProtocol().equals("file")) { 439 File file = toFile(entry); 440 if (!entries.containsKey(file)) { 441 entries.put(file, classloader); 442 } 443 } 444 } 445 } 446 return ImmutableMap.copyOf(entries); 447 } 448 449 /** 450 * Returns the absolute uri of the Class-Path entry value as specified in 451 * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes"> 452 * JAR File Specification</a>. Even though the specification only talks about relative urls, 453 * absolute urls are actually supported too (for example, in Maven surefire plugin). 454 */ 455 @VisibleForTesting 456 static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException { 457 return new URL(jarFile.toURI().toURL(), path); 458 } 459 } 460 461 @VisibleForTesting 462 static final class DefaultScanner extends Scanner { 463 private final SetMultimap<ClassLoader, String> resources = 464 MultimapBuilder.hashKeys().linkedHashSetValues().build(); 465 466 ImmutableSet<ResourceInfo> getResources() { 467 ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder(); 468 for (Map.Entry<ClassLoader, String> entry : resources.entries()) { 469 builder.add(ResourceInfo.of(entry.getValue(), entry.getKey())); 470 } 471 return builder.build(); 472 } 473 474 @Override 475 protected void scanJarFile(ClassLoader classloader, JarFile file) { 476 Enumeration<JarEntry> entries = file.entries(); 477 while (entries.hasMoreElements()) { 478 JarEntry entry = entries.nextElement(); 479 if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) { 480 continue; 481 } 482 resources.get(classloader).add(entry.getName()); 483 } 484 } 485 486 @Override 487 protected void scanDirectory(ClassLoader classloader, File directory) throws IOException { 488 Set<File> currentPath = new HashSet<File>(); 489 currentPath.add(directory.getCanonicalFile()); 490 scanDirectory(directory, classloader, "", currentPath); 491 } 492 493 /** 494 * Recursively scan the given directory, adding resources for each file encountered. Symlinks 495 * which have already been traversed in the current tree path will be skipped to eliminate 496 * cycles; otherwise symlinks are traversed. 497 * 498 * @param directory the root of the directory to scan 499 * @param classloader the classloader that includes resources found in {@code directory} 500 * @param packagePrefix resource path prefix inside {@code classloader} for any files found 501 * under {@code directory} 502 * @param currentPath canonical files already visited in the current directory tree path, for 503 * cycle elimination 504 */ 505 private void scanDirectory( 506 File directory, ClassLoader classloader, String packagePrefix, Set<File> currentPath) 507 throws IOException { 508 File[] files = directory.listFiles(); 509 if (files == null) { 510 logger.warning("Cannot read directory " + directory); 511 // IO error, just skip the directory 512 return; 513 } 514 for (File f : files) { 515 String name = f.getName(); 516 if (f.isDirectory()) { 517 File deref = f.getCanonicalFile(); 518 if (currentPath.add(deref)) { 519 scanDirectory(deref, classloader, packagePrefix + name + "/", currentPath); 520 currentPath.remove(deref); 521 } 522 } else { 523 String resourceName = packagePrefix + name; 524 if (!resourceName.equals(JarFile.MANIFEST_NAME)) { 525 resources.get(classloader).add(resourceName); 526 } 527 } 528 } 529 } 530 } 531 532 @VisibleForTesting 533 static String getClassName(String filename) { 534 int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length(); 535 return filename.substring(0, classNameEnd).replace('/', '.'); 536 } 537 538 // TODO(benyu): Try java.nio.file.Paths#get() when Guava drops JDK 6 support. 539 @VisibleForTesting 540 static File toFile(URL url) { 541 checkArgument(url.getProtocol().equals("file")); 542 try { 543 return new File(url.toURI()); // Accepts escaped characters like %20. 544 } catch (URISyntaxException e) { // URL.toURI() doesn't escape chars. 545 return new File(url.getPath()); // Accepts non-escaped chars like space. 546 } 547 } 548}