/*
 * 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
 *
 * http://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.openjpa.enhance;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.Reflectable;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.lib.util.collections.AbstractReferenceMap.ReferenceStrength;
import org.apache.openjpa.lib.util.concurrent.ConcurrentReferenceHashMap;
import org.apache.openjpa.util.GeneralException;
import org.apache.openjpa.util.UserException;

/**
 * Reflection utilities used to support and augment enhancement.  Used both
 * at enhancement time and at runtime.
 *
 * @author Abe White
 */
public class Reflection {

    private static final Localizer _loc = Localizer.forPackage
        (Reflection.class);

    // Weak HashMap caches of getter/setter/beanProperty methods
    private static Map<Class<?>, Map<String, Method>> getterMethodCache =
        new ConcurrentReferenceHashMap(ReferenceStrength.WEAK, ReferenceStrength.HARD);
    private static Map<Class<?>, Map<String, Method>> setterMethodCache =
        new ConcurrentReferenceHashMap(ReferenceStrength.WEAK, ReferenceStrength.HARD);
    private static Map<Class<?>, Set<String>> beanPropertiesNameCache =
        new ConcurrentReferenceHashMap(ReferenceStrength.WEAK, ReferenceStrength.HARD);

    private static Method getGetterMethod(Class<?> cls, String prop) {
        Method rtnMethod = null;
        Map<String, Method> clsMap = getterMethodCache.get(cls);
        if (clsMap != null) {
            rtnMethod = clsMap.get(prop);
        }
        return rtnMethod;
    }

    private static void setGetterMethod(Class<?> cls, String prop,
        Method method) {
        Map<String, Method> clsMap = getterMethodCache.get(cls);
        if (clsMap == null) {
            clsMap = new ConcurrentReferenceHashMap(ReferenceStrength.HARD,
                ReferenceStrength.WEAK);
            getterMethodCache.put(cls, clsMap);
        }
        clsMap.put(prop, method);
    }

    private static Method getSetterMethod(Class<?> cls, String prop) {
        Method rtnMethod = null;
        Map<String, Method> clsMap = setterMethodCache.get(cls);
        if (clsMap != null) {
            rtnMethod = clsMap.get(prop);
        }
        return rtnMethod;
    }

    private static void setSetterMethod(Class<?> cls, String prop,
        Method method) {
        Map<String, Method> clsMap = setterMethodCache.get(cls);
        if (clsMap == null) {
            clsMap = new ConcurrentReferenceHashMap(ReferenceStrength.HARD,
                ReferenceStrength.WEAK);
            setterMethodCache.put(cls, clsMap);
        }
        clsMap.put(prop, method);
    }

    /**
     * Return the getter method matching the given property name, optionally
     * throwing an exception if none.
     */
    public static Method findGetter(Class cls, String prop, boolean mustExist) {
        Method m = getGetterMethod(cls, prop);
        if (m != null) {
            return m;
        }
        String capProp = StringUtil.capitalize(prop);
        try {
            // this algorithm searches for a get<prop> or is<prop> method in
            // a breadth-first manner.
            for (Class c = cls; c != null && c != Object.class;
                c = c.getSuperclass()) {
                m = getDeclaredMethod(c, "get" + capProp, null);
                if (m != null) {
                    setGetterMethod(cls, prop, m);
                    return m;
                } else {
                    m = getDeclaredMethod(c, "is" + capProp, null);
                    if (m != null && (m.getReturnType() == boolean.class
                        || m.getReturnType() == Boolean.class)) {
                        setGetterMethod(cls, prop, m);
                        return m;
                    } else {
                        m = getDeclaredMethod(c, "get" + prop, null);
                        if (m != null) {
                            setGetterMethod(cls, prop, m);
                            return m;
                        } else {
                            m = getDeclaredMethod(c, "is" + prop, null);
                            if (m != null
                                    && (m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class)) {
                                setGetterMethod(cls, prop, m);
                                return m;
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new GeneralException(e);
        }

        if (mustExist)
            throw new UserException(_loc.get("bad-getter", cls, prop));
        return null;
    }

    /**
     * Return the setter method matching the given property name, optionally
     * throwing an exception if none.  The property must also have a getter.
     */
    public static Method findSetter(Class cls, String prop, boolean mustExist) {
        Method getter = findGetter(cls, prop, mustExist);
        return (getter == null) ? null
            : findSetter(cls, prop, getter.getReturnType(), mustExist);
    }

    /**
     * Return the setter method matching the given property name, optionally
     * throwing an exception if none.
     */
    public static Method findSetter(Class cls, String prop, Class param,
        boolean mustExist) {
        Method m = getSetterMethod(cls, prop);
        if (m != null) {
            return m;
        }
        String name = "set" + StringUtil.capitalize(prop);
        try {
            for (Class c = cls; c != null && c != Object.class;
                c = c.getSuperclass()) {
                m = getDeclaredMethod(c, name, param);
                if (m != null) {
                    setSetterMethod(cls, prop, m);
                    return m;
                }
            }
        } catch (Exception e) {
            throw new GeneralException(e);
        }

        if (mustExist)
            throw new UserException(_loc.get("bad-setter", cls, prop));
        return null;
    }

    /**
     * Invokes <code>cls.getDeclaredMethods()</code>, and returns the method
     * that matches the <code>name</code> and <code>param</code> arguments.
     * Avoids the exception thrown by <code>Class.getDeclaredMethod()</code>
     * for performance reasons. <code>param</code> may be null. Additionally,
     * if there are multiple methods with different return types, this will
     * return the method defined in the least-derived class.
     *
     * @since 0.9.8
     */
    static Method getDeclaredMethod(Class cls, String name,
        Class param) {
        Method[] methods = (Method[]) AccessController.doPrivileged(
            J2DoPrivHelper.getDeclaredMethodsAction(cls));
        Method candidate = null;
        for (Method method : methods) {
            if (name.equals(method.getName())) {
                Class[] methodParams = method.getParameterTypes();
                if (param == null && methodParams.length == 0)
                    candidate = mostDerived(method, candidate);
                else if (param != null && methodParams.length == 1
                        && param.equals(methodParams[0]))
                    candidate = mostDerived(method, candidate);
            }
        }
        return candidate;
    }

    static Method mostDerived(Method meth1, Method meth2) {
        if (meth1 == null)
            return meth2;
        if (meth2 == null)
            return meth1;

        Class cls2 = meth2.getDeclaringClass();
        Class cls1 = meth1.getDeclaringClass();

        if (cls1.equals(cls2)) {
            Class ret1 = meth1.getReturnType();
            Class ret2 = meth2.getReturnType();
            if (ret1.isAssignableFrom(ret2))
                return meth2;
            else if (ret2.isAssignableFrom(ret1))
                return meth1;
            else
                throw new IllegalArgumentException(
                    _loc.get("most-derived-unrelated-same-type", meth1, meth2)
                        .getMessage());
        } else {
            if (cls1.isAssignableFrom(cls2))
                return meth2;
            else if (cls2.isAssignableFrom(cls1))
                return meth1;
            else
                throw new IllegalArgumentException(
                    _loc.get("most-derived-unrelated", meth1, meth2)
                        .getMessage());
        }
    }

    /**
     * Return the field with the given name, optionally throwing an exception
     * if none.
     */
    public static Field findField(Class cls, String name, boolean mustExist) {
        try {
            Field f;
            for (Class c = cls; c != null && c != Object.class;
                c = c.getSuperclass()) {
                f = getDeclaredField(c, name);
                if (f != null)
                    return f;
            }
        } catch (Exception e) {
            throw new GeneralException(e);
        }

        if (mustExist)
            throw new UserException(_loc.get("bad-field", cls, name));
        return null;
    }

    /**
     * Invokes <code>cls.getDeclaredFields()</code>, and returns the field
     * that matches the <code>name</code> argument.  Avoids the exception
     * thrown by <code>Class.getDeclaredField()</code> for performance reasons.
     *
     * @since 0.9.8
     */
    private static Field getDeclaredField(Class cls, String name) {
        Field[] fields = AccessController.doPrivileged(
            J2DoPrivHelper.getDeclaredFieldsAction(cls));
        for (Field field : fields) {
            if (name.equals(field.getName()))
                return field;
        }
        return null;
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static Object get(Object target, Field field) {
        if (target == null || field == null)
            return null;
        makeAccessible(field, field.getModifiers());
        try {
            return field.get(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Get the value of the given named field or a corresponding getter method.
     *
     * @return null if the field does not exist and mustExist is set to false or
     * the given target is null.
     *
     * @exception UserException if mustExist is true and the field or getter
     * method is non-existent
     */
    public static Object getValue(Object obj, String prop, boolean mustExist) {
    	if (obj == null)
    		return null;
    	Class cls = obj.getClass();
    	Field field = findField(cls, prop, false);
    	if (field != null)
    		return get(obj, field);
    	Method getter = findGetter(cls, prop, false);
    	if (getter != null)
    		return get(obj, getter);
        if (mustExist)
            throw new UserException(_loc.get("bad-field", cls, prop));
        return null; // should not reach
    }

    /**
     * Make the given member accessible if it isn't already.
     */
    private static void makeAccessible(AccessibleObject ao, int mods) {
        try {
            if (!Modifier.isPublic(mods) && !ao.isAccessible())
                AccessController.doPrivileged(J2DoPrivHelper
                    .setAccessibleAction(ao, true));
        } catch (SecurityException se) {
            throw new UserException(_loc.get("reflect-security", ao)).
                setFatal(true);
        }
    }

    /**
     * Wrap the given reflection exception as a runtime exception.
     */
    private static RuntimeException wrapReflectionException(Throwable t, Message message) {
        if (t instanceof InvocationTargetException)
            t = ((InvocationTargetException) t).getTargetException();
        t.initCause(new IllegalArgumentException(message.getMessage()));
        if (t instanceof RuntimeException)
            return (RuntimeException) t;
        return new GeneralException(t);
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static boolean getBoolean(Object target, Field field) {
        if (target == null || field == null)
            return false;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getBoolean(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static byte getByte(Object target, Field field) {
        if (target == null || field == null)
            return (byte) 0;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getByte(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static char getChar(Object target, Field field) {
        if (target == null || field == null)
            return (char) 0;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getChar(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static double getDouble(Object target, Field field) {
        if (target == null || field == null)
            return 0D;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getDouble(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static float getFloat(Object target, Field field) {
        if (target == null || field == null)
            return 0F;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getFloat(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static int getInt(Object target, Field field) {
        if (target == null || field == null)
            return 0;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getInt(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static long getLong(Object target, Field field) {
        if (target == null || field == null)
            return 0L;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getLong(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the value of the given field in the given object.
     */
    public static short getShort(Object target, Field field) {
        if (target == null || field == null)
            return (short) 0;
        makeAccessible(field, field.getModifiers());
        try {
            return field.getShort(target);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-field", target, field));
        }
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static Object get(Object target, Method getter) {
        if (target == null || getter == null)
            return null;
        makeAccessible(getter, getter.getModifiers());
        try {
            return getter.invoke(target, (Object[]) null);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("get-method", target, getter));
        }
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static boolean getBoolean(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? false : (Boolean) o;
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static byte getByte(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? (byte) 0 : ((Number) o).byteValue();
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static char getChar(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? (char) 0 : (Character) o;
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static double getDouble(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? 0D : ((Number) o).doubleValue();
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static float getFloat(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? 0F : ((Number) o).floatValue();
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static int getInt(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? 0 : ((Number) o).intValue();
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static long getLong(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? 0L : ((Number) o).longValue();
    }

    /**
     * Return the return value of the given getter in the given object.
     */
    public static short getShort(Object target, Method getter) {
        Object o = get(target, getter);
        return (o == null) ? (short) 0 : ((Number) o).shortValue();
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, Object value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.set(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value,
                    value == null ? "" : value.getClass()}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, boolean value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setBoolean(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "boolean"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, byte value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setByte(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "byte"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, char value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setChar(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "char"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, double value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setDouble(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "double"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, float value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setFloat(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "float"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, int value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setInt(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "int"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, long value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setLong(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "long"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     */
    public static void set(Object target, Field field, short value) {
        if (target == null || field == null)
            return;
        makeAccessible(field, field.getModifiers());
        try {
            field.setShort(target, value);
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-field", new Object[]{target, field, value, "short"}));
        }
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, Object value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, boolean value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, byte value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, char value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, double value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, float value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, int value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, long value, Field field) {
        set(target, field, value);
    }

    /**
     * Set the value of the given field in the given object.
     * Same behavior as above methods, but parameter ordering is rearranged
     * to simplify usage from generated bytecodes.
     *
     * @since 1.0.0
     */
    public static void set(Object target, short value, Field field) {
        set(target, field, value);
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, Object value) {
        if (target == null || setter == null)
            return;
        makeAccessible(setter, setter.getModifiers());
        try {
            setter.invoke(target, new Object[] { value });
        } catch (Throwable t) {
            throw wrapReflectionException(t, _loc.get("set-method", new Object[]{target, setter, value,
                    value == null ? "" : value.getClass()}));
        }
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, boolean value) {
        set(target, setter, (value) ? Boolean.TRUE : Boolean.FALSE);
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, byte value) {
        set(target, setter, Byte.valueOf(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, char value) {
        set(target, setter, Character.valueOf(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, double value) {
        set(target, setter, new Double(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, float value) {
        set(target, setter, new Float(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, int value) {
        set(target, setter, Integer.valueOf(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, long value) {
        set(target, setter, Long.valueOf(value));
    }

    /**
     * Invoke the given setter on the given object.
     */
    public static void set(Object target, Method setter, short value) {
        set(target, setter, Short.valueOf(value));
    }

    /**
     * Gets all bean-style property names of the given Class or its superclass.
     * A bean-style property 'abc' exists in Class C iff C has declared
     * following pair of methods:
     *   public void setAbc(Y y) or public C setAbc(Y y)
     *   public Y getAbc();
     *
     * If a getter property is annotated with {@link Reflectable}, then
     * it is ignored.
     *
     */
    public static Set<String> getBeanStylePropertyNames(Class<?> c) {
        if (c == null)
            return Collections.emptySet();
        Set<String> result = beanPropertiesNameCache.get(c);
        if (result != null) {
            return result;
        }
        Method[] methods = c.getMethods();
        if (methods == null || methods.length < 2)
            return Collections.emptySet();
        result = new TreeSet<>();
        for (Method m : methods) {
            if (m.getName().startsWith("get")) {
                if (!canReflect(m))
                    continue;
                String prop = StringUtil.capitalize(m.getName()
                    .substring("get".length()));
                Class<?> rtype = m.getReturnType();
                try {
                  Method setter = c.getMethod("set"+prop, new Class<?>[]{rtype});
                  if (setter.getReturnType() == void.class ||
                      setter.getReturnType().isAssignableFrom(c))
                  result.add(prop);
                } catch (NoSuchMethodException e) {

                }
            }
        }
        beanPropertiesNameCache.put(c, result);
        return result;
    }

    /**
     * Gets all public field names of the given Class.
     *
     */
    public static Set<String> getPublicFieldNames(Class c) {
        if (c == null)
            return Collections.EMPTY_SET;
        Field[] fields = c.getFields();
        if (fields == null || fields.length == 0)
            return Collections.EMPTY_SET;
        Set<String> result = new TreeSet<>();
        for (Field f : fields) {
            if (canReflect(f))
                result.add(f.getName());
        }
        return result;
    }

    /**
     * Gets values of all field f the given class such that f exactly
     * match the given modifiers and are of given type (Object implies any type)
     * unless f is annotated as {@link Reflectable}.
     *
     */
    public static <T> Set<T> getFieldValues(Class c, int mods, Class<T> t){
        if (c == null)
            return Collections.EMPTY_SET;
        Field[] fields = c.getFields();
        if (fields == null || fields.length == 0)
            return Collections.EMPTY_SET;
        Set<T> result = new TreeSet<>();
        for (Field f : fields) {
            if (mods == f.getModifiers()
            && (t == Object.class || t.isAssignableFrom(f.getType()))
            && canReflect(f)) {
                try {
                    result.add((T)f.get(null));
                } catch (IllegalArgumentException | IllegalAccessException e) {
                }
            }
        }
        return result;
    }

    /**
     * Affirms if the given member is selected for reflection. The decision is
     * based on the following truth table on both the class-level and
     * member-level annotation (null annotation represents MAYBE)
     *
     * Class   member
     * MAYBE   MAYBE   YES
     * MAYBE   YES     YES
     * MAYBE   NO      NO
     *
     * YES     MAYBE   YES
     * YES     YES     YES
     * YES     NO      NO
     *
     * NO      YES     YES
     * NO      MAYBE   NO
     * NO      NO      NO
     *
    */
    static boolean canReflect(Reflectable cls, Reflectable member) {
        if (cls == null || cls.value()) {
            return member == null || member.value();
        } else {
            return member != null && member.value();
        }
    }

    /**
     * Affirms if the original declaration the given field is annotated
     * for reflection.
     */
    static boolean canReflect(Field field) {
        Class cls = field.getDeclaringClass();
        return canReflect((Reflectable)cls.getAnnotation(Reflectable.class),
            field.getAnnotation(Reflectable.class));
    }

    /**
     * Affirms if the original declaration the given method is annotated
     * for reflection.
     */
    static boolean canReflect(Method method) {
        Class cls = getDeclaringClass(method);
        if (cls != method.getDeclaringClass())
            method = getDeclaringMethod(cls, method);
        return canReflect((Reflectable)cls.getAnnotation(Reflectable.class),
            method.getAnnotation(Reflectable.class));
    }

    /**
     * Gets the declaring class of the given method signature but also checks
     * if the method is declared in an interface. If yes, then returns the
     * interface.
     */
    public static Class getDeclaringClass(Method m) {
        if (m == null)
            return null;
        Class cls = m.getDeclaringClass();
        Class[] intfs =  cls.getInterfaces();
        for (Class intf : intfs) {
            if (getDeclaringMethod(intf, m) != null)
                cls = intf;
        }
        return cls;
    }

    /**
     * Gets the method in the given class that has the same signature of the
     * given method, if exists. Otherwise, null.
     */
    public static Method getDeclaringMethod(Class c, Method m) {
        try {
            Method m0 = c.getMethod(m.getName(), m.getParameterTypes());
            return m0;
        } catch (Exception e) {
            return null;
        }
    }
}
