Home | History | Annotate | Download | only in reflect
      1 /*
      2  * Copyright (C) 2012 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.reflect;
     18 
     19 import static com.google.common.base.Preconditions.checkNotNull;
     20 
     21 import com.google.common.annotations.Beta;
     22 import com.google.common.annotations.VisibleForTesting;
     23 import com.google.common.base.CharMatcher;
     24 import com.google.common.base.Predicate;
     25 import com.google.common.base.Splitter;
     26 import com.google.common.collect.FluentIterable;
     27 import com.google.common.collect.ImmutableMap;
     28 import com.google.common.collect.ImmutableSet;
     29 import com.google.common.collect.ImmutableSortedSet;
     30 import com.google.common.collect.Maps;
     31 import com.google.common.collect.Ordering;
     32 import com.google.common.collect.Sets;
     33 
     34 import java.io.File;
     35 import java.io.IOException;
     36 import java.net.URI;
     37 import java.net.URISyntaxException;
     38 import java.net.URL;
     39 import java.net.URLClassLoader;
     40 import java.util.Enumeration;
     41 import java.util.LinkedHashMap;
     42 import java.util.Map;
     43 import java.util.Set;
     44 import java.util.jar.Attributes;
     45 import java.util.jar.JarEntry;
     46 import java.util.jar.JarFile;
     47 import java.util.jar.Manifest;
     48 import java.util.logging.Logger;
     49 
     50 import javax.annotation.Nullable;
     51 
     52 /**
     53  * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources.
     54  *
     55  * @author Ben Yu
     56  * @since 14.0
     57  */
     58 @Beta
     59 public final class ClassPath {
     60   private static final Logger logger = Logger.getLogger(ClassPath.class.getName());
     61 
     62   private static final Predicate<ClassInfo> IS_TOP_LEVEL = new Predicate<ClassInfo>() {
     63     @Override public boolean apply(ClassInfo info) {
     64       return info.className.indexOf('$') == -1;
     65     }
     66   };
     67 
     68   /** Separator for the Class-Path manifest attribute value in jar files. */
     69   private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
     70       Splitter.on(" ").omitEmptyStrings();
     71 
     72   private static final String CLASS_FILE_NAME_EXTENSION = ".class";
     73 
     74   private final ImmutableSet<ResourceInfo> resources;
     75 
     76   private ClassPath(ImmutableSet<ResourceInfo> resources) {
     77     this.resources = resources;
     78   }
     79 
     80   /**
     81    * Returns a {@code ClassPath} representing all classes and resources loadable from {@code
     82    * classloader} and its parent class loaders.
     83    *
     84    * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported.
     85    *
     86    * @throws IOException if the attempt to read class path resources (jar files or directories)
     87    *         failed.
     88    */
     89   public static ClassPath from(ClassLoader classloader) throws IOException {
     90     Scanner scanner = new Scanner();
     91     for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
     92       scanner.scan(entry.getKey(), entry.getValue());
     93     }
     94     return new ClassPath(scanner.getResources());
     95   }
     96 
     97   /**
     98    * Returns all resources loadable from the current class path, including the class files of all
     99    * 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     final ClassLoader loader;
    157 
    158     static ResourceInfo of(String resourceName, ClassLoader loader) {
    159       if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) {
    160         return new ClassInfo(resourceName, loader);
    161       } else {
    162         return new ResourceInfo(resourceName, loader);
    163       }
    164     }
    165 
    166     ResourceInfo(String resourceName, ClassLoader loader) {
    167       this.resourceName = checkNotNull(resourceName);
    168       this.loader = checkNotNull(loader);
    169     }
    170 
    171     /** Returns the url identifying the resource. */
    172     public final URL url() {
    173       return checkNotNull(loader.getResource(resourceName),
    174           "Failed to load resource: %s", resourceName);
    175     }
    176 
    177     /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
    178     public final String getResourceName() {
    179       return resourceName;
    180     }
    181 
    182     @Override public int hashCode() {
    183       return resourceName.hashCode();
    184     }
    185 
    186     @Override public boolean equals(Object obj) {
    187       if (obj instanceof ResourceInfo) {
    188         ResourceInfo that = (ResourceInfo) obj;
    189         return resourceName.equals(that.resourceName)
    190             && loader == that.loader;
    191       }
    192       return false;
    193     }
    194 
    195     // Do not change this arbitrarily. We rely on it for sorting ResourceInfo.
    196     @Override public String toString() {
    197       return resourceName;
    198     }
    199   }
    200 
    201   /**
    202    * Represents a class that can be loaded through {@link #load}.
    203    *
    204    * @since 14.0
    205    */
    206   @Beta
    207   public static final class ClassInfo extends ResourceInfo {
    208     private final String className;
    209 
    210     ClassInfo(String resourceName, ClassLoader loader) {
    211       super(resourceName, loader);
    212       this.className = getClassName(resourceName);
    213     }
    214 
    215     /**
    216      * Returns the package name of the class, without attempting to load the class.
    217      *
    218      * <p>Behaves identically to {@link Package#getName()} but does not require the class (or
    219      * package) to be loaded.
    220      */
    221     public String getPackageName() {
    222       return Reflection.getPackageName(className);
    223     }
    224 
    225     /**
    226      * Returns the simple name of the underlying class as given in the source code.
    227      *
    228      * <p>Behaves identically to {@link Class#getSimpleName()} but does not require the class to be
    229      * loaded.
    230      */
    231     public String getSimpleName() {
    232       int lastDollarSign = className.lastIndexOf('$');
    233       if (lastDollarSign != -1) {
    234         String innerClassName = className.substring(lastDollarSign + 1);
    235         // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are
    236         // entirely numeric whereas local classes have the user supplied name as a suffix
    237         return CharMatcher.DIGIT.trimLeadingFrom(innerClassName);
    238       }
    239       String packageName = getPackageName();
    240       if (packageName.isEmpty()) {
    241         return className;
    242       }
    243 
    244       // Since this is a top level class, its simple name is always the part after package name.
    245       return className.substring(packageName.length() + 1);
    246     }
    247 
    248     /**
    249      * Returns the fully qualified name of the class.
    250      *
    251      * <p>Behaves identically to {@link Class#getName()} but does not require the class to be
    252      * loaded.
    253      */
    254     public String getName() {
    255       return className;
    256     }
    257 
    258     /**
    259      * Loads (but doesn't link or initialize) the class.
    260      *
    261      * @throws LinkageError when there were errors in loading classes that this class depends on.
    262      *         For example, {@link NoClassDefFoundError}.
    263      */
    264     public Class<?> load() {
    265       try {
    266         return loader.loadClass(className);
    267       } catch (ClassNotFoundException e) {
    268         // Shouldn't happen, since the class name is read from the class path.
    269         throw new IllegalStateException(e);
    270       }
    271     }
    272 
    273     @Override public String toString() {
    274       return className;
    275     }
    276   }
    277 
    278   @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries(
    279       ClassLoader classloader) {
    280     LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap();
    281     // Search parent first, since it's the order ClassLoader#loadClass() uses.
    282     ClassLoader parent = classloader.getParent();
    283     if (parent != null) {
    284       entries.putAll(getClassPathEntries(parent));
    285     }
    286     if (classloader instanceof URLClassLoader) {
    287       URLClassLoader urlClassLoader = (URLClassLoader) classloader;
    288       for (URL entry : urlClassLoader.getURLs()) {
    289         URI uri;
    290         try {
    291           uri = entry.toURI();
    292         } catch (URISyntaxException e) {
    293           throw new IllegalArgumentException(e);
    294         }
    295         if (!entries.containsKey(uri)) {
    296           entries.put(uri, classloader);
    297         }
    298       }
    299     }
    300     return ImmutableMap.copyOf(entries);
    301   }
    302 
    303   @VisibleForTesting static final class Scanner {
    304 
    305     private final ImmutableSortedSet.Builder<ResourceInfo> resources =
    306         new ImmutableSortedSet.Builder<ResourceInfo>(Ordering.usingToString());
    307     private final Set<URI> scannedUris = Sets.newHashSet();
    308 
    309     ImmutableSortedSet<ResourceInfo> getResources() {
    310       return resources.build();
    311     }
    312 
    313     void scan(URI uri, ClassLoader classloader) throws IOException {
    314       if (uri.getScheme().equals("file") && scannedUris.add(uri)) {
    315         scanFrom(new File(uri), classloader);
    316       }
    317     }
    318 
    319     @VisibleForTesting void scanFrom(File file, ClassLoader classloader)
    320         throws IOException {
    321       if (!file.exists()) {
    322         return;
    323       }
    324       if (file.isDirectory()) {
    325         scanDirectory(file, classloader);
    326       } else {
    327         scanJar(file, classloader);
    328       }
    329     }
    330 
    331     private void scanDirectory(File directory, ClassLoader classloader) throws IOException {
    332       scanDirectory(directory, classloader, "", ImmutableSet.<File>of());
    333     }
    334 
    335     private void scanDirectory(
    336         File directory, ClassLoader classloader, String packagePrefix,
    337         ImmutableSet<File> ancestors) throws IOException {
    338       File canonical = directory.getCanonicalFile();
    339       if (ancestors.contains(canonical)) {
    340         // A cycle in the filesystem, for example due to a symbolic link.
    341         return;
    342       }
    343       File[] files = directory.listFiles();
    344       if (files == null) {
    345         logger.warning("Cannot read directory " + directory);
    346         // IO error, just skip the directory
    347         return;
    348       }
    349       ImmutableSet<File> newAncestors = ImmutableSet.<File>builder()
    350           .addAll(ancestors)
    351           .add(canonical)
    352           .build();
    353       for (File f : files) {
    354         String name = f.getName();
    355         if (f.isDirectory()) {
    356           scanDirectory(f, classloader, packagePrefix + name + "/", newAncestors);
    357         } else {
    358           String resourceName = packagePrefix + name;
    359           if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
    360             resources.add(ResourceInfo.of(resourceName, classloader));
    361           }
    362         }
    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 (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) {
    376           scan(uri, classloader);
    377         }
    378         Enumeration<JarEntry> entries = jarFile.entries();
    379         while (entries.hasMoreElements()) {
    380           JarEntry entry = entries.nextElement();
    381           if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
    382             continue;
    383           }
    384           resources.add(ResourceInfo.of(entry.getName(), classloader));
    385         }
    386       } finally {
    387         try {
    388           jarFile.close();
    389         } catch (IOException ignored) {}
    390       }
    391     }
    392 
    393     /**
    394      * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
    395      * to <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes">
    396      * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no
    397      * manifest, and an empty set will be returned.
    398      */
    399     @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest(
    400         File jarFile, @Nullable Manifest manifest) {
    401       if (manifest == null) {
    402         return ImmutableSet.of();
    403       }
    404       ImmutableSet.Builder<URI> builder = ImmutableSet.builder();
    405       String classpathAttribute = manifest.getMainAttributes()
    406           .getValue(Attributes.Name.CLASS_PATH.toString());
    407       if (classpathAttribute != null) {
    408         for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
    409           URI uri;
    410           try {
    411             uri = getClassPathEntry(jarFile, path);
    412           } catch (URISyntaxException e) {
    413             // Ignore bad entry
    414             logger.warning("Invalid Class-Path entry: " + path);
    415             continue;
    416           }
    417           builder.add(uri);
    418         }
    419       }
    420       return builder.build();
    421     }
    422 
    423     /**
    424      * Returns the absolute uri of the Class-Path entry value as specified in
    425      * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes">
    426      * JAR File Specification</a>. Even though the specification only talks about relative urls,
    427      * absolute urls are actually supported too (for example, in Maven surefire plugin).
    428      */
    429     @VisibleForTesting static URI getClassPathEntry(File jarFile, String path)
    430         throws URISyntaxException {
    431       URI uri = new URI(path);
    432       if (uri.isAbsolute()) {
    433         return uri;
    434       } else {
    435         return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI();
    436       }
    437     }
    438   }
    439 
    440   @VisibleForTesting static String getClassName(String filename) {
    441     int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
    442     return filename.substring(0, classNameEnd).replace('/', '.');
    443   }
    444 }
    445