/*
* Copyright (C) 2012 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.common.reflect;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
import static java.util.logging.Level.WARNING;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.common.io.Resources;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
Scans the source of a ClassLoader
and finds all loadable classes and resources. Warning: Current limitations:
- Looks only for files and JARs in URLs available from
URLClassLoader
instances or the system class loader. - Only understands
file:
URLs.
In the case of directory classloaders, symlinks are supported but cycles are not traversed.
This guarantees discovery of each unique loadable resource. However, not all possible
aliases for resources on cyclic paths will be listed.
Author: Ben Yu Since: 14.0
/**
* Scans the source of a {@link ClassLoader} and finds all loadable classes and resources.
*
* <p><b>Warning:</b> Current limitations:
*
* <ul>
* <li>Looks only for files and JARs in URLs available from {@link URLClassLoader} instances or
* the {@linkplain ClassLoader#getSystemClassLoader() system class loader}.
* <li>Only understands {@code file:} URLs.
* </ul>
*
* <p>In the case of directory classloaders, symlinks are supported but cycles are not traversed.
* This guarantees discovery of each <em>unique</em> loadable resource. However, not all possible
* aliases for resources on cyclic paths will be listed.
*
* @author Ben Yu
* @since 14.0
*/
@Beta
public final class ClassPath {
private static final Logger logger = Logger.getLogger(ClassPath.class.getName());
private static final Predicate<ClassInfo> IS_TOP_LEVEL =
new Predicate<ClassInfo>() {
@Override
public boolean apply(ClassInfo info) {
return info.className.indexOf('$') == -1;
}
};
Separator for the Class-Path manifest attribute value in jar files. /** Separator for the Class-Path manifest attribute value in jar files. */
private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
Splitter.on(" ").omitEmptyStrings();
private static final String CLASS_FILE_NAME_EXTENSION = ".class";
private final ImmutableSet<ResourceInfo> resources;
private ClassPath(ImmutableSet<ResourceInfo> resources) {
this.resources = resources;
}
Returns a ClassPath
representing all classes and resources loadable from
classloader
and its ancestor class loaders. Warning: ClassPath
can find classes and resources only from:
URLClassLoader
instances' file:
URLs - the system class loader. To search the system class loader even when it is not a
URLClassLoader
(as in Java 9),
ClassPath
searches the files from the java.class.path
system property.
Throws: - IOException – if the attempt to read class path resources (jar files or directories)
failed.
/**
* Returns a {@code ClassPath} representing all classes and resources loadable from {@code
* classloader} and its ancestor class loaders.
*
* <p><b>Warning:</b> {@code ClassPath} can find classes and resources only from:
*
* <ul>
* <li>{@link URLClassLoader} instances' {@code file:} URLs
* <li>the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. To search the
* system class loader even when it is not a {@link URLClassLoader} (as in Java 9), {@code
* ClassPath} searches the files from the {@code java.class.path} system property.
* </ul>
*
* @throws IOException if the attempt to read class path resources (jar files or directories)
* failed.
*/
public static ClassPath from(ClassLoader classloader) throws IOException {
DefaultScanner scanner = new DefaultScanner();
scanner.scan(classloader);
return new ClassPath(scanner.getResources());
}
Returns all resources loadable from the current class path, including the class files of all
loadable classes but excluding the "META-INF/MANIFEST.MF" file.
/**
* Returns all resources loadable from the current class path, including the class files of all
* loadable classes but excluding the "META-INF/MANIFEST.MF" file.
*/
public ImmutableSet<ResourceInfo> getResources() {
return resources;
}
Returns all classes loadable from the current class path.
Since: 16.0
/**
* Returns all classes loadable from the current class path.
*
* @since 16.0
*/
public ImmutableSet<ClassInfo> getAllClasses() {
return FluentIterable.from(resources).filter(ClassInfo.class).toSet();
}
Returns all top level classes loadable from the current class path. /** Returns all top level classes loadable from the current class path. */
public ImmutableSet<ClassInfo> getTopLevelClasses() {
return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet();
}
Returns all top level classes whose package name is packageName
. /** Returns all top level classes whose package name is {@code packageName}. */
public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
checkNotNull(packageName);
ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
for (ClassInfo classInfo : getTopLevelClasses()) {
if (classInfo.getPackageName().equals(packageName)) {
builder.add(classInfo);
}
}
return builder.build();
}
Returns all top level classes whose package name is packageName
or starts with
packageName
followed by a '.'. /**
* Returns all top level classes whose package name is {@code packageName} or starts with {@code
* packageName} followed by a '.'.
*/
public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
checkNotNull(packageName);
String packagePrefix = packageName + '.';
ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
for (ClassInfo classInfo : getTopLevelClasses()) {
if (classInfo.getName().startsWith(packagePrefix)) {
builder.add(classInfo);
}
}
return builder.build();
}
Represents a class path resource that can be either a class file or any other resource file
loadable from the class path.
Since: 14.0
/**
* Represents a class path resource that can be either a class file or any other resource file
* loadable from the class path.
*
* @since 14.0
*/
@Beta
public static class ResourceInfo {
private final String resourceName;
final ClassLoader loader;
static ResourceInfo of(String resourceName, ClassLoader loader) {
if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) {
return new ClassInfo(resourceName, loader);
} else {
return new ResourceInfo(resourceName, loader);
}
}
ResourceInfo(String resourceName, ClassLoader loader) {
this.resourceName = checkNotNull(resourceName);
this.loader = checkNotNull(loader);
}
Returns the url identifying the resource.
Throws: - NoSuchElementException – if the resource cannot be loaded through the class loader,
despite physically existing in the class path.
/**
* Returns the url identifying the resource.
*
* <p>See {@link ClassLoader#getResource}
*
* @throws NoSuchElementException if the resource cannot be loaded through the class loader,
* despite physically existing in the class path.
*/
public final URL url() {
URL url = loader.getResource(resourceName);
if (url == null) {
throw new NoSuchElementException(resourceName);
}
return url;
}
Returns a ByteSource
view of the resource from which its bytes can be read. Throws: - NoSuchElementException – if the resource cannot be loaded through the class loader,
despite physically existing in the class path.
Since: 20.0
/**
* Returns a {@link ByteSource} view of the resource from which its bytes can be read.
*
* @throws NoSuchElementException if the resource cannot be loaded through the class loader,
* despite physically existing in the class path.
* @since 20.0
*/
public final ByteSource asByteSource() {
return Resources.asByteSource(url());
}
Returns a CharSource
view of the resource from which its bytes can be read as characters decoded with the given charset
. Throws: - NoSuchElementException – if the resource cannot be loaded through the class loader,
despite physically existing in the class path.
Since: 20.0
/**
* Returns a {@link CharSource} view of the resource from which its bytes can be read as
* characters decoded with the given {@code charset}.
*
* @throws NoSuchElementException if the resource cannot be loaded through the class loader,
* despite physically existing in the class path.
* @since 20.0
*/
public final CharSource asCharSource(Charset charset) {
return Resources.asCharSource(url(), charset);
}
Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
public final String getResourceName() {
return resourceName;
}
@Override
public int hashCode() {
return resourceName.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ResourceInfo) {
ResourceInfo that = (ResourceInfo) obj;
return resourceName.equals(that.resourceName) && loader == that.loader;
}
return false;
}
// Do not change this arbitrarily. We rely on it for sorting ResourceInfo.
@Override
public String toString() {
return resourceName;
}
}
Represents a class that can be loaded through load
. Since: 14.0
/**
* Represents a class that can be loaded through {@link #load}.
*
* @since 14.0
*/
@Beta
public static final class ClassInfo extends ResourceInfo {
private final String className;
ClassInfo(String resourceName, ClassLoader loader) {
super(resourceName, loader);
this.className = getClassName(resourceName);
}
Returns the package name of the class, without attempting to load the class.
Behaves identically to Package.getName()
but does not require the class (or package) to be loaded.
/**
* Returns the package name of the class, without attempting to load the class.
*
* <p>Behaves identically to {@link Package#getName()} but does not require the class (or
* package) to be loaded.
*/
public String getPackageName() {
return Reflection.getPackageName(className);
}
Returns the simple name of the underlying class as given in the source code.
Behaves identically to Class.getSimpleName()
but does not require the class to be loaded.
/**
* Returns the simple name of the underlying class as given in the source code.
*
* <p>Behaves identically to {@link Class#getSimpleName()} but does not require the class to be
* loaded.
*/
public String getSimpleName() {
int lastDollarSign = className.lastIndexOf('$');
if (lastDollarSign != -1) {
String innerClassName = className.substring(lastDollarSign + 1);
// local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are
// entirely numeric whereas local classes have the user supplied name as a suffix
return CharMatcher.digit().trimLeadingFrom(innerClassName);
}
String packageName = getPackageName();
if (packageName.isEmpty()) {
return className;
}
// Since this is a top level class, its simple name is always the part after package name.
return className.substring(packageName.length() + 1);
}
Returns the fully qualified name of the class.
Behaves identically to Class.getName()
but does not require the class to be loaded.
/**
* Returns the fully qualified name of the class.
*
* <p>Behaves identically to {@link Class#getName()} but does not require the class to be
* loaded.
*/
public String getName() {
return className;
}
Loads (but doesn't link or initialize) the class.
Throws: - LinkageError – when there were errors in loading classes that this class depends on. For example,
NoClassDefFoundError
.
/**
* Loads (but doesn't link or initialize) the class.
*
* @throws LinkageError when there were errors in loading classes that this class depends on.
* For example, {@link NoClassDefFoundError}.
*/
public Class<?> load() {
try {
return loader.loadClass(className);
} catch (ClassNotFoundException e) {
// Shouldn't happen, since the class name is read from the class path.
throw new IllegalStateException(e);
}
}
@Override
public String toString() {
return className;
}
}
Abstract class that scans through the class path represented by a ClassLoader
and calls scanDirectory
and scanJarFile
for directories and jar files on the class path respectively. /**
* Abstract class that scans through the class path represented by a {@link ClassLoader} and calls
* {@link #scanDirectory} and {@link #scanJarFile} for directories and jar files on the class path
* respectively.
*/
abstract static class Scanner {
// We only scan each file once independent of the classloader that resource might be associated
// with.
private final Set<File> scannedUris = Sets.newHashSet();
public final void scan(ClassLoader classloader) throws IOException {
for (Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
scan(entry.getKey(), entry.getValue());
}
}
@VisibleForTesting
final void scan(File file, ClassLoader classloader) throws IOException {
if (scannedUris.add(file.getCanonicalFile())) {
scanFrom(file, classloader);
}
}
Called when a directory is scanned for resource files. /** Called when a directory is scanned for resource files. */
protected abstract void scanDirectory(ClassLoader loader, File directory) throws IOException;
Called when a jar file is scanned for resource entries. /** Called when a jar file is scanned for resource entries. */
protected abstract void scanJarFile(ClassLoader loader, JarFile file) throws IOException;
private void scanFrom(File file, ClassLoader classloader) throws IOException {
try {
if (!file.exists()) {
return;
}
} catch (SecurityException e) {
logger.warning("Cannot access " + file + ": " + e);
// TODO(emcmanus): consider whether to log other failure cases too.
return;
}
if (file.isDirectory()) {
scanDirectory(classloader, file);
} else {
scanJar(file, classloader);
}
}
private void scanJar(File file, ClassLoader classloader) throws IOException {
JarFile jarFile;
try {
jarFile = new JarFile(file);
} catch (IOException e) {
// Not a jar file
return;
}
try {
for (File path : getClassPathFromManifest(file, jarFile.getManifest())) {
scan(path, classloader);
}
scanJarFile(classloader, jarFile);
} finally {
try {
jarFile.close();
} catch (IOException ignored) {
}
}
}
Returns the class path URIs specified by the Class-Path
manifest attribute, according to JAR
File Specification. If manifest
is null, it means the jar file has no manifest, and an empty set will be returned. /**
* Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
* to <a
* href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR
* File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest,
* and an empty set will be returned.
*/
@VisibleForTesting
static ImmutableSet<File> getClassPathFromManifest(File jarFile, @Nullable Manifest manifest) {
if (manifest == null) {
return ImmutableSet.of();
}
ImmutableSet.Builder<File> builder = ImmutableSet.builder();
String classpathAttribute =
manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString());
if (classpathAttribute != null) {
for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
URL url;
try {
url = getClassPathEntry(jarFile, path);
} catch (MalformedURLException e) {
// Ignore bad entry
logger.warning("Invalid Class-Path entry: " + path);
continue;
}
if (url.getProtocol().equals("file")) {
builder.add(toFile(url));
}
}
}
return builder.build();
}
@VisibleForTesting
static ImmutableMap<File, ClassLoader> getClassPathEntries(ClassLoader classloader) {
LinkedHashMap<File, ClassLoader> entries = Maps.newLinkedHashMap();
// Search parent first, since it's the order ClassLoader#loadClass() uses.
ClassLoader parent = classloader.getParent();
if (parent != null) {
entries.putAll(getClassPathEntries(parent));
}
for (URL url : getClassLoaderUrls(classloader)) {
if (url.getProtocol().equals("file")) {
File file = toFile(url);
if (!entries.containsKey(file)) {
entries.put(file, classloader);
}
}
}
return ImmutableMap.copyOf(entries);
}
private static ImmutableList<URL> getClassLoaderUrls(ClassLoader classloader) {
if (classloader instanceof URLClassLoader) {
return ImmutableList.copyOf(((URLClassLoader) classloader).getURLs());
}
if (classloader.equals(ClassLoader.getSystemClassLoader())) {
return parseJavaClassPath();
}
return ImmutableList.of();
}
Returns the URLs in the class path specified by the java.class.path
system property. /**
* Returns the URLs in the class path specified by the {@code java.class.path} {@linkplain
* System#getProperty system property}.
*/
@VisibleForTesting // TODO(b/65488446): Make this a public API.
static ImmutableList<URL> parseJavaClassPath() {
ImmutableList.Builder<URL> urls = ImmutableList.builder();
for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
try {
try {
urls.add(new File(entry).toURI().toURL());
} catch (SecurityException e) { // File.toURI checks to see if the file is a directory
urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
}
} catch (MalformedURLException e) {
logger.log(WARNING, "malformed classpath entry: " + entry, e);
}
}
return urls.build();
}
Returns the absolute uri of the Class-Path entry value as specified in JAR
File Specification. Even though the specification only talks about relative urls,
absolute urls are actually supported too (for example, in Maven surefire plugin).
/**
* Returns the absolute uri of the Class-Path entry value as specified in <a
* href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR
* File Specification</a>. Even though the specification only talks about relative urls,
* absolute urls are actually supported too (for example, in Maven surefire plugin).
*/
@VisibleForTesting
static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException {
return new URL(jarFile.toURI().toURL(), path);
}
}
@VisibleForTesting
static final class DefaultScanner extends Scanner {
private final SetMultimap<ClassLoader, String> resources =
MultimapBuilder.hashKeys().linkedHashSetValues().build();
ImmutableSet<ResourceInfo> getResources() {
ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder();
for (Entry<ClassLoader, String> entry : resources.entries()) {
builder.add(ResourceInfo.of(entry.getValue(), entry.getKey()));
}
return builder.build();
}
@Override
protected void scanJarFile(ClassLoader classloader, JarFile file) {
Enumeration<JarEntry> entries = file.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
continue;
}
resources.get(classloader).add(entry.getName());
}
}
@Override
protected void scanDirectory(ClassLoader classloader, File directory) throws IOException {
Set<File> currentPath = new HashSet<>();
currentPath.add(directory.getCanonicalFile());
scanDirectory(directory, classloader, "", currentPath);
}
Recursively scan the given directory, adding resources for each file encountered. Symlinks
which have already been traversed in the current tree path will be skipped to eliminate
cycles; otherwise symlinks are traversed.
Params: - directory – the root of the directory to scan
- classloader – the classloader that includes resources found in
directory
- packagePrefix – resource path prefix inside
classloader
for any files found under directory
- currentPath – canonical files already visited in the current directory tree path, for
cycle elimination
/**
* Recursively scan the given directory, adding resources for each file encountered. Symlinks
* which have already been traversed in the current tree path will be skipped to eliminate
* cycles; otherwise symlinks are traversed.
*
* @param directory the root of the directory to scan
* @param classloader the classloader that includes resources found in {@code directory}
* @param packagePrefix resource path prefix inside {@code classloader} for any files found
* under {@code directory}
* @param currentPath canonical files already visited in the current directory tree path, for
* cycle elimination
*/
private void scanDirectory(
File directory, ClassLoader classloader, String packagePrefix, Set<File> currentPath)
throws IOException {
File[] files = directory.listFiles();
if (files == null) {
logger.warning("Cannot read directory " + directory);
// IO error, just skip the directory
return;
}
for (File f : files) {
String name = f.getName();
if (f.isDirectory()) {
File deref = f.getCanonicalFile();
if (currentPath.add(deref)) {
scanDirectory(deref, classloader, packagePrefix + name + "/", currentPath);
currentPath.remove(deref);
}
} else {
String resourceName = packagePrefix + name;
if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
resources.get(classloader).add(resourceName);
}
}
}
}
}
@VisibleForTesting
static String getClassName(String filename) {
int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
return filename.substring(0, classNameEnd).replace('/', '.');
}
// TODO(benyu): Try java.nio.file.Paths#get() when Guava drops JDK 6 support.
@VisibleForTesting
static File toFile(URL url) {
checkArgument(url.getProtocol().equals("file"));
try {
return new File(url.toURI()); // Accepts escaped characters like %20.
} catch (URISyntaxException e) { // URL.toURI() doesn't escape chars.
return new File(url.getPath()); // Accepts non-escaped chars like space.
}
}
}