/*******************************************************************************
 * Copyright (c) 1998, 2013 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
 *     05/16/2008-1.0M8 Guy Pelletier 
 *       - 218084: Implement metadata merging functionality between mapping files
 *     09/23/2008-1.1 Guy Pelletier 
 *       - 241651: JPA 2.0 Access Type support
 *     10/01/2008-1.1 Guy Pelletier 
 *       - 249329: To remain JPA 1.0 compliant, any new JPA 2.0 annotations should be referenced by name
 *     03/29/2010-2.1 Guy Pelletier 
 *       - 267217: Add Named Access Type to EclipseLink-ORM
 *     04/09/2010-2.1 Guy Pelletier 
 *       - 307050: Add defaults for access methods of a VIRTUAL access type
 *     06/22/2010-2.2 Guy Pelletier 
 *       - 308729: Persistent Unit deployment exception when mappedsuperclass has no annotations but has lifecycle callbacks
 *     12/01/2010-2.2 Guy Pelletier 
 *       - 331234: xml-mapping-metadata-complete overriden by metadata-complete specification 
 ******************************************************************************/  
package org.eclipse.persistence.internal.jpa.metadata.accessors.objects;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.jpa.metadata.MetadataLogger;
import org.eclipse.persistence.internal.jpa.metadata.accessors.classes.ClassAccessor;

import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_ACCESS_PROPERTY;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_POST_LOAD;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_POST_PERSIST;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_POST_REMOVE;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_POST_UPDATE;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_PRE_PERSIST;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_PRE_REMOVE;
import static org.eclipse.persistence.internal.jpa.metadata.MetadataConstants.JPA_PRE_UPDATE;

/**
 * INTERNAL:
 * An object to hold onto a valid JPA decorated method.
 * 
 * @author Guy Pelletier
 * @since TopLink 10.1.3/EJB 3.0 Preview
 */
public class MetadataMethod extends MetadataAnnotatedElement {

    /** Class that the method is defined in. */
    protected MetadataClass m_metadataClass;
    
    /** Class name of method return type. */
    protected String m_returnType;

    /** List of class names of method parameters. */
    protected List<String> m_parameters;
    
    /** Corresponding set method, if the method is an accessor get method. */
    protected MetadataMethod m_setMethod;
    
    /** Used to store multiple methods with the same name in a class. */
    protected MetadataMethod m_next;
    
    /**
     * Create the method from the class metadata.
     */
    public MetadataMethod(MetadataFactory factory, MetadataClass metadataClass) {
        super(factory);
        m_metadataClass = metadataClass;
        m_parameters = new ArrayList<String>();
    }
    
    /**
     * INTERNAL:
     */
    public void addParameter(String parameter) {
        m_parameters.add(parameter);
    }
    
    /**
     * INTERNAL:
     */
    public MetadataClass getMetadataClass() {
        return m_metadataClass;
    }
    
    /**
     * INTERNAL:
     */
    public MetadataMethod getNext() {
        return m_next;
    }
    
    /**
     * INTERNAL:
     */
    public List<String> getParameters() {
        return m_parameters;
    }
    
    /**
     * INTERNAL:
     */
    public String getReturnType() {
        return m_returnType;
    }
    
    /**
     * INTERNAL:
     * Method to convert a getMethod into a setMethod. This method could return 
     * null if the corresponding set method is not found.
     */ 
    public MetadataMethod getSetMethod() {
        if (m_setMethod == null) {
            m_setMethod = getSetMethod(getMetadataClass());
        }
        return m_setMethod;
    }
    
    /**
     * INTERNAL:
     * Method to convert a getMethod into a setMethod.
     */ 
    public void setSetMethod(MetadataMethod method) {
        m_setMethod = method;
    }
    
    /**
     * INTERNAL:
     * Method to convert a getMethod into a setMethod. This method could return 
     * null if the corresponding set method is not found.
     */ 
    public MetadataMethod getSetMethod(MetadataClass cls) {
        String getMethodName = getName();
        List<String> params = Arrays.asList(new String[] {getReturnType()});
        MetadataMethod setMethod = null;
        
        if (getMethodName.startsWith(Helper.GET_PROPERTY_METHOD_PREFIX)) {
            // Replace 'get' with 'set'.
            setMethod = cls.getMethod(Helper.SET_PROPERTY_METHOD_PREFIX + getMethodName.substring(3), params);
        } else {        
            // methodName.startsWith(IS_PROPERTY_METHOD_PREFIX)
            // Check for a setXyz method first, if it exists use it.
            setMethod = cls.getMethod(Helper.SET_PROPERTY_METHOD_PREFIX + getMethodName.substring(2), params);
            
            if (setMethod == null) {
                // setXyz method was not found try setIsXyz
                setMethod = cls.getMethod(Helper.SET_IS_PROPERTY_METHOD_PREFIX + getMethodName.substring(2), params);
            }
        }
        
        return setMethod;
    }
    
    /**
     * INTERNAL:
     */
    public String getSetMethodName() {
        return getSetMethod().getName();
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasAttributeName() {
        return ! getAttributeName().equals("");
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasParameters() {
        return getParameters().size() > 0;
    }
    
    /**
     * INTERNAL:
     */
    public boolean hasSetMethod() {
        return getSetMethod() != null;
    }
    
    /**
     * 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.
     */
    public boolean isALifeCycleCallbackMethod() {
        return isAnnotationPresent(JPA_POST_LOAD) ||
               isAnnotationPresent(JPA_POST_PERSIST) ||
               isAnnotationPresent(JPA_POST_REMOVE) ||
               isAnnotationPresent(JPA_POST_UPDATE) ||
               isAnnotationPresent(JPA_PRE_PERSIST) ||
               isAnnotationPresent(JPA_PRE_REMOVE) ||
               isAnnotationPresent(JPA_PRE_UPDATE);
    }
    
    /**
     * 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.  If the 
     * mustBeExplicit flag is true, then we are processing the inverse of an 
     * explicit access setting and the property must have an Access(PROPERTY) 
     * setting to be processed. Otherwise, it is ignored.
     */
    public boolean isValidPersistenceMethod(boolean mustBeExplicit, ClassAccessor classAccessor) {
        if (isValidPersistenceElement(mustBeExplicit, JPA_ACCESS_PROPERTY, classAccessor)) {
            return ! isALifeCycleCallbackMethod() && isValidPersistenceMethod(classAccessor, hasDeclaredAnnotations(classAccessor));
        }
        
        return false;
    }
    
    /**
     * 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(ClassAccessor classAccessor, boolean userDecorated) {
        if (! isValidPersistenceElement(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(this, classAccessor.getDescriptorJavaClass());
                } else if (!hasSetMethod()) {
                    throw ValidationException.noCorrespondingSetterMethodDefined(classAccessor.getDescriptorJavaClass(), this);
                } else {
                    // General, catch all remaining cases and log a warning message.
                    getLogger().logConfigMessage(MetadataLogger.IGNORE_MAPPING_METADATA, this, classAccessor.getDescriptorJavaClass());
                }
            }
            
            return false;
        }
        
        return true;
    }
    
    /**
     * INTERNAL:
     */
    public boolean isValidPersistenceMethodName() {
        return (getName().startsWith(Helper.GET_PROPERTY_METHOD_PREFIX) || getName().startsWith(Helper.IS_PROPERTY_METHOD_PREFIX)) && hasAttributeName();
    }

    /**
     * INTERNAL:
     */
    public void setMetadataClass(MetadataClass metadataClass) {
        m_metadataClass = metadataClass;
    }
    
    /**
     * INTERNAL:
     */
    public void setNext(MetadataMethod next) {
        m_next = next;
    }
    
    /**
     * INTERNAL:
     */
    public void setParameters(List<String> parameters) {
        m_parameters = parameters;
    }
    
    /**
     * INTERNAL:
     */
    public void setReturnType(String returnType) {
        m_returnType = returnType;
        setType(returnType);
    }
}
