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

nonapi.io.github.classgraph.utils.ReflectionUtils Maven / Gradle / Ivy

/*
 * This file is part of ClassGraph.
 *
 * Author: Luke Hutchison
 *
 * Hosted at: https://github.com/classgraph/classgraph
 *
 * --
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Luke Hutchison
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without
 * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
 * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
 * OR OTHER DEALINGS IN THE SOFTWARE.
 */
package nonapi.io.github.classgraph.utils;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import io.github.classgraph.ClassGraph;

/** Reflection utility methods that can be used by ClassLoaderHandlers. */
public final class ReflectionUtils {
    /** The reflection driver to use. */
    private static ReflectionDriver reflectionDriver;

    static {
        loadReflectionDriver();
    }

    /** Call this if you change the value of {@link ClassGraph#CIRCUMVENT_ENCAPSULATION}. */
    public static void loadReflectionDriver() {
        if (ClassGraph.CIRCUMVENT_ENCAPSULATION) {
            try {
                reflectionDriver = new NarcissusReflectionDriver();
            } catch (final Throwable t) {
                System.err.println("Could not load Narcissus reflection driver: " + t);
                // Fall back to standard reflection driver
            }
        }
        if (reflectionDriver == null) {
            reflectionDriver = new StandardReflectionDriver();
        }
    }

    /** Reflection driver */
    private static abstract class ReflectionDriver {
        /**
         * Find a class by name.
         *
         * @param className
         *            the class name
         * @return the class reference
         */
        abstract Class findClass(final String className) throws Exception;

        /**
         * Get declared methods for class.
         *
         * @param cls
         *            the class
         * @return the declared methods
         */
        abstract Method[] getDeclaredMethods(Class cls) throws Exception;

        /**
         * Get declared constructors for class.
         *
         * @param 
         *            the generic type
         * @param cls
         *            the class
         * @return the declared constructors
         */
        abstract  Constructor[] getDeclaredConstructors(Class cls) throws Exception;

        /**
         * Get declared fields for class.
         *
         * @param cls
         *            the class
         * @return the declared fields
         */
        abstract Field[] getDeclaredFields(Class cls) throws Exception;

        /**
         * Get the value of a non-static field, boxing the value if necessary.
         *
         * @param object
         *            the object instance to get the field value from
         * @param field
         *            the non-static field
         * @return the value of the field
         */
        abstract Object getField(final Object object, final Field field) throws Exception;

        /**
         * Set the value of a non-static field, unboxing the value if necessary.
         *
         * @param object
         *            the object instance to get the field value from
         * @param field
         *            the non-static field
         * @param value
         *            the value to set
         */
        abstract void setField(final Object object, final Field field, Object value) throws Exception;

        /**
         * Get the value of a static field, boxing the value if necessary.
         *
         * @param field
         *            the static field
         * @return the static field
         */
        abstract Object getStaticField(final Field field) throws Exception;

        /**
         * Set the value of a static field, unboxing the value if necessary.
         *
         * @param field
         *            the static field
         * @param the
         *            value to set
         */
        abstract void setStaticField(final Field field, Object value) throws Exception;

        /**
         * Invoke a non-static method, boxing the result if necessary.
         *
         * @param object
         *            the object instance to invoke the method on
         * @param method
         *            the non-static method
         * @param args
         *            the method arguments (or {@code new Object[0]} if there are no args)
         * @return the return value (possibly a boxed value)
         */
        abstract Object invokeMethod(final Object object, final Method method, final Object... args)
                throws Exception;

        /**
         * Invoke a static method, boxing the result if necessary.
         *
         * @param method
         *            the static method
         * @param args
         *            the method arguments (or {@code new Object[0]} if there are no args)
         * @return the return value (possibly a boxed value)
         */
        abstract Object invokeStaticMethod(final Method method, final Object... args) throws Exception;

        /**
         * Make a field or method accessible.
         * 
         * @param accessibleObject
         *            the field or method.
         * @return true if successful.
         */
        abstract boolean makeAccessible(final AccessibleObject accessibleObject);

        /** Iterator applied to each method of a class and its superclasses/interfaces. */
        private static interface MethodIterator {
            /** @return true to stop iterating, or false to continue iterating */
            boolean foundMethod(Method m) throws Exception;
        }

        /**
         * Iterate through all methods in the given class. Also iterates up through superclasses and interfaces, to
         * collect all methods of the class and its superclasses, and any default methods defined in interfaces.
         *
         * @param cls
         *            the class
         */
        void forAllMethods(final Class cls, final MethodIterator methodIter) throws Exception {
            // Iterate from class to its superclasses, and find initial interfaces to start traversing from
            final Set> visited = new HashSet<>();
            final LinkedList> interfaceQueue = new LinkedList<>();
            for (Class c = cls; c != null; c = c.getSuperclass()) {
                for (final Method m : getDeclaredMethods(c)) {
                    if (methodIter.foundMethod(m)) {
                        return;
                    }
                }
                // Find interfaces and superinterfaces implemented by this class or its superclasses
                if (c.isInterface() && visited.add(c)) {
                    interfaceQueue.add(c);
                }
                for (final Class iface : c.getInterfaces()) {
                    if (visited.add(iface)) {
                        interfaceQueue.add(iface);
                    }
                }
            }
            // Traverse through interfaces looking for default methods
            while (!interfaceQueue.isEmpty()) {
                final Class iface = interfaceQueue.remove();
                for (final Method m : getDeclaredMethods(iface)) {
                    if (methodIter.foundMethod(m)) {
                        return;
                    }
                }
                for (final Class superIface : iface.getInterfaces()) {
                    if (visited.add(superIface)) {
                        interfaceQueue.add(superIface);
                    }
                }
            }
        }

        /**
         * Find a method by name and parameter types in the given class.
         *
         * @param cls
         *            the class
         * @param methodName
         *            the method name.
         * @param paramTypes
         *            the parameter types of the method.
         * @return the {@link Method}
         * @throws NoSuchMethodException
         *             if the class does not contain a method of the given name and parameter types
         */
        Method findMethod(final Class cls, final String methodName, final Class... paramTypes)
                throws Exception {
            final AtomicReference method = new AtomicReference<>();
            forAllMethods(cls, new MethodIterator() {
                @Override
                public boolean foundMethod(final Method m) {
                    if (m.getName().equals(methodName) && Arrays.equals(paramTypes, m.getParameterTypes())
                    // If method is not accessible, fall through and try superclass method of same
                    // name and paramTypes
                            && makeAccessible(m)) {
                        method.set(m);
                        return true;
                    }
                    return false;
                }
            });
            final Method m = method.get();
            if (m != null) {
                return m;
            } else {
                throw new NoSuchMethodException(methodName);
            }
        }

        /**
         * Enumerate all methods in the given class, ignoring visibility and bypassing security checks. Also
         * iterates up through superclasses, to collect all methods of the class and its superclasses.
         *
         * @param cls
         *            the class
         * @return a list of {@link Method} objects representing all methods declared by the class or a superclass.
         */
        List enumerateMethods(final Class cls) throws Exception {
            final List methodOrder = new ArrayList<>();
            forAllMethods(cls, new MethodIterator() {
                @Override
                public boolean foundMethod(final Method m) {
                    methodOrder.add(m);
                    return false;
                }
            });
            return methodOrder;
        }

        /**
         * Find a field by name in the given class.
         *
         * @param cls
         *            the class
         * @param fieldName
         *            the field name.
         * @return the {@link Field}
         * @throws NoSuchFieldException
         *             if the class does not contain a field of the given name
         */
        Field findField(final Class cls, final String fieldName) throws Exception {
            for (Class c = cls; c != null; c = c.getSuperclass()) {
                for (final Field field : getDeclaredFields(c)) {
                    if (field.getName().equals(fieldName)) {
                        return field;
                    }
                }
            }
            throw new NoSuchFieldException(fieldName);
        }
    }

    /**
     * Standard reflection driver (uses {@link AccessibleObject#setAccessible(boolean)} to access non-public fields
     * if necessary).
     */
    private static class StandardReflectionDriver extends ReflectionDriver {
        @Override
        Class findClass(final String className) throws Exception {
            return Class.forName(className);
        }

        @Override
        Method[] getDeclaredMethods(final Class cls) throws Exception {
            return cls.getDeclaredMethods();
        }

        @SuppressWarnings("unchecked")
        @Override
         Constructor[] getDeclaredConstructors(final Class cls) throws Exception {
            return (Constructor[]) cls.getDeclaredConstructors();
        }

        @Override
        Field[] getDeclaredFields(final Class cls) throws Exception {
            return cls.getDeclaredFields();
        }

        @Override
        boolean makeAccessible(final AccessibleObject obj) {
            if (!obj.isAccessible()) {
                try {
                    AccessController.doPrivileged(new PrivilegedAction() {
                        @Override
                        public Void run() {
                            obj.setAccessible(true);
                            return null;
                        }
                    });
                } catch (final Throwable e) {
                    try {
                        obj.setAccessible(true);
                    } catch (final Throwable e2) {
                        return false;
                    }
                }
                return obj.isAccessible();
            }
            return true;
        }

        @Override
        Object getField(final Object object, final Field field) throws Exception {
            makeAccessible(field);
            return field.get(object);
        }

        @Override
        void setField(final Object object, final Field field, final Object value) throws Exception {
            makeAccessible(field);
            field.set(object, value);
        }

        @Override
        Object getStaticField(final Field field) throws Exception {
            makeAccessible(field);
            return field.get(null);
        }

        @Override
        void setStaticField(final Field field, final Object value) throws Exception {
            makeAccessible(field);
            field.set(null, value);
        }

        @Override
        Object invokeMethod(final Object object, final Method method, final Object... args) throws Exception {
            makeAccessible(method);
            return method.invoke(object, args);
        }

        @Override
        Object invokeStaticMethod(final Method method, final Object... args) throws Exception {
            makeAccessible(method);
            return method.invoke(null, args);
        }
    }

    /**
     * Narcissus reflection driver (uses the Narcissus
     * library, if it is available, which allows access to non-public fields and methods, circumventing
     * encapsulation and visibility controls via JNI).
     */
    private static class NarcissusReflectionDriver extends ReflectionDriver {
        private final Map> methodNameToMethods = new HashMap<>();
        private final Class narcissusClass;
        private final Method getDeclaredMethods;
        private final Method findClass;
        private final Method getDeclaredConstructors;
        private final Method getDeclaredFields;
        private final Method getField;
        private final Method setField;
        private final Method getStaticField;
        private final Method setStaticField;
        private final Method invokeMethod;
        private final Method invokeStaticMethod;

        private Method findIndexedMethod(final String methodName, final Class... paramTypes)
                throws NoSuchMethodException {
            final List methods = methodNameToMethods.get(methodName);
            if (methods != null) {
                for (final Method method : methods) {
                    if (Arrays.equals(method.getParameterTypes(), paramTypes)) {
                        return method;
                    }
                }
            }
            throw new NoSuchMethodException(methodName);
        }

        private void indexMethods(final List methods) {
            // Index Narcissus methods by name
            for (final Method method : methods) {
                List methodsForName = methodNameToMethods.get(method.getName());
                if (methodsForName == null) {
                    methodNameToMethods.put(method.getName(), methodsForName = new ArrayList<>());
                }
                methodsForName.add(method);
            }
        }

        NarcissusReflectionDriver() throws Exception {
            // Load Narcissus class via reflection, so that there is no runtime dependency
            final StandardReflectionDriver drv = new StandardReflectionDriver();
            narcissusClass = drv.findClass("io.github.toolfactory.narcissus.Narcissus");
            if (!(Boolean) drv.getStaticField(drv.findField(narcissusClass, "libraryLoaded"))) {
                throw new IllegalArgumentException("Could not load Narcissus native library");
            }

            // Look up needed methods
            indexMethods(drv.enumerateMethods(narcissusClass));
            findClass = findIndexedMethod("findClass", String.class);
            getDeclaredMethods = findIndexedMethod("getDeclaredMethods", Class.class);
            getDeclaredConstructors = findIndexedMethod("getDeclaredConstructors", Class.class);
            getDeclaredFields = findIndexedMethod("getDeclaredFields", Class.class);
            getField = findIndexedMethod("getField", Object.class, Field.class);
            setField = findIndexedMethod("setField", Object.class, Field.class, Object.class);
            getStaticField = findIndexedMethod("getStaticField", Field.class);
            setStaticField = findIndexedMethod("setStaticField", Field.class, Object.class);
            invokeMethod = findIndexedMethod("invokeMethod", Object.class, Method.class, Object[].class);
            invokeStaticMethod = findIndexedMethod("invokeStaticMethod", Method.class, Object[].class);
        }

        @Override
        boolean makeAccessible(final AccessibleObject accessibleObject) {
            return true;
        }

        @Override
        Class findClass(final String className) throws Exception {
            return (Class) findClass.invoke(null, className);
        }

        @Override
        Method[] getDeclaredMethods(final Class cls) throws Exception {
            return (Method[]) getDeclaredMethods.invoke(null, cls);
        }

        @SuppressWarnings("unchecked")
        @Override
         Constructor[] getDeclaredConstructors(final Class cls) throws Exception {
            return (Constructor[]) getDeclaredConstructors.invoke(null, cls);
        }

        @Override
        Field[] getDeclaredFields(final Class cls) throws Exception {
            return (Field[]) getDeclaredFields.invoke(null, cls);
        }

        @Override
        Object getField(final Object object, final Field field) throws Exception {
            return getField.invoke(null, object, field);
        }

        @Override
        void setField(final Object object, final Field field, final Object value) throws Exception {
            setField.invoke(null, object, field, value);
        }

        @Override
        Object getStaticField(final Field field) throws Exception {
            return getStaticField.invoke(null, field);
        }

        @Override
        void setStaticField(final Field field, final Object value) throws Exception {
            setStaticField.invoke(null, field, value);
        }

        @Override
        Object invokeMethod(final Object object, final Method method, final Object... args) throws Exception {
            return invokeMethod.invoke(null, object, method, args);
        }

        @Override
        Object invokeStaticMethod(final Method method, final Object... args) throws Exception {
            return invokeStaticMethod.invoke(null, method, args);
        }
    }

    /**
     * Constructor.
     */
    private ReflectionUtils() {
        // Cannot be constructed
    }

    /**
     * Get the value of the named field in the class of the given object or any of its superclasses. If an exception
     * is thrown while trying to read the field, and throwException is true, then IllegalArgumentException is thrown
     * wrapping the cause, otherwise this will return null. If passed a null object, returns null unless
     * throwException is true, then throws IllegalArgumentException.
     * 
     * @param obj
     *            The object.
     * @param fieldName
     *            The field name.
     * @param throwException
     *            If true, throw an exception if the field value could not be read.
     * @return The field value.
     * @throws IllegalArgumentException
     *             If the field value could not be read.
     */
    public static Object getFieldVal(final Object obj, final String fieldName, final boolean throwException)
            throws IllegalArgumentException {
        if (obj == null || fieldName == null) {
            if (throwException) {
                throw new NullPointerException();
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.getField(obj, reflectionDriver.findField(obj.getClass(), fieldName));
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException(
                        "Can't read field " + obj.getClass().getName() + "." + fieldName + ": " + e);
            }
        }
        return null;
    }

    /**
     * Get the value of the named static field in the given class or any of its superclasses. If an exception is
     * thrown while trying to read the field value, and throwException is true, then IllegalArgumentException is
     * thrown wrapping the cause, otherwise this will return null. If passed a null class reference, returns null
     * unless throwException is true, then throws IllegalArgumentException.
     * 
     * @param cls
     *            The class.
     * @param fieldName
     *            The field name.
     * @param throwException
     *            If true, throw an exception if the field value could not be read.
     * @return The field value.
     * @throws IllegalArgumentException
     *             If the field value could not be read.
     */
    public static Object getStaticFieldVal(final Class cls, final String fieldName, final boolean throwException)
            throws IllegalArgumentException {
        if (cls == null || fieldName == null) {
            if (throwException) {
                throw new NullPointerException();
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.getStaticField(reflectionDriver.findField(cls, fieldName));
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException(
                        "Can't read static field " + cls.getName() + "." + fieldName + ": " + e);
            }
        }
        return null;
    }

    /**
     * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to
     * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause,
     * otherwise this will return null. If passed a null object, returns null unless throwException is true, then
     * throws IllegalArgumentException.
     * 
     * @param obj
     *            The object.
     * @param methodName
     *            The method name.
     * @param throwException
     *            If true, throw an exception if the field value could not be read.
     * @return The result of the method invocation.
     * @throws IllegalArgumentException
     *             If the method could not be invoked.
     */
    public static Object invokeMethod(final Object obj, final String methodName, final boolean throwException)
            throws IllegalArgumentException {
        if (obj == null || methodName == null) {
            if (throwException) {
                throw new IllegalArgumentException("Unexpected null argument");
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.invokeMethod(obj, reflectionDriver.findMethod(obj.getClass(), methodName));
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException("Method \"" + methodName + "\" could not be invoked: " + e);
            }
            return null;
        }
    }

    /**
     * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to
     * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause,
     * otherwise this will return null. If passed a null object, returns null unless throwException is true, then
     * throws IllegalArgumentException.
     * 
     * @param obj
     *            The object.
     * @param methodName
     *            The method name.
     * @param argType
     *            The type of the method argument.
     * @param param
     *            The parameter value to use when invoking the method.
     * @param throwException
     *            Whether to throw an exception on failure.
     * @return The result of the method invocation.
     * @throws IllegalArgumentException
     *             If the method could not be invoked.
     */
    public static Object invokeMethod(final Object obj, final String methodName, final Class argType,
            final Object param, final boolean throwException) throws IllegalArgumentException {
        if (obj == null || methodName == null || argType == null) {
            if (throwException) {
                throw new IllegalArgumentException("Unexpected null argument");
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.invokeMethod(obj,
                    reflectionDriver.findMethod(obj.getClass(), methodName, argType), param);
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException("Method \"" + methodName + "\" could not be invoked: " + e);
            }
            return null;
        }
    }

    /**
     * Invoke the named static method. If an exception is thrown while trying to call the method, and throwException
     * is true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If
     * passed a null class reference, returns null unless throwException is true, then throws
     * IllegalArgumentException.
     * 
     * @param cls
     *            The class.
     * @param methodName
     *            The method name.
     * @param throwException
     *            Whether to throw an exception on failure.
     * @return The result of the method invocation.
     * @throws IllegalArgumentException
     *             If the method could not be invoked.
     */
    public static Object invokeStaticMethod(final Class cls, final String methodName,
            final boolean throwException) throws IllegalArgumentException {
        if (cls == null || methodName == null) {
            if (throwException) {
                throw new IllegalArgumentException("Unexpected null argument");
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.invokeStaticMethod(reflectionDriver.findMethod(cls, methodName));
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException(
                        "Static method \"" + methodName + "\" could not be invoked: " + e);
            }
            return null;
        }
    }

    /**
     * Invoke the named static method. If an exception is thrown while trying to call the method, and throwException
     * is true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If
     * passed a null class reference, returns null unless throwException is true, then throws
     * IllegalArgumentException.
     * 
     * @param cls
     *            The class.
     * @param methodName
     *            The method name.
     * @param argType
     *            The type of the method argument.
     * @param param
     *            The parameter value to use when invoking the method.
     * @param throwException
     *            Whether to throw an exception on failure.
     * @return The result of the method invocation.
     * @throws IllegalArgumentException
     *             If the method could not be invoked.
     */
    public static Object invokeStaticMethod(final Class cls, final String methodName, final Class argType,
            final Object param, final boolean throwException) throws IllegalArgumentException {
        if (cls == null || methodName == null || argType == null) {
            if (throwException) {
                throw new IllegalArgumentException("Unexpected null argument");
            } else {
                return null;
            }
        }
        try {
            return reflectionDriver.invokeStaticMethod(reflectionDriver.findMethod(cls, methodName, argType),
                    param);
        } catch (final Throwable e) {
            if (throwException) {
                throw new IllegalArgumentException(
                        "Static method \"" + methodName + "\" could not be invoked: " + e);
            }
            return null;
        }
    }

    /**
     * Call Class.forName(className), but return null if any exception is thrown.
     * 
     * @param className
     *            The class name to load.
     * @return The class of the requested name, or null if an exception was thrown while trying to load the class.
     */
    public static Class classForNameOrNull(final String className) {
        try {
            return reflectionDriver.findClass(className);
        } catch (final Throwable e) {
            return null;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy