/*
 * 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:

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:

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.

See ClassLoader.getResource

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. } } }