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.Splitter;
024import com.google.common.collect.ImmutableMap;
025import com.google.common.collect.ImmutableSet;
026import com.google.common.collect.ImmutableSortedSet;
027import com.google.common.collect.Maps;
028import com.google.common.collect.Ordering;
029
030import java.io.File;
031import java.io.IOException;
032import java.net.URI;
033import java.net.URISyntaxException;
034import java.net.URL;
035import java.net.URLClassLoader;
036import java.util.Enumeration;
037import java.util.LinkedHashMap;
038import java.util.Map;
039import java.util.jar.JarEntry;
040import java.util.jar.JarFile;
041import java.util.jar.Manifest;
042import java.util.logging.Logger;
043
044import javax.annotation.Nullable;
045
046/**
047 * Scans the source of a {@link ClassLoader} and finds all the classes loadable.
048 *
049 * @author Ben Yu
050 * @since 14.0
051 */
052@Beta
053public final class ClassPath {
054
055  private static final Logger logger = Logger.getLogger(ClassPath.class.getName());
056
057  /** Separator for the Class-Path manifest attribute value in jar files. */
058  private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
059      Splitter.on(" ").omitEmptyStrings();
060
061  private static final String CLASS_FILE_NAME_EXTENSION = ".class";
062
063  private final ImmutableSet<ResourceInfo> resources;
064
065  private ClassPath(ImmutableSet<ResourceInfo> resources) {
066    this.resources = resources;
067  }
068
069  /**
070   * Returns a {@code ClassPath} representing all classes and resources loadable from {@code
071   * classloader} and its parent class loaders.
072   *
073   * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported.
074   *
075   * @throws IOException if the attempt to read class path resources (jar files or directories)
076   *         failed.
077   */
078  public static ClassPath from(ClassLoader classloader) throws IOException {
079    ImmutableSortedSet.Builder<ResourceInfo> resources =
080        new ImmutableSortedSet.Builder<ResourceInfo>(Ordering.usingToString());
081    for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
082      browse(entry.getKey(), entry.getValue(), resources);
083    }
084    return new ClassPath(resources.build());
085  }
086
087  /**
088   * Returns all resources loadable from the current class path, including the class files of all
089   * loadable classes.
090   */
091  public ImmutableSet<ResourceInfo> getResources() {
092    return resources;
093  }
094
095  /** Returns all top level classes loadable from the current class path. */
096  public ImmutableSet<ClassInfo> getTopLevelClasses() {
097    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
098    for (ResourceInfo resource : resources) {
099      if (resource instanceof ClassInfo) {
100        builder.add((ClassInfo) resource);
101      }
102    }
103    return builder.build();
104  }
105
106  /** Returns all top level classes whose package name is {@code packageName}. */
107  public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
108    checkNotNull(packageName);
109    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
110    for (ClassInfo classInfo : getTopLevelClasses()) {
111      if (classInfo.getPackageName().equals(packageName)) {
112        builder.add(classInfo);
113      }
114    }
115    return builder.build();
116  }
117
118  /**
119   * Returns all top level classes whose package name is {@code packageName} or starts with
120   * {@code packageName} followed by a '.'.
121   */
122  public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
123    checkNotNull(packageName);
124    String packagePrefix = packageName + '.';
125    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
126    for (ClassInfo classInfo : getTopLevelClasses()) {
127      if (classInfo.getName().startsWith(packagePrefix)) {
128        builder.add(classInfo);
129      }
130    }
131    return builder.build();
132  }
133
134  /**
135   * Represents a class path resource that can be either a class file or any other resource file
136   * loadable from the class path.
137   *
138   * @since 14.0
139   */
140  @Beta
141  public static class ResourceInfo {
142    private final String resourceName;
143    final ClassLoader loader;
144
145    static ResourceInfo of(String resourceName, ClassLoader loader) {
146      if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION) && !resourceName.contains("$")) {
147        return new ClassInfo(resourceName, loader);
148      } else {
149        return new ResourceInfo(resourceName, loader);
150      }
151    }
152  
153    ResourceInfo(String resourceName, ClassLoader loader) {
154      this.resourceName = checkNotNull(resourceName);
155      this.loader = checkNotNull(loader);
156    }
157
158    /** Returns the url identifying the resource. */
159    public final URL url() {
160      return checkNotNull(loader.getResource(resourceName),
161          "Failed to load resource: %s", resourceName);
162    }
163
164    /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
165    public final String getResourceName() {
166      return resourceName;
167    }
168
169    @Override public int hashCode() {
170      return resourceName.hashCode();
171    }
172
173    @Override public boolean equals(Object obj) {
174      if (obj instanceof ResourceInfo) {
175        ResourceInfo that = (ResourceInfo) obj;
176        return resourceName.equals(that.resourceName)
177            && loader == that.loader;
178      }
179      return false;
180    }
181
182    @Override public String toString() {
183      return resourceName;
184    }
185  }
186
187  /**
188   * Represents a class that can be loaded through {@link #load}.
189   *
190   * @since 14.0
191   */
192  @Beta
193  public static final class ClassInfo extends ResourceInfo {
194    private final String className;
195
196    ClassInfo(String resourceName, ClassLoader loader) {
197      super(resourceName, loader);
198      this.className = getClassName(resourceName);
199    }
200
201    /** Returns the package name of the class, without attempting to load the class. */
202    public String getPackageName() {
203      return Reflection.getPackageName(className);
204    }
205
206    /** Returns the simple name of the underlying class as given in the source code. */
207    public String getSimpleName() {
208      String packageName = getPackageName();
209      if (packageName.isEmpty()) {
210        return className;
211      }
212      // Since this is a top level class, its simple name is always the part after package name.
213      return className.substring(packageName.length() + 1);
214    }
215
216    /** Returns the fully qualified name of the class. */
217    public String getName() {
218      return className;
219    }
220
221    /** Loads (but doesn't link or initialize) the class. */
222    public Class<?> load() {
223      try {
224        return loader.loadClass(className);
225      } catch (ClassNotFoundException e) {
226        // Shouldn't happen, since the class name is read from the class path.
227        throw new IllegalStateException(e);
228      }
229    }
230
231    @Override public String toString() {
232      return className;
233    }
234  }
235
236  @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries(
237      ClassLoader classloader) {
238    LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap();
239    // Search parent first, since it's the order ClassLoader#loadClass() uses.
240    ClassLoader parent = classloader.getParent();
241    if (parent != null) {
242      entries.putAll(getClassPathEntries(parent));
243    }
244    if (classloader instanceof URLClassLoader) {
245      URLClassLoader urlClassLoader = (URLClassLoader) classloader;
246      for (URL entry : urlClassLoader.getURLs()) {
247        URI uri;
248        try {
249          uri = entry.toURI();
250        } catch (URISyntaxException e) {
251          throw new IllegalArgumentException(e);
252        }
253        if (!entries.containsKey(uri)) {
254          entries.put(uri, classloader);
255        }
256      }
257    }
258    return ImmutableMap.copyOf(entries);
259  }
260
261  private static void browse(
262      URI uri, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
263      throws IOException {
264    if (uri.getScheme().equals("file")) {
265      browseFrom(new File(uri), classloader, resources);
266    }
267  }
268
269  @VisibleForTesting static void browseFrom(
270      File file, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
271      throws IOException {
272    if (!file.exists()) {
273      return;
274    }
275    if (file.isDirectory()) {
276      browseDirectory(file, classloader, resources);
277    } else {
278      browseJar(file, classloader, resources);
279    }
280  }
281
282  private static void browseDirectory(
283      File directory, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources) {
284    browseDirectory(directory, classloader, "", resources);
285  }
286
287  private static void browseDirectory(
288      File directory, ClassLoader classloader, String packagePrefix,
289      ImmutableSet.Builder<ResourceInfo> resources) {
290    for (File f : directory.listFiles()) {
291      String name = f.getName();
292      if (f.isDirectory()) {
293        browseDirectory(f, classloader, packagePrefix + name + "/", resources);
294      } else {
295        String resourceName = packagePrefix + name;
296        resources.add(ResourceInfo.of(resourceName, classloader));
297      }
298    }
299  }
300
301  private static void browseJar(
302      File file, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
303      throws IOException {
304    JarFile jarFile;
305    try {
306      jarFile = new JarFile(file);
307    } catch (IOException e) {
308      // Not a jar file
309      return;
310    }
311    try {
312      for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) {
313        browse(uri, classloader, resources);
314      }
315      Enumeration<JarEntry> entries = jarFile.entries();
316      while (entries.hasMoreElements()) {
317        JarEntry entry = entries.nextElement();
318        if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
319          continue;
320        }
321        resources.add(ResourceInfo.of(entry.getName(), classloader));
322      }
323    } finally {
324      try {
325        jarFile.close();
326      } catch (IOException ignored) {}
327    }
328  }
329
330  /**
331   * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
332   * to <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
333   * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest,
334   * and an empty set will be returned.
335   */
336  @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest(
337      File jarFile, @Nullable Manifest manifest) {
338    if (manifest == null) {
339      return ImmutableSet.of();
340    }
341    ImmutableSet.Builder<URI> builder = ImmutableSet.builder();
342    String classpathAttribute = manifest.getMainAttributes().getValue("Class-Path");
343    if (classpathAttribute != null) {
344      for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
345        URI uri;
346        try {
347          uri = getClassPathEntry(jarFile, path);
348        } catch (URISyntaxException e) {
349          // Ignore bad entry
350          logger.warning("Invalid Class-Path entry: " + path);
351          continue;
352        }
353        builder.add(uri);
354      }
355    }
356    return builder.build();
357  }
358
359  /**
360   * Returns the absolute uri of the Class-Path entry value as specified in
361   * <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
362   * JAR File Specification</a>. Even though the specification only talks about relative urls,
363   * absolute urls are actually supported too (for example, in Maven surefire plugin).
364   */
365  @VisibleForTesting static URI getClassPathEntry(File jarFile, String path)
366      throws URISyntaxException {
367    URI uri = new URI(path);
368    if (uri.isAbsolute()) {
369      return uri;
370    } else {
371      return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI();
372    }
373  }
374
375  @VisibleForTesting static String getClassName(String filename) {
376    int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
377    return filename.substring(0, classNameEnd).replace('/', '.');
378  }
379}