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