ClassKit.java

package space.sunqian.common.reflect;

import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.annotations.RetainedParam;
import space.sunqian.common.Fs;
import space.sunqian.common.base.exception.UnknownPrimitiveTypeException;
import space.sunqian.common.base.system.JvmKit;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Iterator;
import java.util.Objects;

/**
 * Utilities for {@link Class}.
 *
 * @author sunqian
 */
public class ClassKit {

    /**
     * Returns the field of the specified name from the given class, or {@code null} if not found. This method first
     * uses {@link Class#getField(String)}. If not found, it will use {@link Class#getDeclaredField(String)} to try
     * again.
     *
     * @param cls  the given class
     * @param name the specified field name
     * @return the field of the specified name from the given class, or {@code null} if not found
     */
    public static @Nullable Field getField(@Nonnull Class<?> cls, @Nonnull String name) {
        return getField(cls, name, true);
    }

    /**
     * Returns the field of the specified name from the given class, or {@code null} if not found. This method first
     * uses {@link Class#getField(String)}. If not found and the {@code searchDeclared} is {@code true}, it will use
     * {@link Class#getDeclaredField(String)} to try again.
     *
     * @param cls            the given class
     * @param name           the specified field name
     * @param searchDeclared specifies whether searches declared fields
     * @return the field of the specified name from the given class, or {@code null} if not found
     */
    public static @Nullable Field getField(@Nonnull Class<?> cls, @Nonnull String name, boolean searchDeclared) {
        try {
            return cls.getField(name);
        } catch (NoSuchFieldException e) {
            if (searchDeclared) {
                try {
                    return cls.getDeclaredField(name);
                } catch (NoSuchFieldException ex) {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * Returns the field of the specified name from the given class, or {@code null} if not found.
     * <p>
     * This method searches via {@link Class#getField(String)}. If the field is not found, then this method will use
     * {@link Class#getDeclaredField(String)} to search again. If the field is still not found, then this method will
     * traverse the hierarchy of superclasses and interfaces of the given class to search via
     * {@link Class#getDeclaredField(String)}.
     *
     * @param cls  the given class
     * @param name the specified field name
     * @return the field of the specified name from the given class, or {@code null} if not found
     */
    public static @Nullable Field searchField(@Nonnull Class<?> cls, @Nonnull String name) {
        Field field = getField(cls, name);
        if (field != null) {
            return field;
        }
        Iterator<Class<?>> supertypesAndInterfaces = toSupertypesAndInterfaces(cls);
        while (supertypesAndInterfaces.hasNext()) {
            Class<?> next = supertypesAndInterfaces.next();
            @Nullable Field nextField = searchField(next, name);
            if (nextField != null) {
                return nextField;
            }
        }
        return null;
    }

    /**
     * Returns the method of the specified name and parameter types from the given class, or {@code null} if not found.
     * This method first uses {@link Class#getMethod(String, Class[])}. If not found, it will use
     * {@link Class#getDeclaredMethod(String, Class[])} to try again.
     *
     * @param cls            the given class
     * @param name           the specified method name
     * @param parameterTypes the specified parameter types
     * @return the method of the specified name and parameter types from the given class, or {@code null} if not found
     */
    public static @Nullable Method getMethod(
        @Nonnull Class<?> cls,
        @Nonnull String name,
        @Nonnull Class<?> @Nonnull @RetainedParam [] parameterTypes
    ) {
        return getMethod(cls, name, parameterTypes, true);
    }

    /**
     * Returns the method of the specified name and parameter types from the given class, or {@code null} if not found.
     * This method first uses {@link Class#getMethod(String, Class[])}. If not found and the {@code searchDeclared} is
     * {@code true}, it will use {@link Class#getDeclaredMethod(String, Class[])} to try again.
     *
     * @param cls            the given class
     * @param name           the specified method name
     * @param parameterTypes the specified parameter types
     * @param searchDeclared specifies whether searches declared methods
     * @return the method of the specified name and parameter types from the given class, or {@code null} if not found
     */
    public static @Nullable Method getMethod(
        @Nonnull Class<?> cls,
        @Nonnull String name,
        @Nonnull Class<?> @Nonnull @RetainedParam [] parameterTypes,
        boolean searchDeclared
    ) {
        try {
            return cls.getMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            if (searchDeclared) {
                try {
                    return cls.getDeclaredMethod(name, parameterTypes);
                } catch (NoSuchMethodException ex) {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * Returns the method of the specified name and parameter types from the given class, or {@code null} if not found.
     * <p>
     * This method searches via {@link Class#getMethod(String, Class[])}. If the method is not found, then this method
     * will use {@link Class#getDeclaredMethod(String, Class[])} to search again. If the method is still not found, then
     * this method will traverse the hierarchy of superclasses and interfaces of the given class to search via
     * {@link Class#getDeclaredMethod(String, Class[])}.
     *
     * @param cls            the given class
     * @param name           the specified method name
     * @param parameterTypes the specified parameter types
     * @return the method of the specified name and parameter types from the given class, or {@code null} if not found
     */
    public static @Nullable Method searchMethod(
        @Nonnull Class<?> cls,
        @Nonnull String name,
        @Nonnull Class<?> @Nonnull @RetainedParam [] parameterTypes
    ) {
        Method method = getMethod(cls, name, parameterTypes);
        if (method != null) {
            return method;
        }
        Iterator<Class<?>> supertypesAndInterfaces = toSupertypesAndInterfaces(cls);
        while (supertypesAndInterfaces.hasNext()) {
            Class<?> next = supertypesAndInterfaces.next();
            @Nullable Method nextMethod = searchMethod(next, name, parameterTypes);
            if (nextMethod != null) {
                return nextMethod;
            }
        }
        return null;
    }

    private static @Nonnull Iterator<Class<?>> toSupertypesAndInterfaces(@Nonnull Class<?> cls) {
        return new Iterator<Class<?>>() {

            private int index = -1;
            private Class<?> @Nullable [] interfaces;
            private @Nullable Class<?> next = getNext();

            @Override
            public boolean hasNext() {
                return next != null;
            }

            @Override
            public Class<?> next() {
                Class<?> result = next;
                next = getNext();
                return result;
            }

            private @Nullable Class<?> getNext() {
                if (index == -1) {
                    index++;
                    Class<?> superclass = cls.getSuperclass();
                    if (superclass != null) {
                        return superclass;
                    }
                }
                if (interfaces == null) {
                    interfaces = cls.getInterfaces();
                }
                if (index < interfaces.length) {
                    return interfaces[index++];
                }
                return null;
            }
        };
    }

    /**
     * Returns the constructor of the specified parameter types from the given class, or {@code null} if not found. This
     * method first uses {@link Class#getConstructor(Class[])}. If not found, it will use
     * {@link Class#getDeclaredConstructor(Class[])} to try again.
     *
     * @param cls            the given class
     * @param parameterTypes the specified parameter types
     * @return the constructor of the specified parameter types from the given class, or {@code null} if not found
     */
    public static @Nullable Constructor<?> getConstructor(
        @Nonnull Class<?> cls,
        @Nonnull Class<?> @Nonnull @RetainedParam [] parameterTypes
    ) {
        return getConstructor(cls, parameterTypes, true);
    }

    /**
     * Returns the constructor of the specified parameter types from the given class, or {@code null} if not found. This
     * method first uses {@link Class#getConstructor(Class[])}. If not found and the {@code searchDeclared} is
     * {@code true}, it will use {@link Class#getDeclaredConstructor(Class[])} to try again.
     *
     * @param cls            the given class
     * @param parameterTypes the specified parameter types
     * @param searchDeclared specifies whether searches declared constructors
     * @return the constructor of the specified parameter types from the given class, or {@code null} if not found
     */
    public static @Nullable Constructor<?> getConstructor(
        @Nonnull Class<?> cls,
        @Nonnull Class<?> @Nonnull @RetainedParam [] parameterTypes,
        boolean searchDeclared
    ) {
        try {
            return cls.getConstructor(parameterTypes);
        } catch (NoSuchMethodException e) {
            if (searchDeclared) {
                try {
                    return cls.getDeclaredConstructor(parameterTypes);
                } catch (NoSuchMethodException ex) {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * Returns a new instance for the given class name with the empty constructor, may be {@code null} if fails.
     * <p>
     * This method first uses {@link #classForName(String, ClassLoader)} to get the class of the given class name, then
     * call {@link #newInstance(Class)} to create a new instance.
     *
     * @param className the given class name
     * @param <T>       the instance's type
     * @return a new instance for the given class name with the empty constructor, may be {@code null} if fails
     */
    public static <T> @Nullable T newInstance(@Nonnull String className) {
        return newInstance(className, null);
    }

    /**
     * Returns a new instance for the given class name with the empty constructor, may be {@code null} if fails.
     * <p>
     * This method first uses {@link #classForName(String, ClassLoader)} to get the class of the given class name, then
     * call {@link #newInstance(Class)} to create a new instance.
     *
     * @param className the given class name
     * @param loader    the given class loader, may be {@code null} if loaded by the default loader
     * @param <T>       the instance's type
     * @return a new instance for the given class name with the empty constructor, may be {@code null} if fails
     */
    public static <T> @Nullable T newInstance(@Nonnull String className, @Nullable ClassLoader loader) {
        @Nullable Class<?> cls = classForName(className, loader);
        if (cls == null) {
            return null;
        }
        return newInstance(cls);
    }

    /**
     * Returns a new instance for the given class with the empty constructor, may be {@code null} if fails.
     *
     * @param <T>  the instance's type
     * @param type the given class
     * @return a new instance for the given class with the empty constructor, may be {@code null} if fails
     */
    public static <T> @Nullable T newInstance(@Nonnull Class<?> type) {
        try {
            Constructor<?> constructor = type.getConstructor();
            return newInstance(constructor);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Creates a new instance with the given constructor and arguments, may be {@code null} if fails.
     *
     * @param constructor the given constructor
     * @param args        the given arguments
     * @param <T>         the instance's type
     * @return a new instance with the given constructor and arguments, may be {@code null} if fails
     */
    public static <T> @Nullable T newInstance(@Nonnull Constructor<?> constructor, Object @Nonnull ... args) {
        try {
            return Fs.as(constructor.newInstance(args));
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns the array class whose component type is the specified type, may be {@code null} if fails. Note
     * {@link TypeVariable} and {@link WildcardType} are unsupported.
     *
     * @param componentType the specified component type
     * @return the array class whose component type is the specified type, may be {@code null} if fails
     */
    public static @Nullable Class<?> arrayClass(@Nonnull Type componentType) {
        return ArrayClassService.INST.arrayClass(componentType);
    }

    /**
     * Returns the array class name whose component type is the specified type, may be {@code null} if fails.
     *
     * @param componentType the specified component type
     * @return the array class name whose component type is the specified type, may be {@code null} if fails
     */
    public static @Nullable String arrayClassName(@Nonnull Class<?> componentType) {
        if (componentType.isArray()) {
            return "[" + componentType.getName();
        }
        if (componentType.isPrimitive()) {
            // No void[]
            if (Objects.equals(componentType, void.class)) {
                return null;
            }
            return "[" + JvmKit.toDescriptor(componentType);
        }
        return "[L" + componentType.getName() + ";";
    }

    /**
     * Returns the wrapper class if the given class is primitive, else return the given class itself.
     *
     * @param cls the given class
     * @return the wrapper class if the given class is primitive, else return the given class itself
     */
    public static @Nonnull Class<?> wrapperClass(@Nonnull Class<?> cls) {
        if (!cls.isPrimitive()) {
            return cls;
        }
        return wrapperPrimitive(cls);
    }

    private static Class<?> wrapperPrimitive(Class<?> cls) {
        if (Objects.equals(cls, boolean.class)) {
            return Boolean.class;
        }
        if (Objects.equals(cls, byte.class)) {
            return Byte.class;
        }
        if (Objects.equals(cls, short.class)) {
            return Short.class;
        }
        if (Objects.equals(cls, char.class)) {
            return Character.class;
        }
        if (Objects.equals(cls, int.class)) {
            return Integer.class;
        }
        if (Objects.equals(cls, long.class)) {
            return Long.class;
        }
        if (Objects.equals(cls, float.class)) {
            return Float.class;
        }
        if (Objects.equals(cls, double.class)) {
            return Double.class;
        }
        if (Objects.equals(cls, void.class)) {
            return Void.class;
        }
        throw new UnknownPrimitiveTypeException(cls);
    }

    /**
     * Returns whether the current runtime exists the class specified by the given class name and loaded by the default
     * class loader.
     *
     * @param className the given class name
     * @return whether the current runtime exists the class specified by the given class name and loaded by the default
     * class loader
     */
    public static boolean classExists(@Nonnull String className) {
        return classExists(className, null);
    }

    /**
     * Returns whether the current runtime exists the class specified by the given class name and loaded by the given
     * class loader.
     *
     * @param className the given class name
     * @param loader    the given class loader, may be {@code null} if loaded by the default loader
     * @return whether the current runtime exists the class specified by the given class name and loaded by the given
     * class loader
     */
    public static boolean classExists(@Nonnull String className, @Nullable ClassLoader loader) {
        return classForName(className, loader) != null;
    }

    /**
     * Returns the {@link Class} object whose name is the given name. This method is equivalent to
     * {@code classForName(name, null)}.
     *
     * @param name the given name of the class or interface
     * @return the {@link Class} object whose name is the given name, or {@code null} if loading fails
     * @see #classForName(String, ClassLoader)
     */
    public static @Nullable Class<?> classForName(@Nonnull String name) {
        return classForName(name, null);
    }

    /**
     * Returns the {@link Class} object whose name is the given name. This method calls {@link Class#forName(String)} if
     * the given class loader is {@code null}, or {@link Class#forName(String, boolean, ClassLoader)} if not.
     *
     * @param name   the given name of the class or interface
     * @param loader the given class loader, may be {@code null}
     * @return the {@link Class} object whose name is the given name, or {@code null} if loading fails
     */
    public static @Nullable Class<?> classForName(@Nonnull String name, @Nullable ClassLoader loader) {
        try {
            return loader == null ? Class.forName(name) : Class.forName(name, true, loader);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Returns a new {@link BytesClassLoader} instance.
     *
     * @return a new {@link BytesClassLoader} instance
     */
    public static @Nonnull BytesClassLoader newClassLoader() {
        return new BytesClassLoader();
    }

    /**
     * Returns whether the given member is static.
     *
     * @param member the given member
     * @return whether the given member is static
     */
    public static boolean isStatic(@Nonnull Member member) {
        return Modifier.isStatic(member.getModifiers());
    }

    /**
     * Returns whether the given member is public.
     *
     * @param member the given member
     * @return whether the given member is public
     */
    public static boolean isPublic(@Nonnull Member member) {
        return Modifier.isPublic(member.getModifiers());
    }

    /**
     * Returns whether the given member is protected.
     *
     * @param member the given member
     * @return whether the given member is protected
     */
    public static boolean isProtected(@Nonnull Member member) {
        return Modifier.isProtected(member.getModifiers());
    }

    /**
     * Returns whether the given member is private.
     *
     * @param member the given member
     * @return whether the given member is private
     */
    public static boolean isPrivate(@Nonnull Member member) {
        return Modifier.isPrivate(member.getModifiers());
    }

    /**
     * Returns whether the given member is package-private (no modifies).
     *
     * @param member the given member
     * @return whether the given member is package-private (no modifies)
     */
    public static boolean isPackagePrivate(@Nonnull Member member) {
        int mod = member.getModifiers();
        return !Modifier.isPublic(mod) && !Modifier.isProtected(mod) && !Modifier.isPrivate(mod);
    }

    /**
     * Returns whether the given class is static.
     *
     * @param cls the given class
     * @return whether the given class is static
     */
    public static boolean isStatic(@Nonnull Class<?> cls) {
        return Modifier.isStatic(cls.getModifiers());
    }

    /**
     * Returns whether the given class is public.
     *
     * @param cls the given class
     * @return whether the given class is public
     */
    public static boolean isPublic(@Nonnull Class<?> cls) {
        return Modifier.isPublic(cls.getModifiers());
    }

    /**
     * Returns whether the given class is protected.
     *
     * @param cls the given class
     * @return whether the given class is protected
     */
    public static boolean isProtected(@Nonnull Class<?> cls) {
        return Modifier.isProtected(cls.getModifiers());
    }

    /**
     * Returns whether the given class is private.
     *
     * @param cls the given class
     * @return whether the given class is private
     */
    public static boolean isPrivate(@Nonnull Class<?> cls) {
        return Modifier.isPrivate(cls.getModifiers());
    }

    /**
     * Returns whether the given class is package-private (no modifies).
     *
     * @param cls the given class
     * @return whether the given class is package-private (no modifies)
     */
    public static boolean isPackagePrivate(@Nonnull Class<?> cls) {
        int mod = cls.getModifiers();
        return !Modifier.isPublic(mod) && !Modifier.isProtected(mod) && !Modifier.isPrivate(mod);
    }

    /**
     * Returns whether the given member can be overridden (its owner class is no final, and itself is no final, no
     * static and no private).
     *
     * @param member the given member
     * @return whether the given member can be overridden (its owner class is no final, and itself is no final, no
     * static and no private)
     */
    public static boolean isOverridable(@Nonnull Member member) {
        int modifiers = member.getModifiers();
        if (!isOverridable(modifiers)) {
            return false;
        }
        Class<?> declaringClass = member.getDeclaringClass();
        return !Modifier.isFinal(declaringClass.getModifiers());
    }

    /**
     * Returns whether the given class can be overridden (no final, no static and no private).
     *
     * @param cls the given class
     * @return whether the given class can be overridden (no final, no static and no private)
     */
    public static boolean isOverridable(@Nonnull Class<?> cls) {
        int modifiers = cls.getModifiers();
        return isOverridable(modifiers);
    }

    private static boolean isOverridable(int mod) {
        return !Modifier.isFinal(mod) &&
            !Modifier.isStatic(mod) &&
            !Modifier.isPrivate(mod);
    }

    private ClassKit() {
    }
}