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