ReflectionUtils.java

/*
 * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.inject.weld.internal.injector;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.glassfish.jersey.inject.weld.internal.type.GenericArrayTypeImpl;
import org.glassfish.jersey.inject.weld.internal.type.ParameterizedTypeImpl;
import org.glassfish.jersey.internal.util.collection.ImmutableCollectors;

/**
 * Utility class for getting the information from class using Reflection API.
 *
 * @author John Wells (john.wells at oracle.com)
 * @author Petr Bouda
 */
class ReflectionUtils {

    private static final Logger LOGGER = Logger.getLogger(ReflectionUtils.class.getName());

    static Set<Field> getAllFields(Class<?> clazz) {
        if (clazz == null || Object.class.equals(clazz)) {
            return Collections.emptySet();
        }
        if (clazz.isInterface()) {
            return Collections.emptySet();
        }

        Set<Field> retVal = new LinkedHashSet<>();
        retVal.addAll(getDeclaredField(clazz));
        retVal.addAll(getAllFields(clazz.getSuperclass()));
        return retVal;
    }

    /**
     * Gets the EXACT set of fields on this class only. No subclasses. So this set should be considered RAW and has not taken into
     * account any subclasses.
     *
     * @param clazz The class to examine.
     * @return all declared fields.
     */
    private static Set<Field> getDeclaredField(final Class<?> clazz) {
        return Arrays.stream(secureGetDeclaredFields(clazz)).collect(ImmutableCollectors.toImmutableLinkedSet());
    }

    private static Field[] secureGetDeclaredFields(final Class<?> clazz) {
        return AccessController.doPrivileged((PrivilegedAction<Field[]>) clazz::getDeclaredFields);
    }

    /**
     * Resolves the generic type of a field given the actual class being instantiated
     *
     * @param topclass The instantiation class.  Must not be null
     * @param field    The non-null field whose type to resolve
     * @return The resolved field type by way of its subclasses.  Will not return
     * null, but may return the original fields generic type
     */
    static Type resolveField(Class<?> topclass, Field field) {
        return resolveMember(topclass, field.getGenericType(), field.getDeclaringClass());
    }

    /**
     * Resolves the generic type of a type and declaring class given the actual class being instantiated
     *
     * @param topclass       The instantiation class.  Must not be null
     * @param lookingForType The type to resolve.  Must not be null
     * @param declaringClass The class of the entity declaring the lookingForType. Must not be null
     * @return The resolved type by way of its subclasses.  Will not return null but may
     * return lookingForType if it could not be further resolved
     */
    private static Type resolveMember(Class<?> topclass, Type lookingForType, Class<?> declaringClass) {
        Map<String, Type> typeArguments = typesFromSubClassToDeclaringClass(topclass, declaringClass);
        if (typeArguments == null) {
            return lookingForType;
        }

        if (lookingForType instanceof ParameterizedType) {
            return fixTypeVariables((ParameterizedType) lookingForType, typeArguments);
        }

        if (lookingForType instanceof GenericArrayType) {
            return fixGenericArrayTypeVariables((GenericArrayType) lookingForType, typeArguments);
        }

        if (!(lookingForType instanceof TypeVariable)) {
            return lookingForType;
        }

        TypeVariable<?> tv = (TypeVariable<?>) lookingForType;
        String typeVariableName = tv.getName();

        Type retVal = typeArguments.get(typeVariableName);
        if (retVal == null) {
            return lookingForType;
        }

        if (retVal instanceof Class) {
            return retVal;
        }

        if (retVal instanceof ParameterizedType) {
            return fixTypeVariables((ParameterizedType) retVal, typeArguments);
        }

        if (retVal instanceof GenericArrayType) {
            return fixGenericArrayTypeVariables((GenericArrayType) retVal, typeArguments);
        }

        return retVal;
    }

    private static Map<String, Type> typesFromSubClassToDeclaringClass(Class<?> topClass, Class<?> declaringClass) {
        if (topClass.equals(declaringClass)) {
            return null;
        }

        Type superType = topClass.getGenericSuperclass();
        Class<?> superClass = getRawClass(superType);

        while (superType != null && superClass != null) {
            if (!(superType instanceof ParameterizedType)) {
                // superType MUST be a Class in this case
                if (superClass.equals(declaringClass)) {
                    return null;
                }

                superType = superClass.getGenericSuperclass();
                superClass = getRawClass(superType);

                continue;
            }

            ParameterizedType superPT = (ParameterizedType) superType;

            Map<String, Type> typeArguments = getTypeArguments(superClass, superPT);

            if (superClass.equals(declaringClass)) {
                return typeArguments;
            }

            superType = superClass.getGenericSuperclass();
            superClass = getRawClass(superType);

            if (superType instanceof ParameterizedType) {
                superType = fixTypeVariables((ParameterizedType) superType, typeArguments);
            }
        }

        throw new AssertionError(topClass.getName() + " is not the same as or a subclass of " + declaringClass.getName());
    }

    /**
     * Gets a mapping of type variable names of the raw class to type arguments of the parameterized type.
     */
    private static Map<String, Type> getTypeArguments(Class<?> rawClass, ParameterizedType type) {
        Map<String, Type> typeMap = new HashMap<>();
        Type[] typeArguments = type.getActualTypeArguments();

        int i = 0;
        for (TypeVariable<?> typeVariable : rawClass.getTypeParameters()) {
            typeMap.put(typeVariable.getName(), typeArguments[i++]);
        }
        return typeMap;
    }

    /**
     * Replace any TypeVariables in the given type's arguments with
     * the actual argument types.  Return the given type if no replacing
     * is required.
     */
    private static Type fixTypeVariables(ParameterizedType type, Map<String, Type> typeArgumentsMap) {
        Type[] newTypeArguments = getNewTypeArguments(type, typeArgumentsMap);
        if (newTypeArguments != null) {
            return new ParameterizedTypeImpl(type.getRawType(), newTypeArguments);
        }
        return type;
    }

    /**
     * Get a new array of type arguments for the given ParameterizedType, replacing any TypeVariables with
     * actual types.  The types should be found in the given arguments map, keyed by variable name.  Return
     * null if no arguments needed to be replaced.
     */
    private static Type[] getNewTypeArguments(final ParameterizedType type,
            final Map<String, Type> typeArgumentsMap) {

        Type[] typeArguments = type.getActualTypeArguments();
        Type[] newTypeArguments = new Type[typeArguments.length];
        boolean newArgsNeeded = false;

        int i = 0;
        for (Type argType : typeArguments) {
            if (argType instanceof TypeVariable) {
                newTypeArguments[i] = typeArgumentsMap.get(((TypeVariable<?>) argType).getName());
                newArgsNeeded = true;
            } else if (argType instanceof ParameterizedType) {
                ParameterizedType original = (ParameterizedType) argType;

                Type[] internalTypeArgs = getNewTypeArguments(original, typeArgumentsMap);
                if (internalTypeArgs != null) {
                    newTypeArguments[i] = new ParameterizedTypeImpl(original.getRawType(), internalTypeArgs);
                    newArgsNeeded = true;
                } else {
                    newTypeArguments[i] = argType;
                }
            } else if (argType instanceof GenericArrayType) {
                GenericArrayType gat = (GenericArrayType) argType;

                Type internalTypeArg = getNewTypeArrayArguments(gat, typeArgumentsMap);
                if (internalTypeArg != null) {
                    if (internalTypeArg instanceof Class<?>) {
                        newTypeArguments[i] = getArrayOfType((Class<?>) internalTypeArg);
                        newArgsNeeded = true;
                    } else if ((internalTypeArg instanceof ParameterizedType)
                            && (((ParameterizedType) internalTypeArg).getRawType() instanceof Class<?>)) {
                        ParameterizedType pt = (ParameterizedType) internalTypeArg;

                        newTypeArguments[i] = getArrayOfType((Class<?>) pt.getRawType());
                        newArgsNeeded = true;
                    } else {
                        newTypeArguments[i] = new GenericArrayTypeImpl(internalTypeArg);
                        newArgsNeeded = true;
                    }
                } else {
                    newTypeArguments[i] = argType;
                }
            } else {
                newTypeArguments[i] = argType;
            }

            i++;
        }

        return newArgsNeeded ? newTypeArguments : null;
    }

    /**
     * Get a new Type for a GenericArrayType, replacing any TypeVariables with
     * actual types.  The types should be found in the given arguments map, keyed by variable name.  Return
     * null if no arguments needed to be replaced.
     */
    private static Type getNewTypeArrayArguments(final GenericArrayType gat,
            final Map<String, Type> typeArgumentsMap) {

        Type typeArgument = gat.getGenericComponentType();

        if (typeArgument instanceof TypeVariable) {
            return typeArgumentsMap.get(((TypeVariable<?>) typeArgument).getName());
        }

        if (typeArgument instanceof ParameterizedType) {
            ParameterizedType original = (ParameterizedType) typeArgument;

            Type[] internalTypeArgs = getNewTypeArguments(original, typeArgumentsMap);
            if (internalTypeArgs != null) {
                return new ParameterizedTypeImpl(original.getRawType(), internalTypeArgs);
            }

            return original;
        }

        if (typeArgument instanceof GenericArrayType) {
            GenericArrayType original = (GenericArrayType) typeArgument;

            Type internalTypeArg = getNewTypeArrayArguments(original, typeArgumentsMap);
            if (internalTypeArg != null) {
                if (internalTypeArg instanceof Class<?>) {
                    return getArrayOfType((Class<?>) internalTypeArg);
                }

                if (internalTypeArg instanceof ParameterizedType) {
                    ParameterizedType pt = (ParameterizedType) internalTypeArg;

                    if (pt.getRawType() instanceof Class<?>) {
                        return getArrayOfType((Class<?>) pt.getRawType());
                    }
                }

                return new GenericArrayTypeImpl(internalTypeArg);
            }

            return null;
        }

        return null;
    }

    /**
     * Replace any TypeVariables in the given type's arguments with
     * the actual argument types.  Return the given type if no replacing
     * is required.
     */
    private static Type fixGenericArrayTypeVariables(GenericArrayType type, Map<String, Type> typeArgumentsMap) {
        Type newTypeArgument = getNewTypeArrayArguments(type, typeArgumentsMap);

        if (newTypeArgument != null) {
            if (newTypeArgument instanceof Class<?>) {
                return getArrayOfType((Class<?>) newTypeArgument);
            }

            if (newTypeArgument instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) newTypeArgument;
                if (pt.getRawType() instanceof Class<?>) {
                    return getArrayOfType((Class<?>) pt.getRawType());
                }
            }

            return new GenericArrayTypeImpl(newTypeArgument);
        }

        return type;
    }

    private static Class<?> getArrayOfType(Class<?> type) {
        return Array.newInstance(type, 0).getClass();
    }

    /**
     * Given the type parameter gets the raw type represented
     * by the type, or null if this has no associated raw class
     *
     * @param type The type to find the raw class on
     * @return The raw class associated with this type
     */
    private static Class<?> getRawClass(Type type) {
        if (type == null) {
            return null;
        }

        if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType) type).getGenericComponentType();

            if (!(componentType instanceof ParameterizedType) && !(componentType instanceof Class)) {
                // type variable is not supported
                return null;
            }

            Class<?> rawComponentClass = getRawClass(componentType);

            String forNameName = "[L" + rawComponentClass.getName() + ";";
            try {
                return Class.forName(forNameName);
            } catch (Throwable th) {
                // ignore, but return null
                return null;
            }
        }

        if (type instanceof Class) {
            return (Class<?>) type;
        }

        if (type instanceof ParameterizedType) {
            Type rawType = ((ParameterizedType) type).getRawType();
            if (rawType instanceof Class) {
                return (Class<?>) rawType;
            }
        }

        return null;
    }

    /**
     * Sets the given field to the given value
     *
     * @param field    The non-null field to set
     * @param instance The non-null instance to set into
     * @param value    The value to which the field should be set
     * @throws Throwable If there was some exception while setting the field
     */
    static void setField(Field field, Object instance, Object value) throws Throwable {
        setAccessible(field);

        try {
            field.set(instance, value);
        } catch (IllegalArgumentException | IllegalAccessException ex) {
            LOGGER.warning(String.format("Failed during setting a value into Class: %s, Field: %s",
                    field.getDeclaringClass().getName(), field.getName()));
            throw ex;
        }
    }

    /**
     * Sets this accessible object to be accessible using the permissions of
     * the hk2-locator bundle (which will need the required grant)
     *
     * @param ao The object to change
     */
    private static void setAccessible(final AccessibleObject ao) {
        if (ao.isAccessible()) {
            return;
        }

        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            ao.setAccessible(true);
            return null;
        });
    }
}