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 
ClassPath(ImmutableSet<ResourceInfo> resources)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    */
from(ClassLoader classloader)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    */
getResources()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    */
getAllClasses()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. */
getTopLevelClasses()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}. */
getTopLevelClasses(String 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    */
getTopLevelClassesRecursive(String packageName)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 
of(String resourceName, ClassLoader loader)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 
ResourceInfo(String resourceName, ClassLoader loader)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. */
url()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". */
getResourceName()178     public final String getResourceName() {
179       return resourceName;
180     }
181 
hashCode()182     @Override public int hashCode() {
183       return resourceName.hashCode();
184     }
185 
equals(Object obj)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.
toString()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 
ClassInfo(String resourceName, ClassLoader loader)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      */
getPackageName()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      */
getSimpleName()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.length() == 0) {
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      */
getName()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      */
load()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 
toString()273     @Override public String toString() {
274       return className;
275     }
276   }
277 
getClassPathEntries( ClassLoader classloader)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 
getResources()309     ImmutableSortedSet<ResourceInfo> getResources() {
310       return resources.build();
311     }
312 
scan(URI uri, ClassLoader classloader)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 
scanFrom(File file, ClassLoader classloader)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 
scanDirectory(File directory, ClassLoader classloader)331     private void scanDirectory(File directory, ClassLoader classloader) throws IOException {
332       scanDirectory(directory, classloader, "", ImmutableSet.<File>of());
333     }
334 
scanDirectory( File directory, ClassLoader classloader, String packagePrefix, ImmutableSet<File> ancestors)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 
scanJar(File file, ClassLoader classloader)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      */
getClassPathFromManifest( File jarFile, @Nullable Manifest manifest)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      */
getClassPathEntry(File jarFile, String path)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 
getClassName(String filename)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