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
17package com.google.common.reflect;
18
19import static com.google.common.base.Preconditions.checkNotNull;
20
21import com.google.common.annotations.Beta;
22import com.google.common.annotations.VisibleForTesting;
23import com.google.common.base.CharMatcher;
24import com.google.common.base.Predicate;
25import com.google.common.base.Splitter;
26import com.google.common.collect.FluentIterable;
27import com.google.common.collect.ImmutableMap;
28import com.google.common.collect.ImmutableSet;
29import com.google.common.collect.ImmutableSortedSet;
30import com.google.common.collect.Maps;
31import com.google.common.collect.Ordering;
32import com.google.common.collect.Sets;
33
34import java.io.File;
35import java.io.IOException;
36import java.net.URI;
37import java.net.URISyntaxException;
38import java.net.URL;
39import java.net.URLClassLoader;
40import java.util.Enumeration;
41import java.util.LinkedHashMap;
42import java.util.Map;
43import java.util.Set;
44import java.util.jar.Attributes;
45import java.util.jar.JarEntry;
46import java.util.jar.JarFile;
47import java.util.jar.Manifest;
48import java.util.logging.Logger;
49
50import 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
59public 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