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; 019import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH; 020import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR; 021import static java.util.logging.Level.WARNING; 022 023import com.google.common.annotations.VisibleForTesting; 024import com.google.common.base.CharMatcher; 025import com.google.common.base.Splitter; 026import com.google.common.collect.FluentIterable; 027import com.google.common.collect.ImmutableList; 028import com.google.common.collect.ImmutableMap; 029import com.google.common.collect.ImmutableSet; 030import com.google.common.collect.Maps; 031import com.google.common.io.ByteSource; 032import com.google.common.io.CharSource; 033import com.google.common.io.Resources; 034import java.io.File; 035import java.io.IOException; 036import java.net.MalformedURLException; 037import java.net.URISyntaxException; 038import java.net.URL; 039import java.net.URLClassLoader; 040import java.nio.charset.Charset; 041import java.util.Enumeration; 042import java.util.HashSet; 043import java.util.LinkedHashMap; 044import java.util.Map; 045import java.util.NoSuchElementException; 046import java.util.Set; 047import java.util.jar.Attributes; 048import java.util.jar.JarEntry; 049import java.util.jar.JarFile; 050import java.util.jar.Manifest; 051import java.util.logging.Logger; 052import javax.annotation.CheckForNull; 053 054/** 055 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources. 056 * 057 * <h2>Prefer <a href="https://github.com/classgraph/classgraph/wiki">ClassGraph</a> over {@code 058 * ClassPath}</h2> 059 * 060 * <p>We recommend using <a href="https://github.com/classgraph/classgraph/wiki">ClassGraph</a> 061 * instead of {@code ClassPath}. ClassGraph improves upon {@code ClassPath} in several ways, 062 * including addressing many of its limitations. Limitations of {@code ClassPath} include: 063 * 064 * <ul> 065 * <li>It looks only for files and JARs in URLs available from {@link URLClassLoader} instances or 066 * the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. This means it does 067 * not look for classes in the <i>module path</i>. 068 * <li>It understands only {@code file:} URLs. This means that it does not understand <a 069 * href="https://openjdk.java.net/jeps/220">{@code jrt:/} URLs</a>, among <a 070 * href="https://github.com/classgraph/classgraph/wiki/Classpath-specification-mechanisms">others</a>. 071 * <li>It does not know how to look for classes when running under an Android VM. (ClassGraph does 072 * not support this directly, either, but ClassGraph documents how to <a 073 * href="https://github.com/classgraph/classgraph/wiki/Build-Time-Scanning">perform build-time 074 * classpath scanning and make the results available to an Android app</a>.) 075 * <li>Like all of Guava, it is not tested under Windows. We have gotten <a 076 * href="https://github.com/google/guava/issues/2130">a report of a specific bug under 077 * Windows</a>. 078 * <li>It <a href="https://github.com/google/guava/issues/2712">returns only one resource for a 079 * given path</a>, even if resources with that path appear in multiple jars or directories. 080 * <li>It assumes that <a href="https://github.com/google/guava/issues/3349">any class with a 081 * {@code $} in its name is a nested class</a>. 082 * </ul> 083 * 084 * <h2>{@code ClassPath} and symlinks</h2> 085 * 086 * <p>In the case of directory classloaders, symlinks are supported but cycles are not traversed. 087 * This guarantees discovery of each <em>unique</em> loadable resource. However, not all possible 088 * aliases for resources on cyclic paths will be listed. 089 * 090 * @author Ben Yu 091 * @since 14.0 092 */ 093@ElementTypesAreNonnullByDefault 094public final class ClassPath { 095 private static final Logger logger = Logger.getLogger(ClassPath.class.getName()); 096 097 /** Separator for the Class-Path manifest attribute value in jar files. */ 098 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR = 099 Splitter.on(" ").omitEmptyStrings(); 100 101 private static final String CLASS_FILE_NAME_EXTENSION = ".class"; 102 103 private final ImmutableSet<ResourceInfo> resources; 104 105 private ClassPath(ImmutableSet<ResourceInfo> resources) { 106 this.resources = resources; 107 } 108 109 /** 110 * Returns a {@code ClassPath} representing all classes and resources loadable from {@code 111 * classloader} and its ancestor class loaders. 112 * 113 * <p><b>Warning:</b> {@code ClassPath} can find classes and resources only from: 114 * 115 * <ul> 116 * <li>{@link URLClassLoader} instances' {@code file:} URLs 117 * <li>the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. To search the 118 * system class loader even when it is not a {@link URLClassLoader} (as in Java 9), {@code 119 * ClassPath} searches the files from the {@code java.class.path} system property. 120 * </ul> 121 * 122 * @throws IOException if the attempt to read class path resources (jar files or directories) 123 * failed. 124 */ 125 public static ClassPath from(ClassLoader classloader) throws IOException { 126 ImmutableSet<LocationInfo> locations = locationsFrom(classloader); 127 128 // Add all locations to the scanned set so that in a classpath [jar1, jar2], where jar1 has a 129 // manifest with Class-Path pointing to jar2, we won't scan jar2 twice. 130 Set<File> scanned = new HashSet<>(); 131 for (LocationInfo location : locations) { 132 scanned.add(location.file()); 133 } 134 135 // Scan all locations 136 ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder(); 137 for (LocationInfo location : locations) { 138 builder.addAll(location.scanResources(scanned)); 139 } 140 return new ClassPath(builder.build()); 141 } 142 143 /** 144 * Returns all resources loadable from the current class path, including the class files of all 145 * loadable classes but excluding the "META-INF/MANIFEST.MF" file. 146 */ 147 public ImmutableSet<ResourceInfo> getResources() { 148 return resources; 149 } 150 151 /** 152 * Returns all classes loadable from the current class path. 153 * 154 * @since 16.0 155 */ 156 public ImmutableSet<ClassInfo> getAllClasses() { 157 return FluentIterable.from(resources).filter(ClassInfo.class).toSet(); 158 } 159 160 /** 161 * Returns all top level classes loadable from the current class path. Note that "top-level-ness" 162 * is determined heuristically by class name (see {@link ClassInfo#isTopLevel}). 163 */ 164 public ImmutableSet<ClassInfo> getTopLevelClasses() { 165 return FluentIterable.from(resources) 166 .filter(ClassInfo.class) 167 .filter(ClassInfo::isTopLevel) 168 .toSet(); 169 } 170 171 /** Returns all top level classes whose package name is {@code packageName}. */ 172 public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) { 173 checkNotNull(packageName); 174 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 175 for (ClassInfo classInfo : getTopLevelClasses()) { 176 if (classInfo.getPackageName().equals(packageName)) { 177 builder.add(classInfo); 178 } 179 } 180 return builder.build(); 181 } 182 183 /** 184 * Returns all top level classes whose package name is {@code packageName} or starts with {@code 185 * packageName} followed by a '.'. 186 */ 187 public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) { 188 checkNotNull(packageName); 189 String packagePrefix = packageName + '.'; 190 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 191 for (ClassInfo classInfo : getTopLevelClasses()) { 192 if (classInfo.getName().startsWith(packagePrefix)) { 193 builder.add(classInfo); 194 } 195 } 196 return builder.build(); 197 } 198 199 /** 200 * Represents a class path resource that can be either a class file or any other resource file 201 * loadable from the class path. 202 * 203 * @since 14.0 204 */ 205 public static class ResourceInfo { 206 private final File file; 207 private final String resourceName; 208 209 final ClassLoader loader; 210 211 static ResourceInfo of(File file, String resourceName, ClassLoader loader) { 212 if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) { 213 return new ClassInfo(file, resourceName, loader); 214 } else { 215 return new ResourceInfo(file, resourceName, loader); 216 } 217 } 218 219 ResourceInfo(File file, String resourceName, ClassLoader loader) { 220 this.file = checkNotNull(file); 221 this.resourceName = checkNotNull(resourceName); 222 this.loader = checkNotNull(loader); 223 } 224 225 /** 226 * Returns the url identifying the resource. 227 * 228 * <p>See {@link ClassLoader#getResource} 229 * 230 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 231 * despite physically existing in the class path. 232 */ 233 public final URL url() { 234 URL url = loader.getResource(resourceName); 235 if (url == null) { 236 throw new NoSuchElementException(resourceName); 237 } 238 return url; 239 } 240 241 /** 242 * Returns a {@link ByteSource} view of the resource from which its bytes can be read. 243 * 244 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 245 * despite physically existing in the class path. 246 * @since 20.0 247 */ 248 public final ByteSource asByteSource() { 249 return Resources.asByteSource(url()); 250 } 251 252 /** 253 * Returns a {@link CharSource} view of the resource from which its bytes can be read as 254 * characters decoded with the given {@code charset}. 255 * 256 * @throws NoSuchElementException if the resource cannot be loaded through the class loader, 257 * despite physically existing in the class path. 258 * @since 20.0 259 */ 260 public final CharSource asCharSource(Charset charset) { 261 return Resources.asCharSource(url(), charset); 262 } 263 264 /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */ 265 public final String getResourceName() { 266 return resourceName; 267 } 268 269 /** Returns the file that includes this resource. */ 270 final File getFile() { 271 return file; 272 } 273 274 @Override 275 public int hashCode() { 276 return resourceName.hashCode(); 277 } 278 279 @Override 280 public boolean equals(@CheckForNull Object obj) { 281 if (obj instanceof ResourceInfo) { 282 ResourceInfo that = (ResourceInfo) obj; 283 return resourceName.equals(that.resourceName) && loader == that.loader; 284 } 285 return false; 286 } 287 288 // Do not change this arbitrarily. We rely on it for sorting ResourceInfo. 289 @Override 290 public String toString() { 291 return resourceName; 292 } 293 } 294 295 /** 296 * Represents a class that can be loaded through {@link #load}. 297 * 298 * @since 14.0 299 */ 300 public static final class ClassInfo extends ResourceInfo { 301 private final String className; 302 303 ClassInfo(File file, String resourceName, ClassLoader loader) { 304 super(file, resourceName, loader); 305 this.className = getClassName(resourceName); 306 } 307 308 /** 309 * Returns the package name of the class, without attempting to load the class. 310 * 311 * <p>Behaves similarly to {@code class.getPackage().}{@link Package#getName() getName()} but 312 * does not require the class (or package) to be loaded. 313 * 314 * <p>But note that this method may behave differently for a class in the default package: For 315 * such classes, this method always returns an empty string. But under some version of Java, 316 * {@code class.getPackage().getName()} produces a {@code NullPointerException} because {@code 317 * class.getPackage()} returns {@code null}. 318 */ 319 public String getPackageName() { 320 return Reflection.getPackageName(className); 321 } 322 323 /** 324 * Returns the simple name of the underlying class as given in the source code. 325 * 326 * <p>Behaves similarly to {@link Class#getSimpleName()} but does not require the class to be 327 * loaded. 328 * 329 * <p>But note that this class uses heuristics to identify the simple name. See a related 330 * discussion in <a href="https://github.com/google/guava/issues/3349">issue 3349</a>. 331 */ 332 public String getSimpleName() { 333 int lastDollarSign = className.lastIndexOf('$'); 334 if (lastDollarSign != -1) { 335 String innerClassName = className.substring(lastDollarSign + 1); 336 // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are 337 // entirely numeric whereas local classes have the user supplied name as a suffix 338 return CharMatcher.inRange('0', '9').trimLeadingFrom(innerClassName); 339 } 340 String packageName = getPackageName(); 341 if (packageName.isEmpty()) { 342 return className; 343 } 344 345 // Since this is a top level class, its simple name is always the part after package name. 346 return className.substring(packageName.length() + 1); 347 } 348 349 /** 350 * Returns the fully qualified name of the class. 351 * 352 * <p>Behaves identically to {@link Class#getName()} but does not require the class to be 353 * loaded. 354 */ 355 public String getName() { 356 return className; 357 } 358 359 /** 360 * Returns true if the class name "looks to be" top level (not nested), that is, it includes no 361 * '$' in the name. This method may return false for a top-level class that's intentionally 362 * named with the '$' character. If this is a concern, you could use {@link #load} and then 363 * check on the loaded {@link Class} object instead. 364 * 365 * @since 30.1 366 */ 367 public boolean isTopLevel() { 368 return className.indexOf('$') == -1; 369 } 370 371 /** 372 * Loads (but doesn't link or initialize) the class. 373 * 374 * @throws LinkageError when there were errors in loading classes that this class depends on. 375 * For example, {@link NoClassDefFoundError}. 376 */ 377 public Class<?> load() { 378 try { 379 return loader.loadClass(className); 380 } catch (ClassNotFoundException e) { 381 // Shouldn't happen, since the class name is read from the class path. 382 throw new IllegalStateException(e); 383 } 384 } 385 386 @Override 387 public String toString() { 388 return className; 389 } 390 } 391 392 /** 393 * Returns all locations that {@code classloader} and parent loaders load classes and resources 394 * from. Callers can {@linkplain LocationInfo#scanResources scan} individual locations selectively 395 * or even in parallel. 396 */ 397 static ImmutableSet<LocationInfo> locationsFrom(ClassLoader classloader) { 398 ImmutableSet.Builder<LocationInfo> builder = ImmutableSet.builder(); 399 for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) { 400 builder.add(new LocationInfo(entry.getKey(), entry.getValue())); 401 } 402 return builder.build(); 403 } 404 405 /** 406 * Represents a single location (a directory or a jar file) in the class path and is responsible 407 * for scanning resources from this location. 408 */ 409 static final class LocationInfo { 410 final File home; 411 private final ClassLoader classloader; 412 413 LocationInfo(File home, ClassLoader classloader) { 414 this.home = checkNotNull(home); 415 this.classloader = checkNotNull(classloader); 416 } 417 418 /** Returns the file this location is from. */ 419 public final File file() { 420 return home; 421 } 422 423 /** Scans this location and returns all scanned resources. */ 424 public ImmutableSet<ResourceInfo> scanResources() throws IOException { 425 return scanResources(new HashSet<File>()); 426 } 427 428 /** 429 * Scans this location and returns all scanned resources. 430 * 431 * <p>This file and jar files from "Class-Path" entry in the scanned manifest files will be 432 * added to {@code scannedFiles}. 433 * 434 * <p>A file will be scanned at most once even if specified multiple times by one or multiple 435 * jar files' "Class-Path" manifest entries. Particularly, if a jar file from the "Class-Path" 436 * manifest entry is already in {@code scannedFiles}, either because it was scanned earlier, or 437 * it was intentionally added to the set by the caller, it will not be scanned again. 438 * 439 * <p>Note that when you call {@code location.scanResources(scannedFiles)}, the location will 440 * always be scanned even if {@code scannedFiles} already contains it. 441 */ 442 public ImmutableSet<ResourceInfo> scanResources(Set<File> scannedFiles) throws IOException { 443 ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder(); 444 scannedFiles.add(home); 445 scan(home, scannedFiles, builder); 446 return builder.build(); 447 } 448 449 private void scan(File file, Set<File> scannedUris, ImmutableSet.Builder<ResourceInfo> builder) 450 throws IOException { 451 try { 452 if (!file.exists()) { 453 return; 454 } 455 } catch (SecurityException e) { 456 logger.warning("Cannot access " + file + ": " + e); 457 // TODO(emcmanus): consider whether to log other failure cases too. 458 return; 459 } 460 if (file.isDirectory()) { 461 scanDirectory(file, builder); 462 } else { 463 scanJar(file, scannedUris, builder); 464 } 465 } 466 467 private void scanJar( 468 File file, Set<File> scannedUris, ImmutableSet.Builder<ResourceInfo> builder) 469 throws IOException { 470 JarFile jarFile; 471 try { 472 jarFile = new JarFile(file); 473 } catch (IOException e) { 474 // Not a jar file 475 return; 476 } 477 try { 478 for (File path : getClassPathFromManifest(file, jarFile.getManifest())) { 479 // We only scan each file once independent of the classloader that file might be 480 // associated with. 481 if (scannedUris.add(path.getCanonicalFile())) { 482 scan(path, scannedUris, builder); 483 } 484 } 485 scanJarFile(jarFile, builder); 486 } finally { 487 try { 488 jarFile.close(); 489 } catch (IOException ignored) { // similar to try-with-resources, but don't fail scanning 490 } 491 } 492 } 493 494 private void scanJarFile(JarFile file, ImmutableSet.Builder<ResourceInfo> builder) { 495 Enumeration<JarEntry> entries = file.entries(); 496 while (entries.hasMoreElements()) { 497 JarEntry entry = entries.nextElement(); 498 if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) { 499 continue; 500 } 501 builder.add(ResourceInfo.of(new File(file.getName()), entry.getName(), classloader)); 502 } 503 } 504 505 private void scanDirectory(File directory, ImmutableSet.Builder<ResourceInfo> builder) 506 throws IOException { 507 Set<File> currentPath = new HashSet<>(); 508 currentPath.add(directory.getCanonicalFile()); 509 scanDirectory(directory, "", currentPath, builder); 510 } 511 512 /** 513 * Recursively scan the given directory, adding resources for each file encountered. Symlinks 514 * which have already been traversed in the current tree path will be skipped to eliminate 515 * cycles; otherwise symlinks are traversed. 516 * 517 * @param directory the root of the directory to scan 518 * @param packagePrefix resource path prefix inside {@code classloader} for any files found 519 * under {@code directory} 520 * @param currentPath canonical files already visited in the current directory tree path, for 521 * cycle elimination 522 */ 523 private void scanDirectory( 524 File directory, 525 String packagePrefix, 526 Set<File> currentPath, 527 ImmutableSet.Builder<ResourceInfo> builder) 528 throws IOException { 529 File[] files = directory.listFiles(); 530 if (files == null) { 531 logger.warning("Cannot read directory " + directory); 532 // IO error, just skip the directory 533 return; 534 } 535 for (File f : files) { 536 String name = f.getName(); 537 if (f.isDirectory()) { 538 File deref = f.getCanonicalFile(); 539 if (currentPath.add(deref)) { 540 scanDirectory(deref, packagePrefix + name + "/", currentPath, builder); 541 currentPath.remove(deref); 542 } 543 } else { 544 String resourceName = packagePrefix + name; 545 if (!resourceName.equals(JarFile.MANIFEST_NAME)) { 546 builder.add(ResourceInfo.of(f, resourceName, classloader)); 547 } 548 } 549 } 550 } 551 552 @Override 553 public boolean equals(@CheckForNull Object obj) { 554 if (obj instanceof LocationInfo) { 555 LocationInfo that = (LocationInfo) obj; 556 return home.equals(that.home) && classloader.equals(that.classloader); 557 } 558 return false; 559 } 560 561 @Override 562 public int hashCode() { 563 return home.hashCode(); 564 } 565 566 @Override 567 public String toString() { 568 return home.toString(); 569 } 570 } 571 572 /** 573 * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according 574 * to <a 575 * href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR 576 * File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest, and 577 * an empty set will be returned. 578 */ 579 @VisibleForTesting 580 static ImmutableSet<File> getClassPathFromManifest( 581 File jarFile, @CheckForNull Manifest manifest) { 582 if (manifest == null) { 583 return ImmutableSet.of(); 584 } 585 ImmutableSet.Builder<File> builder = ImmutableSet.builder(); 586 String classpathAttribute = 587 manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString()); 588 if (classpathAttribute != null) { 589 for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) { 590 URL url; 591 try { 592 url = getClassPathEntry(jarFile, path); 593 } catch (MalformedURLException e) { 594 // Ignore bad entry 595 logger.warning("Invalid Class-Path entry: " + path); 596 continue; 597 } 598 if (url.getProtocol().equals("file")) { 599 builder.add(toFile(url)); 600 } 601 } 602 } 603 return builder.build(); 604 } 605 606 @VisibleForTesting 607 static ImmutableMap<File, ClassLoader> getClassPathEntries(ClassLoader classloader) { 608 LinkedHashMap<File, ClassLoader> entries = Maps.newLinkedHashMap(); 609 // Search parent first, since it's the order ClassLoader#loadClass() uses. 610 ClassLoader parent = classloader.getParent(); 611 if (parent != null) { 612 entries.putAll(getClassPathEntries(parent)); 613 } 614 for (URL url : getClassLoaderUrls(classloader)) { 615 if (url.getProtocol().equals("file")) { 616 File file = toFile(url); 617 if (!entries.containsKey(file)) { 618 entries.put(file, classloader); 619 } 620 } 621 } 622 return ImmutableMap.copyOf(entries); 623 } 624 625 private static ImmutableList<URL> getClassLoaderUrls(ClassLoader classloader) { 626 if (classloader instanceof URLClassLoader) { 627 return ImmutableList.copyOf(((URLClassLoader) classloader).getURLs()); 628 } 629 if (classloader.equals(ClassLoader.getSystemClassLoader())) { 630 return parseJavaClassPath(); 631 } 632 return ImmutableList.of(); 633 } 634 635 /** 636 * Returns the URLs in the class path specified by the {@code java.class.path} {@linkplain 637 * System#getProperty system property}. 638 */ 639 @VisibleForTesting // TODO(b/65488446): Make this a public API. 640 static ImmutableList<URL> parseJavaClassPath() { 641 ImmutableList.Builder<URL> urls = ImmutableList.builder(); 642 for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) { 643 try { 644 try { 645 urls.add(new File(entry).toURI().toURL()); 646 } catch (SecurityException e) { // File.toURI checks to see if the file is a directory 647 urls.add(new URL("file", null, new File(entry).getAbsolutePath())); 648 } 649 } catch (MalformedURLException e) { 650 logger.log(WARNING, "malformed classpath entry: " + entry, e); 651 } 652 } 653 return urls.build(); 654 } 655 656 /** 657 * Returns the absolute uri of the Class-Path entry value as specified in <a 658 * href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR 659 * File Specification</a>. Even though the specification only talks about relative urls, absolute 660 * urls are actually supported too (for example, in Maven surefire plugin). 661 */ 662 @VisibleForTesting 663 static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException { 664 return new URL(jarFile.toURI().toURL(), path); 665 } 666 667 @VisibleForTesting 668 static String getClassName(String filename) { 669 int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length(); 670 return filename.substring(0, classNameEnd).replace('/', '.'); 671 } 672 673 // TODO(benyu): Try java.nio.file.Paths#get() when Guava drops JDK 6 support. 674 @VisibleForTesting 675 static File toFile(URL url) { 676 checkArgument(url.getProtocol().equals("file")); 677 try { 678 return new File(url.toURI()); // Accepts escaped characters like %20. 679 } catch (URISyntaxException e) { // URL.toURI() doesn't escape chars. 680 return new File(url.getPath()); // Accepts non-escaped chars like space. 681 } 682 } 683}