Coverage Summary for Class: ClassPath (com.google.common.reflect)

Class Method, % Line, %
ClassPath 70.6% (12/17) 68.8% (64/93)
ClassPath$1 100% (2/2) 100% (2/2)
ClassPath$ClassInfo 57.1% (4/7) 33.3% (6/18)
ClassPath$LocationInfo 83.3% (10/12) 75.4% (52/69)
ClassPath$ResourceInfo 40% (4/10) 54.5% (12/22)
Total 66.7% (32/48) 66.7% (136/204)


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