net.freeutils.util.Reflect Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util;
import java.io.File;
import java.lang.reflect.*;
import java.util.*;
import java.math.BigInteger;
import java.math.BigDecimal;
import java.net.URL;
/**
* The {@code Reflect} class contains static reflection-related utility methods.
*/
public class Reflect {
/**
* Private constructor to avoid external instantiation.
*/
private Reflect() {}
/**
* Returns the name of a constant which is defined in given Class,
* has a name beginning with given prefix, and has given value. If the
* constant's name cannot be found, the value is returned as a hex String.
*
* @param cls the Class containing the constant
* @param constPrefix the prefix of the constant name (used in grouping constants)
* @param value the constant's value
* @return the name of the constant
*/
public static String getConstName(Class> cls, String constPrefix, long value) {
return getConstName(cls, constPrefix, value, false);
}
/**
* Returns the name of a constant (static field) which is defined in the
* given class, has a name beginning with given prefix, and has given value.
* If the constant's name cannot be found, the value's hex string is
* returned.
*
* @param cls the class containing the constant
* @param constPrefix the prefix of the constant name (used in grouping constants)
* @param value the constant's value
* @param removeConstPrefix if true constPrefix is removed from returned string
* @return the name of the constant
*/
public static String getConstName(Class> cls, String constPrefix,
long value, boolean removeConstPrefix) {
Field[] fields = cls.getFields();
try {
for (Field field : fields) {
String name = field.getName();
if (name.startsWith(constPrefix) && field.getLong(null) == value) {
int prefixLength = constPrefix.length();
return removeConstPrefix && name.length() > prefixLength
? name.substring(prefixLength) : name;
}
}
} catch (IllegalAccessException ignore) {}
return "0x" + Long.toHexString(value);
}
/**
* Returns the names of all constants (static fields) which are defined in the
* given class and have a name beginning with given prefix.
*
* @param cls the class containing the constant
* @param constPrefix the prefix of the constant name (used in grouping constants)
* @param removeConstPrefix if true constPrefix is removed from returned strings
* @return the names of the constants
*/
public static String[] getConstNames(Class> cls, String constPrefix,
boolean removeConstPrefix) {
Field[] fields = cls.getFields();
List names = new ArrayList<>();
int prefixLength = constPrefix.length();
for (Field field : fields) {
String name = field.getName();
if (name.startsWith(constPrefix)) {
names.add(removeConstPrefix && name.length() > prefixLength
? name.substring(prefixLength) : name);
}
}
return names.toArray(new String[0]);
}
/**
* Returns the value of the named constant (static field) which is defined in the
* given class.
*
* @param the returned constant type
* @param cls the class containing the constant
* @param constName the constant name
* @return the constant's value, or null if no such constant exists
*/
@SuppressWarnings("unchecked")
public static T getConstValue(Class> cls, String constName) {
try {
return (T)cls.getField(constName).get(null);
} catch (Exception e) {
return null;
}
}
/**
* Returns the location (file, jar, etc.) from which the given class was loaded.
* This is useful when trying to resolve classpath conflicts at runtime.
*
* @param cls the class whose location is requested
* @return the location from which the class was loaded, or null if unknown
*/
public static URL getClassLocation(Class> cls) {
return cls.getResource("/" + cls.getName().replace('.', '/') + ".class");
}
/**
* Returns the last modification time of the file containing the definition
* of the given class (a class file or jar file).
*
* @param cls a class whose containing file's modification time is returned
* @return the last modification time of the file containing the definition
* of the given class, or -1 if it cannot be determined
*/
public static long getClassModificationTime(Class> cls) {
long modified = -1;
URL url = getClassLocation(cls);
if (url != null) {
try {
String file;
// if it's within a jar file, get the jar file itself
if (url.getProtocol().equals("jar")) {
file = url.getPath();
int ind = file.indexOf('!'); // class within jar
if (ind > -1)
file = file.substring(0, ind);
url = new URL(file); // leave only jar file URL
}
file = url.toURI().getPath(); // proper URL decoding
modified = new File(file).lastModified();
} catch (Exception e) {
modified = -1;
}
}
return modified > 0 ? modified : -1;
}
/**
* Returns a new instance of the class with the given name.
*
* The class is instantiated using the context classloader
* for the current thread.
*
* @param the instance type
* @param className the name of the class
* @return a new instance of the class with the given name
* @throws InstantiationException if this Class represents an abstract class, an interface,
* an array class, a primitive type, or void; or if the class has no nullary
* constructor; or if the instantiation fails for some other reason
* @throws IllegalAccessException if the class or its nullary constructor is not accessible
* @throws ClassNotFoundException if the class cannot be located by the context class loader
* @throws NoSuchMethodException if the class has no empty constructor
* @throws InvocationTargetException if the constructor throws an exception
* @see Class#forName(String)
* @see Constructor#newInstance(Object...)
*/
@SuppressWarnings("unchecked")
public static T newInstance(String className) throws InstantiationException,
IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return (T)Class.forName(className, true, loader).getDeclaredConstructor().newInstance();
}
/**
* Returns a new instance of the class with the given name.
*
* @param the instance type
* @param className the name of the class
* @param packages default package names to be prepended to
* the given class name if it cannot be found
* @return a new instance of the class with the given name
* @throws InstantiationException if this Class represents an abstract class, an interface,
* an array class, a primitive type, or void; or if the class has no nullary
* constructor; or if the instantiation fails for some other reason
* @throws IllegalAccessException if the class or its nullary constructor is not accessible
* @throws ClassNotFoundException if the class cannot be located by the context class loader
* @throws NoSuchMethodException if the class has no empty constructor
* @throws InvocationTargetException if the constructor throws an exception
* @see Class#forName(String)
* @see Constructor#newInstance(Object...)
*/
public static T newInstance(String className, String... packages) throws InstantiationException,
IllegalAccessException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException {
try {
return Reflect.newInstance(className);
} catch (ClassNotFoundException cnfe) {
for (String p : packages) {
try {
return Reflect.newInstance(p + "." + className);
} catch (ClassNotFoundException ignore) {} // try next
}
throw cnfe; // all failed, so throw original exception
}
}
/**
* Returns a new instance of the class with the given name and parameters, using given class loader.
*
* @param the instance type
* @param loader the class loader used to load the class
* @param className the name of the class
* @param parameterTypes the constructor parameter types
* @param parameters the constructor parameters
* @return a new instance of the class with the given name
* @throws InstantiationException if this Class represents an abstract class, an interface,
* an array class, a primitive type, or void; or if the class has no nullary
* constructor; or if the instantiation fails for some other reason
* @throws IllegalAccessException if the specified constructor is not accessible
* @throws ClassNotFoundException if the class cannot be located by the given class loader
* @throws NoSuchMethodException if a matching constructor is not found
* @throws InvocationTargetException if the constructor throws an exception
*/
@SuppressWarnings("unchecked")
public static T newInstance(ClassLoader loader, String className,
Class>[] parameterTypes, Object[] parameters) throws InstantiationException,
IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
Class cls = (Class)loader.loadClass(className);
Constructor constructor = cls.getConstructor(parameterTypes);
return constructor.newInstance(parameters);
}
/**
* Returns a new instance of the class with the given name and parameters, using given class loader.
*
* @param the instance type
* @param loader the class loader used to load the class
* @param className the name of the class
* @param typesAndParams the constructor parameter types and values interleaved
* @return a new instance of the class with the given name
* @throws InstantiationException if this Class represents an abstract class, an interface,
* an array class, a primitive type, or void; or if the class has no nullary
* constructor; or if the instantiation fails for some other reason
* @throws IllegalAccessException if the specified constructor is not accessible
* @throws ClassNotFoundException if the class cannot be located by the given class loader
* @throws NoSuchMethodException if a matching constructor is not found
* @throws InvocationTargetException if the constructor throws an exception
*/
public static T newInstance(ClassLoader loader, String className, Object... typesAndParams)
throws InstantiationException, IllegalAccessException, ClassNotFoundException,
NoSuchMethodException, InvocationTargetException {
int len = typesAndParams.length / 2;
Class>[] types = new Class>[len];
Object[] values = new Object[len];
for (int i = 0; i < len; i++) {
types[i] = (Class>)typesAndParams[i * 2];
values[i] = typesAndParams[i * 2 + 1];
}
return newInstance(loader, className, types, values);
}
/**
* Returns a new {@link Proxy} instance which implements the given interfaces by dispatching
* method invocations to the given handler.
*
* This is a convenient wrapper for {@link Proxy#newProxyInstance} which uses the handler's class loader,
* the varargs list of interfaces, and a generic return type.
*
* @param one of the proxy's interface types
* @param handler the invocation handler to dispatch method invocations to
* @param interfaces the list of interfaces for the proxy class to implement
* @return a new proxy instance
*/
@SuppressWarnings("unchecked")
public static T newProxyInstance(InvocationHandler handler, Class>... interfaces) {
return (T)Proxy.newProxyInstance(handler.getClass().getClassLoader(), interfaces , handler);
}
/**
* Casts a given number to the given target Number class (primitive, wrapper or BigInteger/BigDecimal).
*
* @param the target number type
* @param n the number to cast
* @param cls the target class (primitive, wrapper or BigInteger/BigDecimal)
* @return the cast number
* @throws ClassCastException if the cast fails
*/
@SuppressWarnings("unchecked")
public static T castNumber(Number n, Class cls) {
if (cls.isInstance(n)) return (T)n;
if (cls == Long.class || cls == Long.TYPE) return (T)(Long)n.longValue();
if (cls == Integer.class || cls == Integer.TYPE) return (T)(Integer)n.intValue();
if (cls == Short.class || cls == Short.TYPE) return (T)(Short)n.shortValue();
if (cls == Byte.class || cls == Byte.TYPE) return (T)(Byte)n.byteValue();
if (cls == Float.class || cls == Float.TYPE) return (T)(Float)n.floatValue();
if (cls == Double.class || cls == Double.TYPE) return (T)(Double)n.doubleValue();
if (cls == BigInteger.class) return (T)BigInteger.valueOf(n.longValue());
if (cls == BigDecimal.class) return (T)BigDecimal.valueOf(n.doubleValue());
throw new ClassCastException("cannot cast " + n.getClass() + " to " + cls);
}
/**
* Attempts to convert the given value to an assignment-compatible instance
* of the given class.
*
* - If the given value is already of an appropriate type, it is returned
*
- otherwise, if the target class is a Number (primitive, wrapper or
* BigInteger/BigDecimal), an attempt is made to convert it to the appropriate type:
*
* - if value is a different type of Number it is converted using {@link #castNumber}
*
- otherwise the output of value.toString() is attempted to be parsed as a
* Number and then cast.
*
* - If the target class is a Boolean (primitive or wrapper), value.toString()
* is parsed into a Boolean
*
- If the target class is an enum type, the enum constant is returned
* by invoking {@code Enum.valueOf(cls, value.toString())}
*
- If the target class is a String, value.toString() is returned
*
*
* @param the conversion target type
* @param value the value to convert
* @param cls the target class to convert to
* @return the converted value (or the value itself)
* @throws ClassCastException if the conversion fails
*/
@SuppressWarnings("unchecked")
public static T convert(Object value, Class cls) throws ClassCastException {
if (value == null) {
if (cls.isPrimitive())
throw new ClassCastException("null value cannot be converted to primitive");
return null;
}
Class> valClass = value.getClass();
if (cls.isAssignableFrom(valClass))
return (T)value;
if (cls.isPrimitive() || Number.class.isAssignableFrom(cls)) {
if (!Number.class.isAssignableFrom(valClass)) {
if (cls == Boolean.class || cls == Boolean.TYPE) {
return (T)Boolean.valueOf(value.toString());
} else if (Date.class.isAssignableFrom(valClass)) {
value = ((Date)value).getTime();
} else {
try {
value = new BigDecimal(value.toString());
} catch (NumberFormatException nfe) {
throw new ClassCastException("cannot convert value '" + value + "' to " + cls);
}
}
}
value = castNumber((Number)value, (Class extends Number>)cls);
} else if (cls.isEnum()) {
value = Enum.valueOf((Class)cls, value.toString());
} else if (cls.isAssignableFrom(String.class)) {
value = value.toString();
}
return (T)value;
}
/**
* Returns the given class and all of its superclasses (if it is not an interface)
* or its superinterfaces (if it is an interface).
* The returned classes are ordered from the given class upwards.
*
* @param cls the classes whose superclasses or superinterfaces are returned
* @return the given class and all of its superclasses or superinterfaces
*/
public static List> getSuperClasses(Class> cls) {
List> classes = new ArrayList<>();
classes.add(cls);
if (cls.isInterface()) {
classes.addAll(Arrays.asList(cls.getInterfaces()));
} else {
for (; cls != null; cls = cls.getSuperclass())
classes.add(cls);
}
return classes;
}
/**
* Returns all methods of the given class (and its superclasses) that have the
* given name and number of parameters.
*
* @param cls a class
* @param name the name of the methods to return
* @param paramCount the number of parameters in the methods to return, or a negative
* number if any number of parameters should be returned
* @return the methods
*/
public static List getMethods(Class> cls, String name, int paramCount) {
List methods = new ArrayList<>();
for (Class> c : getSuperClasses(cls))
for (Method m : c.getDeclaredMethods())
if (m.getName().equals(name) && (paramCount < 0 || m.getParameterTypes().length == paramCount))
methods.add(m);
return methods;
}
/**
* Returns the method of the given class (or its superclasses) with the given arguments,
* or null if it is not found.
*
* @param cls the class whose method is returned
* @param name the name of the method
* @param parameterTypes the list of parameters
* @return the Method object that matches the specified name and parameterTypes
*/
public static Method getMethod(Class> cls, String name, Class>... parameterTypes) {
for (Method m : getMethods(cls, name, parameterTypes.length))
if (Arrays.equals(parameterTypes, m.getParameterTypes()))
return m;
return null;
}
/**
* Sets the value of a named property on the given object.
*
* For a given property named 'propname', this method attempts to invoke on the given object
* the methods named 'setPropname' which accept a single parameter. An attempt is made to convert
* the given value to the appropriate type using {@code #convert(Class, Object)}.
* The method returns after the first successful method invocation, or if all invocations (if any) failed.
*
* @param obj the object on which the property is to be set
* @param name the property name
* @param value the property value
* @return true if the property was set, false otherwise
*/
public static boolean setProperty(Object obj, String name, Object value) {
String methodName = "set" + Strings.capitalize(name);
for (Method m : getMethods(obj.getClass(), methodName, 1)) {
try {
value = convert(value, m.getParameterTypes()[0]);
m.invoke(obj, value);
return true;
} catch (Exception ignore) {}
}
return false;
}
/**
* Sets the values of named properties on the given object.
*
* This is a convenience method which calls {@link #setProperty(Object, String, Object)}
* for each of the given properties.
*
* @param obj the object on which the property is to be set
* @param properties the properties to set
* @return true if all properties were set, false if at least one of them was not set
*/
public static boolean setProperties(Object obj, Map properties) {
boolean res = true;
for (Map.Entry entry : properties.entrySet())
res &= setProperty(obj, entry.getKey(), entry.getValue());
return res;
}
/**
* Returns the list of bean field names for the given class.
*
* The bean field names are the non-empty strings XXXX for which the given
* class has a non-static public getXXXX method with no parameters and a
* non-static public setXXXX method with a single parameter of the same type
* as the getXXXX return type.
*
* @param cls the class whose fields are returned
* @return the list of bean field names for the given class,
* or an empty list if the class has no bean fields
*/
public static List getBeanFieldNames(Class> cls) {
Method[] methods = cls.getMethods();
List names = new ArrayList<>();
for (Method m : methods) {
String name = m.getName();
if ((name.startsWith("get") && name.length() > 3
|| m.getReturnType() == boolean.class
&& name.startsWith("is") && name.length() > 2)
&& !Modifier.isStatic(m.getModifiers())
&& m.getParameterTypes().length == 0) {
name = name.substring(name.charAt(0) == 'i' ? 2 : 3);
try {
Method m2 = cls.getMethod("set" + name, m.getReturnType());
if (!Modifier.isStatic(m2.getModifiers())) {
names.add(Strings.decapitalizeFirst(name));
}
} catch (NoSuchMethodException ignore) {
// no matching pair of methods - skip
}
}
}
names.sort(Comparator.naturalOrder()); // for consistency
return names;
}
/**
* Returns the given object's values corresponding to the given bean fields.
*
* For each given field name XXXX, the corresponding object in the returned
* list is the result of invoking a method named getXXXX or isXXX with no
* parameters on the given object, or null if such a method does not exist
* or its invocation fails.
*
* @param obj the object whose field values are returned
* @param fields the given object's bean field names to retrieve, such as
* the return value of calling getBeanFieldNames() for the given
* object's class
* @return the given object's values corresponding to the given bean fields
*/
public static List