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<ClassInfo> classes;
064
065  private ClassPath(ImmutableSet<ClassInfo> classes) {
066    this.classes = classes;
067  }
068
069  /**
070   * Returns a {@code ClassPath} representing all classes loadable from {@code classloader} and its
071   * 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<ClassInfo> builder = new ImmutableSortedSet.Builder<ClassInfo>(
080        Ordering.usingToString());
081    for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
082      builder.addAll(readClassesFrom(entry.getKey(), entry.getValue()));
083    }
084    return new ClassPath(builder.build());
085  }
086
087  /** Returns all top level classes loadable from the current class path. */
088  public ImmutableSet<ClassInfo> getTopLevelClasses() {
089    return classes;
090  }
091
092  /** Returns all top level classes whose package name is {@code packageName}. */
093  public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
094    checkNotNull(packageName);
095    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
096    for (ClassInfo classInfo : classes) {
097      if (classInfo.getPackageName().equals(packageName)) {
098        builder.add(classInfo);
099      }
100    }
101    return builder.build();
102  }
103
104  /**
105   * Returns all top level classes whose package name is {@code packageName} or starts with
106   * {@code packageName} followed by a '.'.
107   */
108  public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
109    checkNotNull(packageName);
110    String packagePrefix = packageName + '.';
111    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
112    for (ClassInfo classInfo : classes) {
113      if (classInfo.getName().startsWith(packagePrefix)) {
114        builder.add(classInfo);
115      }
116    }
117    return builder.build();
118  }
119
120  /** Represents a class that can be loaded through {@link #load}. */
121  public static final class ClassInfo {
122    private final String className;
123    private final ClassLoader loader;
124
125    @VisibleForTesting ClassInfo(String className, ClassLoader loader) {
126      this.className = checkNotNull(className);
127      this.loader = checkNotNull(loader);
128    }
129
130    /** Returns the package name of the class, without attempting to load the class. */
131    public String getPackageName() {
132      return Reflection.getPackageName(className);
133    }
134
135    /** Returns the simple name of the underlying class as given in the source code. */
136    public String getSimpleName() {
137      String packageName = getPackageName();
138      if (packageName.isEmpty()) {
139        return className;
140      }
141      // Since this is a top level class, its simple name is always the part after package name.
142      return className.substring(packageName.length() + 1);
143    }
144
145    /** Returns the fully qualified name of the class. */
146    public String getName() {
147      return className;
148    }
149
150    /** Loads (but doesn't link or initialize) the class. */
151    public Class<?> load() {
152      try {
153        return loader.loadClass(className);
154      } catch (ClassNotFoundException e) {
155        // Shouldn't happen, since the class name is read from the class path.
156        throw new IllegalStateException(e);
157      }
158    }
159
160    @Override public int hashCode() {
161      return className.hashCode();
162    }
163
164    @Override public boolean equals(Object obj) {
165      if (obj instanceof ClassInfo) {
166        ClassInfo that = (ClassInfo) obj;
167        return className.equals(that.className)
168            && loader == that.loader;
169      }
170      return false;
171    }
172
173    @Override public String toString() {
174      return className;
175    }
176  }
177
178  @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries(
179      ClassLoader classloader) {
180    LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap();
181    // Search parent first, since it's the order ClassLoader#loadClass() uses.
182    ClassLoader parent = classloader.getParent();
183    if (parent != null) {
184      entries.putAll(getClassPathEntries(parent));
185    }
186    if (classloader instanceof URLClassLoader) {
187      URLClassLoader urlClassLoader = (URLClassLoader) classloader;
188      for (URL entry : urlClassLoader.getURLs()) {
189        URI uri;
190        try {
191          uri = entry.toURI();
192        } catch (URISyntaxException e) {
193          throw new IllegalArgumentException(e);
194        }
195        if (!entries.containsKey(uri)) {
196          entries.put(uri, classloader);
197        }
198      }
199    }
200    return ImmutableMap.copyOf(entries);
201  }
202
203  private static ImmutableSet<ClassInfo> readClassesFrom(URI uri, ClassLoader classloader)
204      throws IOException {
205    if (uri.getScheme().equals("file")) {
206      return readClassesFrom(new File(uri), classloader);
207    } else {
208      return ImmutableSet.of();
209    }
210  }
211
212  @VisibleForTesting static ImmutableSet<ClassInfo> readClassesFrom(
213      File file, ClassLoader classloader)
214      throws IOException {
215    if (!file.exists()) {
216      return ImmutableSet.of();
217    }
218    if (file.isDirectory()) {
219      return readClassesFromDirectory(file, classloader);
220    } else {
221      return readClassesFromJar(file, classloader);
222    }
223  }
224
225  private static ImmutableSet<ClassInfo> readClassesFromDirectory(
226      File directory, ClassLoader classloader) {
227    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
228    readClassesFromDirectory(directory, classloader, "", builder);
229    return builder.build();
230  }
231
232  private static void readClassesFromDirectory(
233      File directory, ClassLoader classloader,
234      String packagePrefix, ImmutableSet.Builder<ClassInfo> builder) {
235    for (File f : directory.listFiles()) {
236      String name = f.getName();
237      if (f.isDirectory()) {
238        readClassesFromDirectory(f, classloader, packagePrefix + name + ".", builder);
239      } else if (isTopLevelClassFile(name)) {
240        String className = packagePrefix + getClassName(name);
241        builder.add(new ClassInfo(className, classloader));
242      }
243    }
244  }
245
246  private static ImmutableSet<ClassInfo> readClassesFromJar(File file, ClassLoader classloader)
247      throws IOException {
248    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
249    JarFile jarFile = new JarFile(file);
250    for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) {
251      builder.addAll(readClassesFrom(uri, classloader));
252    }
253    Enumeration<JarEntry> entries = jarFile.entries();
254    while (entries.hasMoreElements()) {
255      JarEntry entry = entries.nextElement();
256      if (isTopLevelClassFile(entry.getName())) {
257        String className = getClassName(entry.getName().replace('/', '.'));
258        builder.add(new ClassInfo(className, classloader));
259      }
260    }
261    return builder.build();
262  }
263
264  /**
265   * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
266   * to <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
267   * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest,
268   * and an empty set will be returned.
269   */
270  @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest(
271      File jarFile, @Nullable Manifest manifest) {
272    if (manifest == null) {
273      return ImmutableSet.of();
274    }
275    ImmutableSet.Builder<URI> builder = ImmutableSet.builder();
276    String classpathAttribute = manifest.getMainAttributes().getValue("Class-Path");
277    if (classpathAttribute != null) {
278      for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
279        URI uri;
280        try {
281          uri = getClassPathEntry(jarFile, path);
282        } catch (URISyntaxException e) {
283          // Ignore bad entry
284          logger.warning("Invalid Class-Path entry: " + path);
285          continue;
286        }
287        builder.add(uri);
288      }
289    }
290    return builder.build();
291  }
292
293  /**
294   * Returns the absolute uri of the Class-Path entry value as specified in
295   * <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
296   * JAR File Specification</a>. Even though the specification only talks about relative urls,
297   * absolute urls are actually supported too (for example, in Maven surefire plugin).
298   */
299  @VisibleForTesting static URI getClassPathEntry(File jarFile, String path)
300      throws URISyntaxException {
301    URI uri = new URI(path);
302    if (uri.isAbsolute()) {
303      return uri;
304    } else {
305      return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI();
306    }
307  }
308
309  @VisibleForTesting static boolean isTopLevelClassFile(String filename) {
310    return filename.endsWith(CLASS_FILE_NAME_EXTENSION) && filename.indexOf('$') < 0;
311  }
312
313  @VisibleForTesting static String getClassName(String filename) {
314    int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
315    return filename.substring(0, classNameEnd);
316  }
317}