/*******************************************************************************
 * Copyright (c) 1998, 2015 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 v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 *     08/23/2010-2.2 Michael O'Brien 
 *        - 323043: application.xml module ordering may cause weaving not to occur causing an NPE.
 *                       warn if expected "_persistence_*_vh" method not found
 *                       instead of throwing NPE during deploy validation.
 *     30/05/2012-2.4 Guy Pelletier
 *       - 354678: Temp classloader is still being used during metadata processing
 *
 ******************************************************************************/
package org.eclipse.persistence.internal.security;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.persistence.internal.helper.Helper;

/**
 * INTERNAL:
 * Privileged Access Helper provides a utility so all calls that require privileged access can use the same code.
 * 
 * Do privileged blocks can be used with a security manager to grant a code base (eclipselink.jar) access to certain
 * Java operations such as reflection.  Generally a security manager is not enabled in a JVM, so this is not an issue.
 * If a security manager is enabled, then either the application can be configured to have access to operations such as
 * reflection, or only EclipseLink can be given access.  If only EclipseLink is desired to be given access then
 * do privileged must be enabled through the System property "eclipselink.security.usedoprivileged"=true.
 * 
 * Note the usage of do privileged has major impacts on performance, so should normally be avoided.
 */
public class PrivilegedAccessHelper {
    private static boolean defaultUseDoPrivilegedValue = false;
    private static boolean shouldCheckPrivilegedAccess = true;
    private static boolean shouldUsePrivilegedAccess = false;

    private static Map<String, Class> primitiveClasses;

    static {
        primitiveClasses = new HashMap<>();
        primitiveClasses.put("boolean", boolean.class);
        primitiveClasses.put("int", int.class);
        primitiveClasses.put("long", long.class);
        primitiveClasses.put("float", float.class);
        primitiveClasses.put("double", double.class);
        primitiveClasses.put("char", char.class);
        primitiveClasses.put("byte", byte.class);
        primitiveClasses.put("void", void.class);
        primitiveClasses.put("short", short.class);
    }
    
    /**
     * INTERNAL
     * It will be used to set default value of property "eclipselink.security.usedoprivileged"
     * if not passed as system property. This is used by GlassfishPlatform.
     */
    public static void setDefaultUseDoPrivilegedValue(boolean def) {
        defaultUseDoPrivilegedValue = def;
        shouldCheckPrivilegedAccess = true;
    }
    
    /**
     * Finding a field within a class potentially has to navigate through it's superclasses to eventually
     * find the field.  This method is called by the public getDeclaredField() method and does a recursive
     * search for the named field in the given classes or it's superclasses.
     */
    private static Field findDeclaredField(Class javaClass, String fieldName) throws NoSuchFieldException {
        try {
            return javaClass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException ex) {
            Class superclass = javaClass.getSuperclass();
            if (superclass == null) {
                throw ex;
            } else {
                return findDeclaredField(superclass, fieldName);
            }
        }
    }

    /**
     * Finding a method within a class potentially has to navigate through it's superclasses to eventually
     * find the method.  This method is called by the public getDeclaredMethod() method and does a recursive
     * search for the named method in the given classes or it's superclasses.
     */
    private static Method findMethod(Class javaClass, String methodName, Class[] methodParameterTypes) throws NoSuchMethodException {
        try {
            // use a combination of getDeclaredMethod() and recursion to ensure we get the non-public methods
            // getMethod will not help because it returns only public methods
            return javaClass.getDeclaredMethod(methodName, methodParameterTypes);
        } catch (NoSuchMethodException ex) {
            Class superclass = javaClass.getSuperclass();
            if (superclass == null) {
                throw ex;
            } else {
                try{
                    return findMethod(superclass, methodName, methodParameterTypes);
                }catch (NoSuchMethodException lastEx){
                    throw ex;
                }
            }
        }
    }
    
    /**
     * Execute a java Class.forName().  Wrap the call in a doPrivileged block if necessary.
     * @param className
     */
    public static Class getClassForName(final String className) throws ClassNotFoundException {
        // Check for primitive types.
        Class primitive = primitiveClasses.get(className);
        if (primitive != null) {
            return primitive;
        }
        return Class.forName(className);
    }

    /**
     * Execute a java Class.forName() wrap the call in a doPrivileged block if necessary.
     */
    public static Class getClassForName(final String className, final boolean initialize, final ClassLoader loader) throws ClassNotFoundException {
        // Check for primitive types.
        Class primitive = primitiveClasses.get(className);
        if (primitive != null) {
            return primitive;
        }
        return Class.forName(className, initialize, loader);
    }

    /**
     * Gets the class loader for a given class.  Wraps the call in a privileged block if necessary
     */
    public static ClassLoader getClassLoaderForClass(final Class clazz) {
        return clazz.getClassLoader();
    }

    /**
     * Gets the class loader for a given class.  Wraps the call in a privileged block if necessary
     */
    public static ClassLoader privilegedGetClassLoaderForClass(final Class clazz) {
        try{
            return AccessController.doPrivileged(new PrivilegedGetClassLoaderForClass(clazz));
        }catch (PrivilegedActionException ex){
            throw (RuntimeException) ex.getCause();
        }
    }

    /**
     * Get the public constructor for the given class and given arguments and wrap it in doPrivileged if
     * necessary.  The shouldSetAccessible parameter allows the the setAccessible API to be called as well.
     * This option was added to avoid making multiple doPrivileged calls within InstantiationPolicy.
     * @param javaClass The class to get the Constructor for
     * @param args An array of classes representing the argument types of the constructor
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchMethodException
     */
    public static Constructor getConstructorFor(final Class javaClass, final Class[] args, final boolean shouldSetAccessible) throws NoSuchMethodException {
        Constructor result = null;
        try {
            result = javaClass.getConstructor(args);
        } catch (NoSuchMethodException missing) {
            // Search for any constructor with the same number of arguments and assignable types.
            for (Constructor constructor : javaClass.getConstructors()) {
                if (constructor.getParameterTypes().length == args.length) {
                    boolean found = true;
                    for (int index = 0; index < args.length; index++) {
                        Class parameterType = Helper.getObjectClass(constructor.getParameterTypes()[index]);
                        Class argType = Helper.getObjectClass(args[index]);                      
                        if ((!parameterType.isAssignableFrom(argType))
                                && (!argType.isAssignableFrom(parameterType))) {
                            found = false; 
                            break;
                        }
                    }
                    if (found) {
                        result = constructor;
                        break;
                    }
                }
            }
            if (result == null) {
                throw missing;
            }
        }
        if (shouldSetAccessible) {
            result.setAccessible(true);
        }
        return result;
    }

    /**
     *  Get the context ClassLoader for a thread.  Wrap the call in a doPrivileged block if necessary.
     */
    public static ClassLoader getContextClassLoader(final Thread thread) {
        return thread.getContextClassLoader();
    }

    /**
     * Get the constructor for the given class and given arguments (regardless of whether it is public
     * or private))and wrap it in doPrivileged if necessary.  The shouldSetAccessible parameter allows
     * the the setAccessible API to be called as well. This option was added to avoid making multiple
     * doPrivileged calls within InstantiationPolicy.
     * @param javaClass The class to get the Constructor for
     * @param args An array of classes representing the argument types of the constructor
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchMethodException
     */
    public static Constructor getDeclaredConstructorFor(final Class javaClass, final Class[] args, final boolean shouldSetAccessible) throws NoSuchMethodException {
        Constructor result = javaClass.getDeclaredConstructor(args);
        if (shouldSetAccessible) {
            result.setAccessible(true);
        }
        return result;
    }

    /**
     * Get a field in a class or its superclasses and wrap the call in doPrivileged if necessary.
     * The shouldSetAccessible parameter allows the the setAccessible API to be called as well.
     * This option was added to avoid making multiple doPrivileged calls within InstanceVariableAttributeAccessor.
     * @param javaClass The class to get the field from
     * @param fieldName The name of the field
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchFieldException
     */
    public static Field getField(final Class javaClass, final String fieldName, final boolean shouldSetAccessible) throws NoSuchFieldException {
        Field field = findDeclaredField(javaClass, fieldName);
        if (shouldSetAccessible) {
            field.setAccessible(true);
        }
        return field;
    }

    /**
     * Get a field actually declared in a class and wrap the call in doPrivileged if necessary.
     * The shouldSetAccessible parameter allows the the setAccessible API to be called as well.
     * This option was added to avoid making multiple doPrivileged calls within InstanceVariableAttributeAccessor.
     * @param javaClass The class to get the field from
     * @param fieldName The name of the field
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchFieldException
     */
    public static Field getDeclaredField(final Class javaClass, final String fieldName, final boolean shouldSetAccessible) throws NoSuchFieldException {
        Field field = javaClass.getDeclaredField(fieldName);
        if (shouldSetAccessible) {
            field.setAccessible(true);
        }
        return field;
    }

    /**
     * Get the list of fields in a class.  Wrap the call in doPrivileged if necessary
     * Excludes inherited fields.
     * @param clazz the class to get the fields from.
     */
    public static Field[] getDeclaredFields(final Class clazz) {
        return clazz.getDeclaredFields();
    }

    /**
     * Get the list of public fields in a class.  Wrap the call in doPrivileged if necessary
     * @param clazz the class to get the fields from.
     */
    public static Field[] getFields(final Class clazz) {
        return clazz.getFields();
    }

   /**
     * Return a method on a given class with the given method name and parameter 
     * types. This call will NOT traverse the superclasses. Wrap the call in 
     * doPrivileged if necessary.
     * @param method the class to get the method from
     * @param methodName the name of the method to get
     * @param methodParameters a list of classes representing the classes of the
     *  parameters of the method.
     */
    public static Method getDeclaredMethod(final Class clazz, final String methodName, final Class[] methodParameterTypes) throws NoSuchMethodException {
         return clazz.getDeclaredMethod(methodName, methodParameterTypes);
    }
    
    /**
     * Get a method declared in the given class. Wrap the call in doPrivileged 
     * if necessary. This call will traverse the superclasses. The 
     * shouldSetAccessible parameter allows the the setAccessible API to be 
     * called as well. This option was added to avoid making multiple 
     * doPrivileged calls within MethodBasedAttributeAccessor.
     * @param javaClass The class to get the method from
     * @param methodName The name of the method to get
     * @param methodParameterTypes A list of classes representing the classes of the parameters of the mthod
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchMethodException
     */
    public static Method getMethod(final Class javaClass, final String methodName, final Class[] methodParameterTypes, final boolean shouldSetAccessible) throws NoSuchMethodException {
        Method method = findMethod(javaClass, methodName, methodParameterTypes);
        if (shouldSetAccessible) {
            method.setAccessible(true);
        }
        return method;
    }
    
    /**
     * Get a public method declared in the given class. Wrap the call in doPrivileged if necessary. 
     * This call will traverse the superclasses. The shouldSetAccessible parameter allows the the 
     * setAccessible API to be called as well. This option was added to avoid making multiple 
     * doPrivileged calls within MethodBasedAttributeAccessor.
     * 
     * @param javaClass The class to get the method from
     * @param methodName The name of the method to get
     * @param methodParameterTypes A list of classes representing the classes of the parameters of the method
     * @param shouldSetAccessible whether or not to call the setAccessible API
     * @throws java.lang.NoSuchMethodException
     */
    public static Method getPublicMethod(final Class javaClass, final String methodName, final Class[] methodParameterTypes, final boolean shouldSetAccessible) throws NoSuchMethodException {
        // Return the (public) method - will traverse superclass(es) if necessary
        Method method = javaClass.getMethod(methodName, methodParameterTypes);
        if (shouldSetAccessible) {
            method.setAccessible(true);
        }
        return method;
    }

    /**
     * Get the list of methods in a class. Wrap the call in doPrivileged if 
     * necessary. Excludes inherited methods.
     * @param clazz the class to get the methods from.
     */
    public static Method[] getDeclaredMethods(final Class clazz) {
        return clazz.getDeclaredMethods();
    }
    
    /**
     * Get the return type for a given method. Wrap the call in doPrivileged if necessary.
     * @param field
     */
    public static Class getFieldType(final Field field) {
        return field.getType();
    }

    /**
     * Get the line separator character.
     * Previous versions of TopLink always did this in a privileged block so we will continue to do so.
     */
    public static String getLineSeparator() {
        if (shouldUsePrivilegedAccess()) {
            return AccessController.doPrivileged(new PrivilegedGetSystemProperty("file.separator"));
        } else {
            return org.eclipse.persistence.internal.helper.Helper.cr();
        }
    }

    /**
     * Get the list of parameter types for a given method.  Wrap the call in doPrivileged if necessary.
     * @param method The method to get the parameter types of
     */
    public static Class[] getMethodParameterTypes(final Method method) {
        return method.getParameterTypes();
    }

    /**
     * Get the return type for a given method. Wrap the call in doPrivileged if necessary.
     * @param method
     */
    public static Class getMethodReturnType(final Method method) {
        // 323148: a null method as a possible problem with module ordering breaking weaving - has been trapped by implementors of this method.
        return method.getReturnType();
    }
    
    /**
     * Get the list of methods in a class. Wrap the call in doPrivileged if 
     * necessary. This call will traverse the superclasses.
     * @param clazz the class to get the methods from.
     */
    public static Method[] getMethods(final Class clazz) {
        return clazz.getMethods();
    }
    
    /**
     * Get the value of the given field in the given object.
     */
    public static Object getValueFromField(final Field field, final Object object) throws IllegalAccessException {
        return field.get(object);
    }

    /**
     * Construct an object with the given Constructor and the given array of arguments.  Wrap the call in a
     * doPrivileged block if necessary.
     */
    public static Object invokeConstructor(final Constructor constructor, final Object[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
        return constructor.newInstance(args);
    }

    /**
     * Invoke the givenMethod on a givenObject. Assumes method does not take
     * parameters. Wrap in a doPrivileged block if necessary.
     */
    public static Object invokeMethod(final Method method, final Object object) throws IllegalAccessException, InvocationTargetException {
        return invokeMethod(method, object, (Object[]) null);
    }
    
    /**
     * Invoke the givenMethod on a givenObject using the array of parameters given.  Wrap in a doPrivileged block
     * if necessary.
     */
    public static Object invokeMethod(final Method method, final Object object, final Object[] parameters) throws IllegalAccessException, InvocationTargetException {
        // Ensure the method is accessible.
        if (!method.isAccessible()) {
            method.setAccessible(true);
        }
        return method.invoke(object, parameters);
    }

    /**
     * Get a new instance of a class using the default constructor.  Wrap the call in a privileged block
     * if necessary.
     */
    public static Object newInstanceFromClass(final Class clazz) throws IllegalAccessException, InstantiationException {
        return clazz.newInstance();
    }

    /**
     * Set the value of a given field in the given object with the value given.  Wrap the call in a privileged block
     * if necessary.
     */
    public static void setValueInField(final Field field, final Object object, final Object value) throws IllegalAccessException {
        field.set(object, value);
    }

    /**
     * This method checks to see if calls should be made to doPrivileged.
     * It will only return true if a security manager is enabled,
     * and the "eclipselink.security.usedoprivileged" property is set.
     * <p>
     * Note: it is not possible to run EclipseLink using doPrivileged blocks when there is no SecurityManager
     * enabled.
     */
    public static boolean shouldUsePrivilegedAccess() {
        // #shouldUsePrivilegedAccess will be evaluated the first time this method is called and then 'reevaluated' only
        // after explicit call to #setDefaultUseDoPrivilegedValue().
        if (shouldCheckPrivilegedAccess) {
            if (System.getSecurityManager() != null) {    
                String usePrivileged = AccessController.doPrivileged(new PrivilegedGetSystemProperty("eclipselink.security.usedoprivileged"));
                if (usePrivileged == null) {
                    shouldUsePrivilegedAccess = defaultUseDoPrivilegedValue;
                } else {
                    shouldUsePrivilegedAccess = usePrivileged.equalsIgnoreCase("true");
                }
            } else {
                shouldUsePrivilegedAccess = false;
            }
            shouldCheckPrivilegedAccess = false;
        }
        return shouldUsePrivilegedAccess;
    }
}
