/*******************************************************************************
 * Copyright (c) 1998, 2008 Oracle. 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
 *     05/16/2008-1.0M8 Guy Pelletier 
 *       - 218084: Implement metadata merging functionality between mapping files
 ******************************************************************************/  
package org.eclipse.persistence.internal.jpa.metadata.accessors.objects;

import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;

import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;

import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.jpa.metadata.MetadataDescriptor;
import org.eclipse.persistence.internal.jpa.metadata.MetadataLogger;
import org.eclipse.persistence.internal.jpa.metadata.xml.XMLEntityMappings;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedGetMethod;

/**
 * INTERNAL:
 * An object to hold onto a valid EJB 3.0 decorated method.
 * 
 * @author Guy Pelletier
 * @since TopLink 10.1.3/EJB 3.0 Preview
 */
public class MetadataMethod extends MetadataAnnotatedElement {
    public static final String IS_PROPERTY_METHOD_PREFIX = "is";
    public static final String GET_PROPERTY_METHOD_PREFIX = "get";
    private static final String SET_PROPERTY_METHOD_PREFIX = "set";
    private static final String SET_IS_PROPERTY_METHOD_PREFIX = "setIs";
    private static final int POSITION_AFTER_IS_PREFIX = IS_PROPERTY_METHOD_PREFIX.length();
    private static final int POSITION_AFTER_GET_PREFIX = GET_PROPERTY_METHOD_PREFIX.length();
    
    protected Method m_getMethod;
    protected Method m_setMethod;
    
    /**
     * INTERNAL:
     * Called from CMP3Policy ... doesn't need a logger.
     */
    public MetadataMethod(Method getMethod, Class declaringClass) {
        super(getMethod);
        
        init(getMethod, declaringClass);
    }
    
    /**
     * INTERNAL:
     */
    public MetadataMethod(Method getMethod, MetadataLogger logger) {
        super(getMethod, logger);
        
        init(getMethod, getMethod.getDeclaringClass());
    }
    
    /**
     * INTERNAL:
     */
    public MetadataMethod(Method getMethod, Method setMethod, String attributeName, XMLEntityMappings entityMappings) {
        super(getMethod, entityMappings);
        
        m_getMethod = getMethod;
        m_setMethod = setMethod;
        
        setName(getMethod.getName());
        setAttributeName(attributeName);
        setRelationType(getMethod.getGenericReturnType());
    }
    
    /**
     * INTERNAL:
     */
    public MetadataMethod(Method getMethod, XMLEntityMappings entityMappings) {
        super(getMethod, entityMappings);
        
        init(getMethod, getMethod.getDeclaringClass()); 
    }
    
    /**
     * INTERNAL:
     * Method to convert a getXyz or isXyz method name to an xyz attribute name.
     * NOTE: The method name passed it may not actually be a method name, so
     * by default return the name passed in.
     */
    public static String getAttributeNameFromMethodName(String methodName) {
        String leadingChar = "";
        String restOfName = methodName;
        
        // We're looking at method named 'get' or 'set', therefore,
        // there is no attribute name, set it to "" string for now.
        if (methodName.equals(GET_PROPERTY_METHOD_PREFIX) || methodName.equals(IS_PROPERTY_METHOD_PREFIX)) {
            return "";
        } else if (methodName.startsWith(GET_PROPERTY_METHOD_PREFIX)) {
            leadingChar = methodName.substring(POSITION_AFTER_GET_PREFIX, POSITION_AFTER_GET_PREFIX + 1);
            restOfName = methodName.substring(POSITION_AFTER_GET_PREFIX + 1);
        } else if (methodName.startsWith(IS_PROPERTY_METHOD_PREFIX)){
            leadingChar = methodName.substring(POSITION_AFTER_IS_PREFIX, POSITION_AFTER_IS_PREFIX + 1);
            restOfName = methodName.substring(POSITION_AFTER_IS_PREFIX + 1);
        }
        
        return leadingChar.toLowerCase().concat(restOfName);
    }
    
    /**
     * INTERNAL:
     */
    protected Method getMethod() {
        return (Method) getAnnotatedElement();
    }
    
    /**
     * INTERNAL:
     * Method to convert a getMethod into a setMethod. This method could return 
     * null if the corresponding set method is not found.
     */ 
    public Method getSetMethod() {
        return m_setMethod;
    }
    
    /**
     * INTERNAL:
     * Method to convert a getMethod into a setMethod. This method could return 
     * null if the corresponding set method is not found.
     */ 
    public Method getSetMethod(Method method, Class cls) {
        String getMethodName = method.getName();
        Class[] params = new Class[] { method.getReturnType() };
            
        if (getMethodName.startsWith(GET_PROPERTY_METHOD_PREFIX)) {
            // Replace 'get' with 'set'.
            return getMethod(SET_PROPERTY_METHOD_PREFIX + getMethodName.substring(3), cls, params);
        }
        
        // methodName.startsWith(IS_PROPERTY_METHOD_PREFIX)
        // Check for a setXyz method first, if it exists use it.
        Method setMethod = getMethod(SET_PROPERTY_METHOD_PREFIX + getMethodName.substring(2), cls, params);
        
        if (setMethod == null) {
            // setXyz method was not found try setIsXyz
            return getMethod(SET_IS_PROPERTY_METHOD_PREFIX + getMethodName.substring(2), cls, params);
        }
        
        return setMethod;
    }
    
    /**
     * INTERNAL:
     * If the methodName passed in is a declared method on cls, then return
     * the methodName. Otherwise return null to indicate it does not exist.
     */
    protected Method getMethod(String methodName, Class cls, Class[] params) {
        try {
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                try {
                    return AccessController.doPrivileged(new PrivilegedGetMethod(cls, methodName, params, true));
                } catch (PrivilegedActionException exception) {
                    return null;
                }
            } else {
                return PrivilegedAccessHelper.getMethod(cls, methodName, params, true);
            }
        } catch (NoSuchMethodException e1) {
            return null;
        }
    }
    
    /**
     * INTERNAL:
     */
    public String getSetMethodName() {
        return m_setMethod.getName();
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasAttributeName() {
        return ! getAttributeName().equals("");
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasParameters() {
        return m_getMethod.getParameterTypes().length > 0;
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasSetMethod() {
        return m_setMethod != null;
    }
    
    /**
     * INTERNAL:
     */
    protected void init(Method getMethod, Class declaringClass) {
        m_getMethod = getMethod;
        m_setMethod = getSetMethod(getMethod, declaringClass);
        
        setName(getMethod.getName());
        setAttributeName(getAttributeNameFromMethodName(getMethod.getName()));
        setRelationType(getMethod.getGenericReturnType());
    }
    
    /**
     * INTERNAL:
     * Return true if it has a valid name (starts with get or is) and has a
     * property name (getXyz or isXyz) and does not have parameters and has
     * an associated set method.
     */
    protected boolean isALifeCycleCallbackMethod() {
        return isAnnotationPresent(PostLoad.class) ||
               isAnnotationPresent(PostPersist.class) ||
               isAnnotationPresent(PostRemove.class) ||
               isAnnotationPresent(PostUpdate.class) ||
               isAnnotationPresent(PrePersist.class) ||
               isAnnotationPresent(PreRemove.class) ||
               isAnnotationPresent(PreUpdate.class);
    }
    
    /**
     * INTERNAL:
     * Return true if it has a valid name (starts with 'get' or 'is') and has a
     * property name (get'Xyz' or is'Xyz') and does not have parameters and has
     * an associated set method.
     */
    protected boolean isValidPersistenceMethod() {
        return isValidPersistenceMethodName() && ! hasParameters() && hasSetMethod();
    }
    
    /**
     * INTERNAL:
     * Return true is this method is a valid persistence method. This method
     * will validate against any declared annotations on the method.
     */
    public boolean isValidPersistenceMethod(MetadataDescriptor descriptor) {
        return ! isALifeCycleCallbackMethod() && isValidPersistenceMethod(descriptor, hasDeclaredAnnotations(descriptor));
    }
    
    /**
     * INTERNAL:
     * Return true is this method is a valid persistence method. User decorated
     * is used to indicate that the method either had persistence annotations
     * defined on it or that it was specified in XML.
     */
    public boolean isValidPersistenceMethod(MetadataDescriptor descriptor, boolean userDecorated) {
        if (! isValidPersistenceElement(getMethod().getModifiers()) || ! isValidPersistenceMethod()) {
            // So it's not a valid method, did the user decorate it with
            // annotations or specify it in XML?
            if (userDecorated) {
                // Let's figure out which exception we should throw then ...
                if (hasParameters()) {
                    throw ValidationException.mappingMetadataAppliedToMethodWithArguments(getMethod(), descriptor.getJavaClass());
                } else if (!hasSetMethod()) {
                    throw ValidationException.noCorrespondingSetterMethodDefined(descriptor.getJavaClass(), getMethod());
                } else {
                    // General, catch all remaining exception cases. 
                    throw ValidationException.mappingMetadataAppliedToInvalidAttribute(getMethod(), descriptor.getJavaClass());
                }
            }
            
            return false;
        }
        
        return true;
    }
    
    /**
     * INTERNAL:
     */
    public boolean isValidPersistenceMethodName() {
        return (getName().startsWith(GET_PROPERTY_METHOD_PREFIX) || getName().startsWith(IS_PROPERTY_METHOD_PREFIX)) && hasAttributeName();
    }
}
