All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.reflect.ClassPath Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote Jakarta Enterprise Beans and Jakarta Messaging, including all dependencies. It is intended for use by those not using maven, maven users should just import the Jakarta Enterprise Beans and Jakarta Messaging BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 35.0.0.Final
Show newest version
/*
 * 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.compatqual.NullableDecl;

/**
 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources.
 *
 * 

Warning: Current limitations: * *

    *
  • Looks only for files and JARs in URLs available from {@link URLClassLoader} instances or * the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. *
  • Only understands {@code 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 */ @Beta public final class ClassPath { private static final Logger logger = Logger.getLogger(ClassPath.class.getName()); private static final Predicate IS_TOP_LEVEL = new Predicate() { @Override public boolean apply(ClassInfo info) { return info.className.indexOf('$') == -1; } }; /** 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 resources; private ClassPath(ImmutableSet resources) { this.resources = resources; } /** * Returns a {@code ClassPath} representing all classes and resources loadable from {@code * classloader} and its ancestor class loaders. * *

Warning: {@code ClassPath} can find classes and resources only from: * *

    *
  • {@link URLClassLoader} instances' {@code file:} URLs *
  • 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. *
* * @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. */ public ImmutableSet getResources() { return resources; } /** * Returns all classes loadable from the current class path. * * @since 16.0 */ public ImmutableSet getAllClasses() { return FluentIterable.from(resources).filter(ClassInfo.class).toSet(); } /** Returns all top level classes loadable from the current class path. */ public ImmutableSet getTopLevelClasses() { return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet(); } /** Returns all top level classes whose package name is {@code packageName}. */ public ImmutableSet getTopLevelClasses(String packageName) { checkNotNull(packageName); ImmutableSet.Builder 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 {@code packageName} or starts with {@code * packageName} followed by a '.'. */ public ImmutableSet getTopLevelClassesRecursive(String packageName) { checkNotNull(packageName); String packagePrefix = packageName + '.'; ImmutableSet.Builder 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 */ @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 {@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 {@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 {@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". */ 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 {@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 {@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 {@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 {@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, {@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 {@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 scannedUris = Sets.newHashSet(); public final void scan(ClassLoader classloader) throws IOException { for (Entry 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. */ protected abstract void scanDirectory(ClassLoader loader, File directory) throws IOException; /** 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 {@code Class-Path} manifest attribute, according * to JAR * File Specification. If {@code manifest} is null, it means the jar file has no manifest, * and an empty set will be returned. */ @VisibleForTesting static ImmutableSet getClassPathFromManifest( File jarFile, @NullableDecl Manifest manifest) { if (manifest == null) { return ImmutableSet.of(); } ImmutableSet.Builder 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 getClassPathEntries(ClassLoader classloader) { LinkedHashMap 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 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 {@code java.class.path} {@linkplain * System#getProperty system property}. */ @VisibleForTesting // TODO(b/65488446): Make this a public API. static ImmutableList parseJavaClassPath() { ImmutableList.Builder 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). */ @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 resources = MultimapBuilder.hashKeys().linkedHashSetValues().build(); ImmutableSet getResources() { ImmutableSet.Builder builder = ImmutableSet.builder(); for (Entry entry : resources.entries()) { builder.add(ResourceInfo.of(entry.getValue(), entry.getKey())); } return builder.build(); } @Override protected void scanJarFile(ClassLoader classloader, JarFile file) { Enumeration 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 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. * * @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 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. } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy