View Javadoc
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.collect.MultimapBuilder;
34  import com.google.common.collect.SetMultimap;
35  import com.google.common.collect.Sets;
36  import com.google.common.io.ByteSource;
37  import com.google.common.io.CharSource;
38  import com.google.common.io.Resources;
39  import java.io.File;
40  import java.io.IOException;
41  import java.net.MalformedURLException;
42  import java.net.URISyntaxException;
43  import java.net.URL;
44  import java.net.URLClassLoader;
45  import java.nio.charset.Charset;
46  import java.util.Enumeration;
47  import java.util.HashSet;
48  import java.util.LinkedHashMap;
49  import java.util.Map;
50  import java.util.NoSuchElementException;
51  import java.util.Set;
52  import java.util.jar.Attributes;
53  import java.util.jar.JarEntry;
54  import java.util.jar.JarFile;
55  import java.util.jar.Manifest;
56  import java.util.logging.Logger;
57  import javax.annotation.Nullable;
58  
59  /**
60   * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources.
61   *
62   * <p><b>Warning:</b> Current limitations:
63   *
64   * <ul>
65   *   <li>Looks only for files and JARs in URLs available from {@link URLClassLoader} instances or
66   *       the {@linkplain ClassLoader#getSystemClassLoader() system class loader}.
67   *   <li>Only understands {@code file:} URLs.
68   * </ul>
69   *
70   * <p>In the case of directory classloaders, symlinks are supported but cycles are not traversed.
71   * This guarantees discovery of each <em>unique</em> loadable resource. However, not all possible
72   * aliases for resources on cyclic paths will be listed.
73   *
74   * @author Ben Yu
75   * @since 14.0
76   */
77  @Beta
78  public final class ClassPath {
79    private static final Logger logger = Logger.getLogger(ClassPath.class.getName());
80  
81    private static final Predicate<ClassInfo> IS_TOP_LEVEL =
82        new Predicate<ClassInfo>() {
83          @Override
84          public boolean apply(ClassInfo info) {
85            return info.className.indexOf('$') == -1;
86          }
87        };
88  
89    /** Separator for the Class-Path manifest attribute value in jar files. */
90    private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
91        Splitter.on(" ").omitEmptyStrings();
92  
93    private static final String CLASS_FILE_NAME_EXTENSION = ".class";
94  
95    private final ImmutableSet<ResourceInfo> resources;
96  
97    private ClassPath(ImmutableSet<ResourceInfo> resources) {
98      this.resources = resources;
99    }
100 
101   /**
102    * Returns a {@code ClassPath} representing all classes and resources loadable from {@code
103    * classloader} and its ancestor class loaders.
104    *
105    * <p><b>Warning:</b> {@code ClassPath} can find classes and resources only from:
106    *
107    * <ul>
108    *   <li>{@link URLClassLoader} instances' {@code file:} URLs
109    *   <li>the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. To search the
110    *       system class loader even when it is not a {@link URLClassLoader} (as in Java 9), {@code
111    *       ClassPath} searches the files from the {@code java.class.path} system property.
112    * </ul>
113    *
114    * @throws IOException if the attempt to read class path resources (jar files or directories)
115    *     failed.
116    */
117   public static ClassPath from(ClassLoader classloader) throws IOException {
118     DefaultScanner scanner = new DefaultScanner();
119     scanner.scan(classloader);
120     return new ClassPath(scanner.getResources());
121   }
122 
123   /**
124    * Returns all resources loadable from the current class path, including the class files of all
125    * loadable classes but excluding the "META-INF/MANIFEST.MF" file.
126    */
127   public ImmutableSet<ResourceInfo> getResources() {
128     return resources;
129   }
130 
131   /**
132    * Returns all classes loadable from the current class path.
133    *
134    * @since 16.0
135    */
136   public ImmutableSet<ClassInfo> getAllClasses() {
137     return FluentIterable.from(resources).filter(ClassInfo.class).toSet();
138   }
139 
140   /** Returns all top level classes loadable from the current class path. */
141   public ImmutableSet<ClassInfo> getTopLevelClasses() {
142     return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet();
143   }
144 
145   /** Returns all top level classes whose package name is {@code packageName}. */
146   public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
147     checkNotNull(packageName);
148     ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
149     for (ClassInfo classInfo : getTopLevelClasses()) {
150       if (classInfo.getPackageName().equals(packageName)) {
151         builder.add(classInfo);
152       }
153     }
154     return builder.build();
155   }
156 
157   /**
158    * Returns all top level classes whose package name is {@code packageName} or starts with
159    * {@code packageName} followed by a '.'.
160    */
161   public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
162     checkNotNull(packageName);
163     String packagePrefix = packageName + '.';
164     ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
165     for (ClassInfo classInfo : getTopLevelClasses()) {
166       if (classInfo.getName().startsWith(packagePrefix)) {
167         builder.add(classInfo);
168       }
169     }
170     return builder.build();
171   }
172 
173   /**
174    * Represents a class path resource that can be either a class file or any other resource file
175    * loadable from the class path.
176    *
177    * @since 14.0
178    */
179   @Beta
180   public static class ResourceInfo {
181     private final String resourceName;
182 
183     final ClassLoader loader;
184 
185     static ResourceInfo of(String resourceName, ClassLoader loader) {
186       if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) {
187         return new ClassInfo(resourceName, loader);
188       } else {
189         return new ResourceInfo(resourceName, loader);
190       }
191     }
192 
193     ResourceInfo(String resourceName, ClassLoader loader) {
194       this.resourceName = checkNotNull(resourceName);
195       this.loader = checkNotNull(loader);
196     }
197 
198     /**
199      * Returns the url identifying the resource.
200      *
201      * <p>See {@link ClassLoader#getResource}
202      *
203      * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
204      *     despite physically existing in the class path.
205      */
206     public final URL url() {
207       URL url = loader.getResource(resourceName);
208       if (url == null) {
209         throw new NoSuchElementException(resourceName);
210       }
211       return url;
212     }
213 
214     /**
215      * Returns a {@link ByteSource} view of the resource from which its bytes can be read.
216      *
217      * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
218      *     despite physically existing in the class path.
219      * @since 20.0
220      */
221     public final ByteSource asByteSource() {
222       return Resources.asByteSource(url());
223     }
224 
225     /**
226      * Returns a {@link CharSource} view of the resource from which its bytes can be read as
227      * characters decoded with the given {@code charset}.
228      *
229      * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
230      *     despite physically existing in the class path.
231      * @since 20.0
232      */
233     public final CharSource asCharSource(Charset charset) {
234       return Resources.asCharSource(url(), charset);
235     }
236 
237     /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
238     public final String getResourceName() {
239       return resourceName;
240     }
241 
242     @Override
243     public int hashCode() {
244       return resourceName.hashCode();
245     }
246 
247     @Override
248     public boolean equals(Object obj) {
249       if (obj instanceof ResourceInfo) {
250         ResourceInfo that = (ResourceInfo) obj;
251         return resourceName.equals(that.resourceName) && loader == that.loader;
252       }
253       return false;
254     }
255 
256     // Do not change this arbitrarily. We rely on it for sorting ResourceInfo.
257     @Override
258     public String toString() {
259       return resourceName;
260     }
261   }
262 
263   /**
264    * Represents a class that can be loaded through {@link #load}.
265    *
266    * @since 14.0
267    */
268   @Beta
269   public static final class ClassInfo extends ResourceInfo {
270     private final String className;
271 
272     ClassInfo(String resourceName, ClassLoader loader) {
273       super(resourceName, loader);
274       this.className = getClassName(resourceName);
275     }
276 
277     /**
278      * Returns the package name of the class, without attempting to load the class.
279      *
280      * <p>Behaves identically to {@link Package#getName()} but does not require the class (or
281      * package) to be loaded.
282      */
283     public String getPackageName() {
284       return Reflection.getPackageName(className);
285     }
286 
287     /**
288      * Returns the simple name of the underlying class as given in the source code.
289      *
290      * <p>Behaves identically to {@link Class#getSimpleName()} but does not require the class to be
291      * loaded.
292      */
293     public String getSimpleName() {
294       int lastDollarSign = className.lastIndexOf('$');
295       if (lastDollarSign != -1) {
296         String innerClassName = className.substring(lastDollarSign + 1);
297         // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are
298         // entirely numeric whereas local classes have the user supplied name as a suffix
299         return CharMatcher.digit().trimLeadingFrom(innerClassName);
300       }
301       String packageName = getPackageName();
302       if (packageName.isEmpty()) {
303         return className;
304       }
305 
306       // Since this is a top level class, its simple name is always the part after package name.
307       return className.substring(packageName.length() + 1);
308     }
309 
310     /**
311      * Returns the fully qualified name of the class.
312      *
313      * <p>Behaves identically to {@link Class#getName()} but does not require the class to be
314      * loaded.
315      */
316     public String getName() {
317       return className;
318     }
319 
320     /**
321      * Loads (but doesn't link or initialize) the class.
322      *
323      * @throws LinkageError when there were errors in loading classes that this class depends on.
324      *     For example, {@link NoClassDefFoundError}.
325      */
326     public Class<?> load() {
327       try {
328         return loader.loadClass(className);
329       } catch (ClassNotFoundException e) {
330         // Shouldn't happen, since the class name is read from the class path.
331         throw new IllegalStateException(e);
332       }
333     }
334 
335     @Override
336     public String toString() {
337       return className;
338     }
339   }
340 
341   /**
342    * Abstract class that scans through the class path represented by a {@link ClassLoader} and calls
343    * {@link #scanDirectory} and {@link #scanJarFile} for directories and jar files on the class path
344    * respectively.
345    */
346   abstract static class Scanner {
347 
348     // We only scan each file once independent of the classloader that resource might be associated
349     // with.
350     private final Set<File> scannedUris = Sets.newHashSet();
351 
352     public final void scan(ClassLoader classloader) throws IOException {
353       for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
354         scan(entry.getKey(), entry.getValue());
355       }
356     }
357 
358     /** Called when a directory is scanned for resource files. */
359     protected abstract void scanDirectory(ClassLoader loader, File directory) throws IOException;
360 
361     /** Called when a jar file is scanned for resource entries. */
362     protected abstract void scanJarFile(ClassLoader loader, JarFile file) throws IOException;
363 
364     @VisibleForTesting
365     final void scan(File file, ClassLoader classloader) throws IOException {
366       if (scannedUris.add(file.getCanonicalFile())) {
367         scanFrom(file, classloader);
368       }
369     }
370 
371     private void scanFrom(File file, ClassLoader classloader) throws IOException {
372       try {
373         if (!file.exists()) {
374           return;
375         }
376       } catch (SecurityException e) {
377         logger.warning("Cannot access " + file + ": " + e);
378         // TODO(emcmanus): consider whether to log other failure cases too.
379         return;
380       }
381       if (file.isDirectory()) {
382         scanDirectory(classloader, file);
383       } else {
384         scanJar(file, classloader);
385       }
386     }
387 
388     private void scanJar(File file, ClassLoader classloader) throws IOException {
389       JarFile jarFile;
390       try {
391         jarFile = new JarFile(file);
392       } catch (IOException e) {
393         // Not a jar file
394         return;
395       }
396       try {
397         for (File path : getClassPathFromManifest(file, jarFile.getManifest())) {
398           scan(path, classloader);
399         }
400         scanJarFile(classloader, jarFile);
401       } finally {
402         try {
403           jarFile.close();
404         } catch (IOException ignored) {
405         }
406       }
407     }
408 
409     /**
410      * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
411      * to
412      * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">
413      * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no
414      * manifest, and an empty set will be returned.
415      */
416     @VisibleForTesting
417     static ImmutableSet<File> getClassPathFromManifest(File jarFile, @Nullable Manifest manifest) {
418       if (manifest == null) {
419         return ImmutableSet.of();
420       }
421       ImmutableSet.Builder<File> builder = ImmutableSet.builder();
422       String classpathAttribute =
423           manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString());
424       if (classpathAttribute != null) {
425         for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
426           URL url;
427           try {
428             url = getClassPathEntry(jarFile, path);
429           } catch (MalformedURLException e) {
430             // Ignore bad entry
431             logger.warning("Invalid Class-Path entry: " + path);
432             continue;
433           }
434           if (url.getProtocol().equals("file")) {
435             builder.add(toFile(url));
436           }
437         }
438       }
439       return builder.build();
440     }
441 
442     @VisibleForTesting
443     static ImmutableMap<File, ClassLoader> getClassPathEntries(ClassLoader classloader) {
444       LinkedHashMap<File, ClassLoader> entries = Maps.newLinkedHashMap();
445       // Search parent first, since it's the order ClassLoader#loadClass() uses.
446       ClassLoader parent = classloader.getParent();
447       if (parent != null) {
448         entries.putAll(getClassPathEntries(parent));
449       }
450       for (URL url : getClassLoaderUrls(classloader)) {
451         if (url.getProtocol().equals("file")) {
452           File file = toFile(url);
453           if (!entries.containsKey(file)) {
454             entries.put(file, classloader);
455           }
456         }
457       }
458       return ImmutableMap.copyOf(entries);
459     }
460 
461     private static ImmutableList<URL> getClassLoaderUrls(ClassLoader classloader) {
462       if (classloader instanceof URLClassLoader) {
463         return ImmutableList.copyOf(((URLClassLoader) classloader).getURLs());
464       }
465       if (classloader.equals(ClassLoader.getSystemClassLoader())) {
466         return parseJavaClassPath();
467       }
468       return ImmutableList.of();
469     }
470 
471     /**
472      * Returns the URLs in the class path specified by the {@code java.class.path} {@linkplain
473      * System#getProperty system property}.
474      */
475     @VisibleForTesting // TODO(b/65488446): Make this a public API.
476     static ImmutableList<URL> parseJavaClassPath() {
477       ImmutableList.Builder<URL> urls = ImmutableList.builder();
478       for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
479         try {
480           try {
481             urls.add(new File(entry).toURI().toURL());
482           } catch (SecurityException e) { // File.toURI checks to see if the file is a directory
483             urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
484           }
485         } catch (MalformedURLException e) {
486           logger.log(WARNING, "malformed classpath entry: " + entry, e);
487         }
488       }
489       return urls.build();
490     }
491 
492     /**
493      * Returns the absolute uri of the Class-Path entry value as specified in
494      * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">
495      * JAR File Specification</a>. Even though the specification only talks about relative urls,
496      * absolute urls are actually supported too (for example, in Maven surefire plugin).
497      */
498     @VisibleForTesting
499     static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException {
500       return new URL(jarFile.toURI().toURL(), path);
501     }
502   }
503 
504   @VisibleForTesting
505   static final class DefaultScanner extends Scanner {
506     private final SetMultimap<ClassLoader, String> resources =
507         MultimapBuilder.hashKeys().linkedHashSetValues().build();
508 
509     ImmutableSet<ResourceInfo> getResources() {
510       ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder();
511       for (Map.Entry<ClassLoader, String> entry : resources.entries()) {
512         builder.add(ResourceInfo.of(entry.getValue(), entry.getKey()));
513       }
514       return builder.build();
515     }
516 
517     @Override
518     protected void scanJarFile(ClassLoader classloader, JarFile file) {
519       Enumeration<JarEntry> entries = file.entries();
520       while (entries.hasMoreElements()) {
521         JarEntry entry = entries.nextElement();
522         if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
523           continue;
524         }
525         resources.get(classloader).add(entry.getName());
526       }
527     }
528 
529     @Override
530     protected void scanDirectory(ClassLoader classloader, File directory) throws IOException {
531       Set<File> currentPath = new HashSet<>();
532       currentPath.add(directory.getCanonicalFile());
533       scanDirectory(directory, classloader, "", currentPath);
534     }
535 
536     /**
537      * Recursively scan the given directory, adding resources for each file encountered. Symlinks
538      * which have already been traversed in the current tree path will be skipped to eliminate
539      * cycles; otherwise symlinks are traversed.
540      *
541      * @param directory the root of the directory to scan
542      * @param classloader the classloader that includes resources found in {@code directory}
543      * @param packagePrefix resource path prefix inside {@code classloader} for any files found
544      *     under {@code directory}
545      * @param currentPath canonical files already visited in the current directory tree path, for
546      *     cycle elimination
547      */
548     private void scanDirectory(
549         File directory, ClassLoader classloader, String packagePrefix, Set<File> currentPath)
550         throws IOException {
551       File[] files = directory.listFiles();
552       if (files == null) {
553         logger.warning("Cannot read directory " + directory);
554         // IO error, just skip the directory
555         return;
556       }
557       for (File f : files) {
558         String name = f.getName();
559         if (f.isDirectory()) {
560           File deref = f.getCanonicalFile();
561           if (currentPath.add(deref)) {
562             scanDirectory(deref, classloader, packagePrefix + name + "/", currentPath);
563             currentPath.remove(deref);
564           }
565         } else {
566           String resourceName = packagePrefix + name;
567           if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
568             resources.get(classloader).add(resourceName);
569           }
570         }
571       }
572     }
573   }
574 
575   @VisibleForTesting
576   static String getClassName(String filename) {
577     int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
578     return filename.substring(0, classNameEnd).replace('/', '.');
579   }
580 
581   // TODO(benyu): Try java.nio.file.Paths#get() when Guava drops JDK 6 support.
582   @VisibleForTesting
583   static File toFile(URL url) {
584     checkArgument(url.getProtocol().equals("file"));
585     try {
586       return new File(url.toURI());  // Accepts escaped characters like %20.
587     } catch (URISyntaxException e) {  // URL.toURI() doesn't escape chars.
588       return new File(url.getPath());  // Accepts non-escaped chars like space.
589     }
590   }
591 }