View Javadoc
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 }