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