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

net.freeutils.util.Reflect Maven / Gradle / Ivy

The newest version!
/*
 *  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)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 getBeanFieldValues(Object obj, Collection fields) { List values = new ArrayList<>(fields.size()); for (String name : fields) { try { String capitalized = Strings.capitalizeFirst(name); try { Method m = obj.getClass().getMethod("get" + capitalized); values.add(m.invoke(obj)); } catch (NoSuchMethodException nsme) { Method m = obj.getClass().getMethod("is" + capitalized); values.add(m.invoke(obj)); } } catch (Exception e) { values.add(null); } } return values; } /** * Returns the mapping of bean field names and corresponding values for the * given object. *

* The bean field names are the non-empty strings XXXX for which the * given class has a non-static public getXXX method with no parameters * (for methods returning a boolean also isXXX naming is valid) and a * non-static public setXXXX method with a single parameter of the same * type as the getXXXX return type. * * @param obj the object whose fields are returned * @return the map of bean field names and corresponding values for the * given object, or an empty map if the object has no bean fields */ public static Map getBeanFields(Object obj) { List names = getBeanFieldNames(obj.getClass()); List values = getBeanFieldValues(obj, names); Map map = new LinkedHashMap<>(); for (int i = 0; i < names.size(); i++) map.put(names.get(i), values.get(i)); return map; } /** * Returns whether two objects are equal. *

* Two objects a and b are considered equal if a == b, or a.equals(b), * or both are not null and b has every field of a with the same value, * or they are arrays of the same component type, length, and content. * Fields and array elements are compared using this method, recursively. * Classes under the "java.lang" package are not checked for field equality * (if they are not equal by reference or equals() method, they are unequal.) *

* This method is useful when objects need to be compared by content for * equality, but may or may not implement their own equals() method that * compares their respective fields. * * @param a the first object * @param b the second object * @return true if the objects are equal, false otherwise */ public static boolean isEqual(Object a, Object b) { if (a == b || a != null && a.equals(b)) return true; if (a == null || b == null) return false; Class cls = a.getClass(); if (cls.isPrimitive() || cls.getPackage() != null && cls.getPackage().getName().equals("java.lang")) return false; if (cls.isArray()) { int lena = Array.getLength(a); int lenb = Array.getLength(b); if (lena != lenb || cls.getComponentType() != b.getClass().getComponentType()) return false; for (int i = 0; i < lena; i++) if (!isEqual(Array.get(a, i), Array.get(b, i))) return false; return true; } for (Field f : cls.getDeclaredFields()) { if (!Modifier.isStatic(f.getModifiers())) { try { f.setAccessible(true); if (!isEqual(f.get(a), f.get(b))) return false; } catch (IllegalAccessException e) { return false; } } } return true; } /** * Returns the estimated size of an object as bytes of memory used. * Recursive non-static references are included as well. * * @param obj an object (can be null if the class represents a primitive type) * @param referenceSize the size of a reference (pointer) in bytes; * this is typically 4 on 32-bit platforms, and 8 on 64-bit platforms * @return the size of the object in bytes */ public static int sizeOf(Object obj, int referenceSize) { return obj == null ? 0 : sizeOf(obj, obj.getClass(), new HashSet<>(), referenceSize); } /** * Returns the estimated size of an object as bytes of memory used, including all * non-static fields: primitives, arrays and nested objects (recursively). Each * object instance is counted only once, even if there are more than one references * to it (although the memory taken up by the references themselves is counted). *

* Note that this is only an estimate, as the actual memory allocation per object * is platform and implementation specific, may include additional padding to * various sized boundaries, special optimizations, etc. * * @param obj an object (can be null if the class represents a primitive type) * @param cls the object's class * @param known a set of known instances that should not be counted again * @param referenceSize the size of a reference (pointer) in bytes; * this is typically 4 on 32-bit platforms, and 8 on 64-bit platforms, * but compressed references (enabled by default in JDK7) are 4 bytes * on 64-bit platforms as well * @return the size of the object in bytes */ public static int sizeOf(Object obj, Class cls, Set known, int referenceSize) { if (cls.isPrimitive()) { if (cls == Integer.TYPE || cls == Float.TYPE) return 4; if (cls == Boolean.TYPE || cls == Byte.TYPE) return 1; if (cls == Long.TYPE || cls == Double.TYPE) return 8; if (cls == Short.TYPE || cls == Character.TYPE) return 2; } if (obj == null || known.contains(obj)) return 0; known.add(obj); int size = 8; // Object overhead (assumed) int recursiveSize = 0; // other objects if (cls.isArray()) { size += 4; // length field (assumed) int len = Array.getLength(obj); if (len > 0) { if (cls.getComponentType().isPrimitive()) { size += len * sizeOf(null, cls.getComponentType(), known, referenceSize); } else { size += len * referenceSize; for (int i = 0; i < len; i++) { Object element = Array.get(obj, i); if (element != null) recursiveSize += sizeOf(element, element.getClass(), known, referenceSize); } } } } else { while (cls != null) { for (Field f : cls.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; try { f.setAccessible(true); if (f.getType().isPrimitive()) { size += sizeOf(null, f.getType(), known, referenceSize); } else { size += referenceSize; Object element = f.get(obj); if (element != null) recursiveSize += sizeOf(element, element.getClass(), known, referenceSize); } } catch (Exception ignore) { // can't happen } } cls = cls.getSuperclass(); } } // pad to 8 byte boundary size += (8 - size % 8) % 8; return size + recursiveSize; } }