MethodUtils.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.beanutils2;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import org.apache.commons.collections4.map.ConcurrentReferenceHashMap;
import org.apache.commons.collections4.map.ConcurrentReferenceHashMap.ReferenceType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>
 * Utility reflection methods focused on methods in general rather than properties in particular.
 * </p>
 *
 * <h2>Known Limitations</h2>
 * <h3>Accessing Public Methods In A Default Access Superclass</h3>
 * <p>
 * There is an issue when invoking public methods contained in a default access superclass. Reflection locates these methods fine and correctly assigns them as
 * public. However, an {@code IllegalAccessException} is thrown if the method is invoked.
 * </p>
 *
 * <p>
 * {@code MethodUtils} contains a workaround for this situation. It will attempt to call {@code setAccessible} on this method. If this call succeeds, then the
 * method can be invoked as normal. This call will only succeed when the application has sufficient security privileges. If this call fails then a warning will
 * be logged and the method may fail.
 * </p>
 *
 * @see <a href="https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/reflect/MethodUtils.html">Apache Commons Lang MethodUtils</a>
 */
public final class MethodUtils {

    /**
     * Represents the key to looking up a Method by reflection.
     */
    private static final class MethodKey {

        private final Class<?> cls;
        private final String methodName;
        private final Class<?>[] paramTypes;
        private final boolean exact;
        private final int hashCode;

        /**
         * The sole constructor.
         *
         * @param cls        the class to reflect, must not be null.
         * @param methodName the method name to obtain.
         * @param paramTypes the array of classes representing the parameter types.
         * @param exact      whether the match has to be exact.
         */
        public MethodKey(final Class<?> cls, final String methodName, final Class<?>[] paramTypes, final boolean exact) {
            this.cls = Objects.requireNonNull(cls, "cls");
            this.methodName = Objects.requireNonNull(methodName, "methodName");
            this.paramTypes = paramTypes != null ? paramTypes : BeanUtils.EMPTY_CLASS_ARRAY;
            this.exact = exact;
            this.hashCode = methodName.length();
        }

        /**
         * Checks for equality.
         *
         * @param obj object to be tested for equality.
         * @return true, if the object describes the same Method.
         */
        @Override
        public boolean equals(final Object obj) {
            if (!(obj instanceof MethodKey)) {
                return false;
            }
            final MethodKey md = (MethodKey) obj;
            return exact == md.exact && methodName.equals(md.methodName) && cls.equals(md.cls) && Arrays.equals(paramTypes, md.paramTypes);
        }

        /**
         * Returns the string length of method name. I.e. if the hash codes are different, the objects are different. If the hash codes are the same, need to
         * use the equals method to determine equality.
         *
         * @return the string length of method name.
         */
        @Override
        public int hashCode() {
            return hashCode;
        }

        @Override
        public String toString() {
            return "MethodKey [cls=" + cls + ", methodName=" + methodName + ", paramTypes=" + Arrays.toString(paramTypes) + ", exact=" + exact + "]";
        }
    }

    private static final Log LOG = LogFactory.getLog(MethodUtils.class);

    /**
     * Indicates whether methods should be cached for improved performance.
     * <p>
     * Note that when this class is deployed via a shared classloader in a container, this will affect all webapps. However making this configurable per webapp
     * would mean having a map keyed by context classloader which may introduce memory-leak problems.
     * </p>
     */
    private static boolean CACHE_ENABLED = true;

    /**
     * Stores a cache of MethodKey -> Method.
     * <p>
     * The keys into this map only ever exist as temporary variables within methods of this class, and are never exposed to users of this class. This means that
     * this map is used only as a mechanism for limiting the size of the cache, that is, a way to tell the garbage collector that the contents of the cache can
     * be completely garbage-collected whenever it needs the memory. Whether this is a good approach to this problem is doubtful; something like the Commons
     * Collections LRUMap may be more appropriate (though of course selecting an appropriate size is an issue).
     * </p>
     * <p>
     * This static variable is safe even when this code is deployed via a shared class loader because it is keyed via a MethodKey object which has a Class as
     * one of its members and that member is used in the MethodKey.equals method. So two components that load the same class via different class loaders will
     * generate non-equal MethodKey objects and hence end up with different entries in the map.
     * </p>
     */
    // @formatter:off
    private static final Map<MethodKey, Method> CACHE = new ConcurrentReferenceHashMap.Builder<MethodKey, Method>()
            .setKeyReferenceType(ReferenceType.WEAK)
            .setValueReferenceType(ReferenceType.WEAK)
            .get();
    // @formatter:on

    /**
     * Clear the method cache.
     *
     * @return the number of cached methods cleared.
     * @since 1.8.0
     */
    public static synchronized int clearCache() {
        final int size = CACHE.size();
        CACHE.clear();
        return size;
    }

    private static Method computeIfAbsent(final MethodKey key, final Function<MethodKey, ? extends Method> mappingFunction) {
        final Method method = CACHE_ENABLED ? CACHE.computeIfAbsent(key, k -> mappingFunction.apply(k)) : mappingFunction.apply(key);
        if (LOG.isTraceEnabled()) {
            LOG.trace("Matched " + key + " with method: " + method + ", CACHE_ENABLED: " + CACHE_ENABLED);
        }
        return method;
    }

    /**
     * Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
     * {@code null}.
     *
     * @param clazz  The class of the object.
     * @param method The method that we wish to call.
     * @return The accessible method.
     * @since 1.8.0
     */
    public static Method getAccessibleMethod(Class<?> clazz, Method method) {
        // Make sure we have a method to check
        if (method == null) {
            return null;
        }
        // If the requested method is not public we cannot call it
        if (!Modifier.isPublic(method.getModifiers())) {
            return null;
        }
        boolean sameClass = true;
        if (clazz == null) {
            clazz = method.getDeclaringClass();
        } else {
            if (!method.getDeclaringClass().isAssignableFrom(clazz)) {
                throw new IllegalArgumentException(clazz.getName() + " is not assignable from " + method.getDeclaringClass().getName());
            }
            sameClass = clazz.equals(method.getDeclaringClass());
        }
        // If the class is public, we are done
        if (Modifier.isPublic(clazz.getModifiers())) {
            if (!sameClass && !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
                setMethodAccessible(method); // Default access superclass workaround
            }
            return method;
        }
        final String methodName = method.getName();
        final Class<?>[] parameterTypes = method.getParameterTypes();
        // Check the implemented interfaces and subinterfaces
        method = getAccessibleMethodFromInterfaceNest(clazz, methodName, parameterTypes);
        // Check the superclass chain
        if (method == null) {
            method = getAccessibleMethodFromSuperclass(clazz, methodName, parameterTypes);
        }
        return method;
    }

    /**
     * Gets an accessible method (that is, one that can be invoked via reflection) with given name and parameters. If no such method can be found, return
     * {@code null}. This is just a convenient wrapper for {@link #getAccessibleMethod(Method method)}.
     *
     * @param clazz          get method from this class.
     * @param methodName     get method with this name.
     * @param parameterTypes with these parameters types.
     * @return The accessible method.
     */
    public static Method getAccessibleMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
        return computeIfAbsent(new MethodKey(clazz, methodName, parameterTypes, true), k -> {
            try {
                return getAccessibleMethod(clazz, clazz.getMethod(methodName, parameterTypes));
            } catch (final NoSuchMethodException e) {
                return null;
            }
        });
    }

    /**
     * Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
     * {@code null}.
     *
     * @param method The method that we wish to call.
     * @return The accessible method.
     */
    public static Method getAccessibleMethod(final Method method) {
        return method != null ? getAccessibleMethod(method.getDeclaringClass(), method) : null;
    }

    /**
     * Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified method, by scanning through all implemented
     * interfaces and subinterfaces. If no such method can be found, return {@code null}.
     *
     * <p>
     * There isn't any good reason why this method must be private. It is because there doesn't seem any reason why other classes should call this rather than
     * the higher level methods.
     * </p>
     *
     * @param clazz          Parent class for the interfaces to be checked.
     * @param methodName     Method name of the method we wish to call.
     * @param parameterTypes The parameter type signatures.
     */
    private static Method getAccessibleMethodFromInterfaceNest(Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) {
        Method method = null;
        // Search up the superclass chain
        for (; clazz != null; clazz = clazz.getSuperclass()) {
            // Check the implemented interfaces of the parent class
            final Class<?>[] interfaces = clazz.getInterfaces();
            for (final Class<?> anInterface : interfaces) {
                // Is this interface public?
                if (!Modifier.isPublic(anInterface.getModifiers())) {
                    continue;
                }
                // Does the method exist on this interface?
                try {
                    method = anInterface.getDeclaredMethod(methodName, parameterTypes);
                } catch (final NoSuchMethodException e) {
                    /*
                     * Swallow, if no method is found after the loop then this method returns null.
                     */
                }
                if (method != null) {
                    return method;
                }
                // Recursively check our parent interfaces
                method = getAccessibleMethodFromInterfaceNest(anInterface, methodName, parameterTypes);
                if (method != null) {
                    return method;
                }
            }
        }
        // We did not find anything
        return null;
    }

    /**
     * Gets an accessible method (that is, one that can be invoked via reflection) by scanning through the superclasses. If no such method can be found, return
     * {@code null}.
     *
     * @param clazz          Class to be checked.
     * @param methodName     Method name of the method we wish to call.
     * @param parameterTypes The parameter type signatures.
     */
    private static Method getAccessibleMethodFromSuperclass(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) {
        Class<?> parentClazz = clazz.getSuperclass();
        while (parentClazz != null) {
            if (Modifier.isPublic(parentClazz.getModifiers())) {
                try {
                    return parentClazz.getMethod(methodName, parameterTypes);
                } catch (final NoSuchMethodException e) {
                    return null;
                }
            }
            parentClazz = parentClazz.getSuperclass();
        }
        return null;
    }

    /**
     * Gets an accessible method that matches the given name and has compatible parameters. Compatible parameters mean that every method parameter is assignable
     * from the given parameters. In other words, it finds a method with the given name that will take the parameters given.
     *
     * <p>
     * This method is slightly indeterministic since it loops through methods names and return the first matching method.
     * </p>
     *
     * <p>
     * This method can match primitive parameter by passing in wrapper classes. For example, a {@code Boolean</code> will match a primitive <code>boolean}
     * parameter.
     * </p>
     *
     * @param clazz          find method in this class.
     * @param methodName     find method with this name.
     * @param parameterTypes find method with compatible parameters.
     * @return The accessible method.
     */
    public static Method getMatchingAccessibleMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) {
        return computeIfAbsent(new MethodKey(clazz, methodName, parameterTypes, false), k -> {
            final Method method = org.apache.commons.lang3.reflect.MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
            if (method != null) {
                setMethodAccessible(method); // Default access superclass workaround
            }
            return method;
        });
    }

    /**
     * Sets whether methods should be cached for greater performance or not, default is {@code true}.
     *
     * @param cacheMethods {@code true} if methods should be cached for greater performance, otherwise {@code false}
     * @since 1.8.0
     */
    public static synchronized void setCacheMethods(final boolean cacheMethods) {
        CACHE_ENABLED = cacheMethods;
        if (!CACHE_ENABLED) {
            CACHE.clear();
        }
    }

    /**
     * Try to make the method accessible
     *
     * @param method The source arguments
     */
    private static void setMethodAccessible(final Method method) {
        try {
            //
            // XXX Default access superclass workaround
            //
            // When a public class has a default access superclass
            // with public methods, these methods are accessible.
            // Calling them from compiled code works fine.
            //
            // Unfortunately, using reflection to invoke these methods
            // seems to (wrongly) to prevent access even when the method
            // modifier is public.
            //
            // The following workaround solves the problem but will only
            // work from sufficiently privileges code.
            //
            // Better workarounds would be gratefully accepted.
            //
            if (!method.isAccessible()) {
                method.setAccessible(true);
            }
        } catch (final SecurityException e) {
            // log but continue just in case the method.invoke works anyway
            LOG.debug("Cannot setAccessible on method. Therefore cannot use jvm access bug workaround.", e);
        }
    }

    private MethodUtils() {
        // empty
    }
}