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