org.plumelib.reflection.ReflectionPlume Maven / Gradle / Ivy
Show all versions of reflection-util Show documentation
// If you edit this file, you must also edit its tests.
// For tests of this and the entire plume package, see class TestPlume.
package org.plumelib.reflection;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import org.checkerframework.checker.interning.qual.Interned;
import org.checkerframework.checker.mustcall.qual.MustCallUnknown;
import org.checkerframework.checker.mustcall.qual.PolyMustCall;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.ClassGetName;
import org.checkerframework.checker.signature.qual.ClassGetSimpleName;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.dataflow.qual.Pure;
/** Utility functions related to reflection, Class, Method, ClassLoader, and classpath. */
public final class ReflectionPlume {
/** This class is a collection of methods; it does not represent anything. */
private ReflectionPlume() {
throw new Error("do not instantiate");
}
// //////////////////////////////////////////////////////////////////////
// Class
//
/**
* Return true iff sub is a subtype of sup. If sub == sup, then sub is considered a subtype of sub
* and this method returns true.
*
* @param sub class to test for being a subtype
* @param sup class to test for being a supertype
* @return true iff sub is a subtype of sup
*/
@SuppressWarnings({
"allcheckers:purity.not.deterministic.call", // getInterfaces() is used as a set
"allcheckers:method.guarantee.violated" // getInterfaces() is used as a set
})
@Pure
public static boolean isSubtype(Class> sub, Class> sup) {
if (sub == sup) {
return true;
}
// Handle superclasses
Class> parent = sub.getSuperclass();
// If parent == null, sub == Object
if ((parent != null) && (parent == sup || isSubtype(parent, sup))) {
return true;
}
// Handle interfaces
@SuppressWarnings({"lock:method.guarantee.violated"}) // order doesn't matter
Class>[] interfaces = sub.getInterfaces();
for (Class> ifc : interfaces) {
if (ifc == sup || isSubtype(ifc, sup)) {
return true;
}
}
return false;
}
/** Used by {@link #classForName}. */
private static HashMap> primitiveClasses = new HashMap<>(8);
static {
primitiveClasses.put("boolean", Boolean.TYPE);
primitiveClasses.put("byte", Byte.TYPE);
primitiveClasses.put("char", Character.TYPE);
primitiveClasses.put("double", Double.TYPE);
primitiveClasses.put("float", Float.TYPE);
primitiveClasses.put("int", Integer.TYPE);
primitiveClasses.put("long", Long.TYPE);
primitiveClasses.put("short", Short.TYPE);
}
// TODO: Should create a method that handles any ClassGetName (including primitives), but not
// fully-qualified names. A routine with a polymorphic parameter type is confusing.
/**
* Like {@link Class#forName(String)}: given a string representing a non-array class, returns the
* Class. Unlike {@link Class#forName(String)}, the argument may be a primitive type or a
* fully-qualified name (in addition to a binary name).
*
* If the given name can't be found, this method changes the last '.' to a dollar sign ($) and
* tries again. This accounts for inner classes that are incorrectly passed in in fully-qualified
* format instead of binary format. (It should try multiple dollar signs, not just at the last
* position.)
*
*
Recall the rather odd specification for {@link Class#forName(String)}: the argument is a
* binary name for non-arrays, but a field descriptor for arrays. This method uses the same rules,
* but additionally handles primitive types and, for non-arrays, fully-qualified names.
*
* @param className name of the class
* @return the Class corresponding to className
* @throws ClassNotFoundException if the class is not found
*/
// The @ClassGetName annotation encourages proper use, even though this can take a
// fully-qualified name (only for a non-array).
public static Class> classForName(@ClassGetName String className)
throws ClassNotFoundException {
Class> result = primitiveClasses.get(className);
if (result != null) {
return result;
}
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
while (true) {
int pos = className.lastIndexOf('.');
if (pos < 0) {
throw e;
}
@SuppressWarnings("signature") // checked below & exception is handled
@ClassGetName String innerName = className.substring(0, pos) + "$" + className.substring(pos + 1);
className = innerName;
try {
return Class.forName(className);
} catch (ClassNotFoundException ee) {
continue; // nothing to do
}
}
}
}
/**
* Returns the simple unqualified class name that corresponds to the specified fully qualified
* name. For example, if qualifiedName is java.lang.String, String will be returned.
*
* @param qualifiedName the fully-qualified name of a class
* @return the simple unqualified name of the class
*/
// TODO: does not follow the specification for inner classes (where the
// type name should be empty), but I think this is more informative anyway.
@SuppressWarnings("signature") // string conversion
public static @ClassGetSimpleName String fullyQualifiedNameToSimpleName(
@FullyQualifiedName String qualifiedName) {
int offset = qualifiedName.lastIndexOf('.');
if (offset == -1) {
return qualifiedName;
}
return qualifiedName.substring(offset + 1);
}
/**
* Returns the class name, including outer classes but without the package. Uses "." as the
* separator between outer an inner classes, as in Java source code.
*
* @param c a class
* @return the class name, including outer classes but without the package
*/
public static String nameWithoutPackage(Class> c) {
Class> enclosing = c.getEnclosingClass();
if (enclosing == null) {
return c.getSimpleName();
}
StringBuilder result = new StringBuilder(c.getSimpleName());
while (enclosing != null) {
result.insert(0, enclosing.getSimpleName() + ".");
enclosing = enclosing.getEnclosingClass();
}
return result.toString();
}
// //////////////////////////////////////////////////////////////////////
// ClassLoader
//
/**
* This static nested class has no purpose but to define defineClassFromFile.
* ClassLoader.defineClass is protected, so I subclass ClassLoader in order to call defineClass.
*/
private static class PromiscuousLoader extends ClassLoader {
/** Create a new PromiscuousLoader. */
public PromiscuousLoader() {}
/**
* Converts the bytes in a file into an instance of class Class, and also resolves (links) the
* class. Delegates the real work to defineClass.
*
* @see ClassLoader#defineClass(String,byte[],int,int)
* @param className the expected binary name of the class to define, or null if not known
* @param pathname the file from which to load the class
* @return the {@code Class} object that was created
* @throws FileNotFoundException if the file does not exist
* @throws IOException if there is trouble reading the file
*/
public Class> defineClassFromFile(@BinaryName String className, String pathname)
throws FileNotFoundException, IOException {
int numbytes;
byte[] classBytes;
int bytesRead;
try (FileInputStream fi = new FileInputStream(pathname)) {
numbytes = fi.available();
classBytes = new byte[numbytes];
bytesRead = fi.read(classBytes);
}
if (bytesRead < numbytes) {
throw new Error(
String.format(
"Expected to read %d bytes from %s, got %d", numbytes, pathname, bytesRead));
}
Class> returnClass = defineClass(className, classBytes, 0, numbytes);
resolveClass(returnClass); // link the class
return returnClass;
}
}
/** A ClassLoader that can call defineClassFromFile. */
private static PromiscuousLoader thePromiscuousLoader = new PromiscuousLoader();
/**
* Converts the bytes in a file into an instance of class Class, and resolves (links) the class.
* Like {@link ClassLoader#defineClass(String,byte[],int,int)}, but takes a file name rather than
* an array of bytes as an argument, and also resolves (links) the class.
*
* @see ClassLoader#defineClass(String,byte[],int,int)
* @param className the name of the class to define, or null if not known
* @param pathname the pathname of a .class file
* @return a Java Object corresponding to the Class defined in the .class file
* @throws FileNotFoundException if the file cannot be found
* @throws IOException if there is trouble reading the file
*/
// Also throws UnsupportedClassVersionError and some other exceptions.
public static Class> defineClassFromFile(@BinaryName String className, String pathname)
throws FileNotFoundException, IOException {
return thePromiscuousLoader.defineClassFromFile(className, pathname);
}
// //////////////////////////////////////////////////////////////////////
// Classpath
//
// Perhaps abstract out the simpler addToPath from this
/**
* Add the directory to the system classpath.
*
* @param dir directory to add to the system classpath
*/
public static void addToClasspath(String dir) {
// If the dir isn't on CLASSPATH, add it.
String pathSep = System.getProperty("path.separator");
// what is the point of the "replace()" call?
String cp = System.getProperty("java.class.path", ".").replace('\\', '/');
StringTokenizer tokenizer = new StringTokenizer(cp, pathSep, false);
boolean found = false;
while (tokenizer.hasMoreTokens() && !found) {
found = tokenizer.nextToken().equals(dir);
}
if (!found) {
System.setProperty("java.class.path", dir + pathSep + cp);
}
}
/**
* Returns the classpath as a multi-line string.
*
* @return the classpath as a multi-line string
*/
public static String classpathToString() {
return System.getProperty("java.class.path")
.replace(File.pathSeparator, System.lineSeparator());
}
// //////////////////////////////////////////////////////////////////////
// Method
//
/**
* Maps from a comma-delimited string of arg types, such as appears in a method signature, to an
* array of Class objects, one for each arg type. Example keys include: "java.lang.String,
* java.lang.String, java.lang.Class[]" and "int,int".
*/
static HashMap[]> args_seen = new HashMap<>();
/**
* Given a method signature, return the method.
*
* Example calls are:
*
*
* UtilPlume.methodForName(
* "org.plumelib.reflection.ReflectionPlume.methodForName"
* +"(java.lang.String, java.lang.String, java.lang.Class[])")
* UtilPlume.methodForName("org.plumelib.reflection.ReflectionPlume.methodForName"
* +"(java.lang.String,java.lang.String,java.lang.Class[])")
* UtilPlume.methodForName("java.lang.Math.min(int,int)")
*
*
* @param method a method signature
* @return the method corresponding to the given signature
* @throws ClassNotFoundException if the class is not found
* @throws NoSuchMethodException if the method is not found
*/
public static Method methodForName(String method)
throws ClassNotFoundException, NoSuchMethodException, SecurityException {
int oparenpos = method.indexOf('(');
int dotpos = method.lastIndexOf('.', oparenpos);
int cparenpos = method.indexOf(')', oparenpos);
if ((dotpos == -1) || (oparenpos == -1) || (cparenpos == -1)) {
throw new Error(
"malformed method name should contain a period, open paren, and close paren: "
+ method
+ " <<"
+ dotpos
+ ","
+ oparenpos
+ ","
+ cparenpos
+ ">>");
}
for (int i = cparenpos + 1; i < method.length(); i++) {
if (!Character.isWhitespace(method.charAt(i))) {
throw new Error(
"malformed method name should contain only whitespace following close paren");
}
}
@SuppressWarnings("signature") // throws exception if class does not exist
@BinaryName String classname = method.substring(0, dotpos);
String methodname = method.substring(dotpos + 1, oparenpos);
String allArgnames = method.substring(oparenpos + 1, cparenpos).trim();
Class>[] argclasses = args_seen.get(allArgnames);
if (argclasses == null) {
@BinaryName String[] argnames;
if (allArgnames.equals("")) {
argnames = new String[0];
} else {
@SuppressWarnings("signature") // string manipulation: splitting a method signature
@BinaryName String[] bnArgnames = allArgnames.split(" *, *");
argnames = bnArgnames;
}
@MonotonicNonNull Class>[] argclassesTmp = new Class>[argnames.length];
for (int i = 0; i < argnames.length; i++) {
@BinaryName String bnArgname = argnames[i];
@ClassGetName String cgnArgname = Signatures.binaryNameToClassGetName(bnArgname);
argclassesTmp[i] = classForName(cgnArgname);
}
// TODO: Shouldn't this require a warning suppression?
Class>[] argclassesRes = (@NonNull Class>[]) argclassesTmp;
argclasses = argclassesRes;
args_seen.put(allArgnames, argclassesRes);
}
return methodForName(classname, methodname, argclasses);
}
/**
* Given a class name and a method name in that class, return the method.
*
* @param classname class in which to find the method
* @param methodname the method name
* @param params the parameters of the method
* @return the method named classname.methodname with parameters params
* @throws ClassNotFoundException if the class is not found
* @throws NoSuchMethodException if the method is not found
*/
public static Method methodForName(
@BinaryName String classname, String methodname, Class>[] params)
throws ClassNotFoundException, NoSuchMethodException, SecurityException {
Class> c = Class.forName(classname);
Method m = c.getDeclaredMethod(methodname, params);
return m;
}
// //////////////////////////////////////////////////////////////////////
// Reflection
//
// TODO: add method invokeMethod; see
// java/Translation/src/graph/tests/Reflect.java (but handle returning a
// value).
// TODO: make this restore the access to its original value, such as private?
/**
* Sets the given field, which may be final and/or private. Leaves the field accessible.
*
* @param o object in which to set the field; null iff the field is static
* @param fieldName name of field to set
* @param value new value of field; may be null iff the field is nullable
* @throws NoSuchFieldException if the field does not exist in the object
*/
public static void setFinalField(Object o, String fieldName, @Interned Object value)
throws NoSuchFieldException {
Class> c = o.getClass();
while (c != Object.class) { // Class is interned
// System.out.printf ("Setting field %s in %s%n", fieldName, c);
try {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(o, value);
return;
} catch (NoSuchFieldException e) {
if (c.getSuperclass() == Object.class) { // Class is interned
throw e;
}
} catch (IllegalAccessException e) {
throw new Error("This can't happen: " + e);
}
c = c.getSuperclass();
assert c != null : "@AssumeAssertion(nullness): c was not Object, so is not null now";
}
throw new NoSuchFieldException(fieldName);
}
// TODO: make this restore the access to its original value, such as private?
/**
* Reads the given field, which may be private. Leaves the field accessible. Use with care!
*
* @param o object in which to set the field
* @param fieldName name of field to set
* @return new value of field
* @throws NoSuchFieldException if the field does not exist in the object
*/
public static @Nullable Object getPrivateField(Object o, String fieldName)
throws NoSuchFieldException {
Class> c = o.getClass();
while (c != Object.class) { // Class is interned
// System.out.printf ("Setting field %s in %s%n", fieldName, c);
try {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
return f.get(o);
} catch (IllegalAccessException e) {
throw new Error("This can't happen: " + e);
} catch (NoSuchFieldException e) {
if (c.getSuperclass() == Object.class) { // Class is interned
throw e;
}
// nothing to do; will now examine superclass
}
c = c.getSuperclass();
assert c != null : "@AssumeAssertion(nullness): c was not Object, so is not null now";
}
throw new NoSuchFieldException(fieldName);
}
/**
* Returns the least upper bound of the given classes.
*
* @param a a class
* @param b a class
* @param the (inferred) least upper bound of the two arguments
* @return the least upper bound of the two classes, or null if both are null
*/
public static @Nullable Class leastUpperBound(@Nullable Class a, @Nullable Class b) {
if (a == b) {
return a;
} else if (a == null) {
return b;
} else if (b == null) {
return a;
} else if (a == Void.TYPE) {
return b;
} else if (b == Void.TYPE) {
return a;
} else if (a.isAssignableFrom(b)) {
return a;
} else if (b.isAssignableFrom(a)) {
return b;
} else {
// There may not be a unique least upper bound.
// Probably return some specific class rather than a wildcard.
throw new Error("Not yet implemented");
}
}
/**
* Returns the least upper bound of all the given classes.
*
* @param classes a non-empty list of classes
* @param the (inferred) least upper bound of the arguments
* @return the least upper bound of all the given classes
*/
public static @Nullable Class leastUpperBound(@Nullable Class[] classes) {
Class result = null;
for (Class clazz : classes) {
result = leastUpperBound(result, clazz);
}
return result;
}
/**
* Returns the least upper bound of the classes of the given objects.
*
* @param objects a list of objects
* @param the (inferred) least upper bound of the arguments
* @return the least upper bound of the classes of the given objects, or null if all arguments are
* null
*/
@SuppressWarnings("unchecked") // cast to Class
public static @Nullable Class leastUpperBound(@PolyMustCall @PolyNull Object[] objects) {
Class result = null;
for (Object obj : objects) {
if (obj != null) {
result = leastUpperBound(result, (Class) obj.getClass());
}
}
return result;
}
/**
* Returns the least upper bound of the classes of the given objects.
*
* @param objects a non-empty list of objects
* @param the (inferred) least upper bound of the arguments
* @return the least upper bound of the classes of the given objects, or null if all arguments are
* null
*/
@SuppressWarnings("unchecked") // cast to Class
public static @Nullable Class leastUpperBound(
List extends @MustCallUnknown @Nullable Object> objects) {
Class result = null;
for (Object obj : objects) {
if (obj != null) {
result = leastUpperBound(result, (Class) obj.getClass());
}
}
return result;
}
}