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