/*
 * Copyright 2002-2004 The Apache Software Foundation.
 * 
 * Licensed 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.axis.description;

import org.apache.axis.AxisServiceConfig;
import org.apache.axis.Constants;
import org.apache.axis.InternalException;
import org.apache.axis.AxisProperties;
import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.encoding.*;
import org.apache.axis.constants.Style;
import org.apache.axis.constants.Use;
import org.apache.axis.message.SOAPBodyElement;
import org.apache.axis.message.SOAPEnvelope;
import org.apache.axis.utils.JavaUtils;
import org.apache.axis.utils.Messages;
import org.apache.axis.utils.bytecode.ParamNameExtractor;
import org.apache.axis.wsdl.Skeleton;
import org.apache.axis.wsdl.fromJava.Namespaces;
import org.apache.commons.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import javax.xml.rpc.holders.Holder;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;


/**
 * A ServiceDesc is an abstract description of a service.
 *
 * ServiceDescs contain OperationDescs, which are descriptions of operations.
 * The information about a service's operations comes from one of two places:
 * 1) deployment, or 2) introspection.
 *
 * @author Glen Daniels (gdaniels@apache.org)
 */
public class JavaServiceDesc implements ServiceDesc {
    protected static Log log =
            LogFactory.getLog(JavaServiceDesc.class.getName());

    /** The name of this service */
    private String name = null;

    /** The documentation of this service */
	private String documentation = null;

    /** Style/Use */
    private Style style = Style.RPC;
    private Use   use   = Use.ENCODED;

    // Style and Use are related.  By default, if Style==RPC, Use should be
    // ENCODED.  But if Style==DOCUMENT, Use should be LITERAL.  So we want
    // to keep the defaults synced until someone explicitly sets the Use.
    private boolean useSet = false;

    /** Our operations - a list of OperationDescs */
    private ArrayList operations = new ArrayList();

    /** A collection of namespaces which will map to this service */
    private List namespaceMappings = null;

    /**
     * Where does our WSDL document live?  If this is non-null, the "?WSDL"
     * generation will automatically return this file instead of dynamically
     * creating a WSDL.  BE CAREFUL because this means that Handlers will
     * not be able to add to the WSDL for extensions/headers....
     */
    private String wsdlFileName = null;

    /**
     * An endpoint URL which someone has specified for this service.  If
     * this is set, WSDL generation will pick it up instead of defaulting
     * to the transport URL.
     */
    private String endpointURL = null;

    /** Place to store user-extensible service-related properties */
    private HashMap properties = null;

    /** Lookup caches */
    private HashMap name2OperationsMap = null;
    private HashMap qname2OperationsMap = null;
    private transient HashMap method2OperationMap = new HashMap();
    
    // THE FOLLOWING STUFF IS ALL JAVA-SPECIFIC, AND WILL BE FACTORED INTO
    // A JAVA-SPECIFIC SUBCLASS.  --Glen

    /** List of allowed methods */
    /** null allows everything, an empty ArrayList allows nothing */
    private List allowedMethods = null;

    /** List if disallowed methods */
    private List disallowedMethods = null;

    /** Implementation class */
    private Class implClass = null;

    /**
     * Is the implementation a Skeleton?  If this is true, it will generate
     * a Fault to provide OperationDescs via WSDD.
     */
    private boolean isSkeletonClass = false;

    /** Cached copy of the skeleton "getOperationDescByName" method */
    private transient Method skelMethod = null;

    /** Classes at which we should stop looking up the inheritance chain
     *  when introspecting
     */
    private ArrayList stopClasses = null;

    /** Lookup caches */
    private transient HashMap method2ParamsMap = new HashMap();
    private OperationDesc messageServiceDefaultOp = null;

    /** Method names for which we have completed any introspection necessary */
    private ArrayList completedNames = new ArrayList();

    /** Our typemapping for resolving Java<->XML type issues */
    private TypeMapping tm = null;
    private TypeMappingRegistry tmr = null;

    private boolean haveAllSkeletonMethods = false;
    private boolean introspectionComplete = false;

    /**
     * Default constructor
     */
    public JavaServiceDesc() {
    }

    /**
     * What kind of service is this?
     * @return
     */
    public Style getStyle() {
        return style;
    }

    public void setStyle(Style style) {
        this.style = style;
        if (!useSet) {
            // Use hasn't been explicitly set, so track style
            use = style == Style.RPC ? Use.ENCODED : Use.LITERAL;
        }
    }


    /**
     * What kind of use is this?
     * @return
     */
    public Use getUse() {
        return use;
    }

    public void setUse(Use use) {
        useSet = true;
        this.use = use;
    }

    /**
     * Determine whether or not this is a "wrapped" invocation, i.e. whether
     * the outermost XML element of the "main" body element represents a
     * method call, with the immediate children of that element representing
     * arguments to the method.
     *
     * @return true if this is wrapped (i.e. RPC or WRAPPED style),
     *         false otherwise
     */
    public boolean isWrapped()
    {
        return ((style == Style.RPC) || 
                (style == Style.WRAPPED));
    }

    /**
     * the wsdl file of the service.
     * When null, it means that the wsdl should be autogenerated
     * @return filename or null
     */
    public String getWSDLFile() {
        return wsdlFileName;
    }

    /**
     * set the wsdl file of the service; this causes the named
     * file to be returned on a ?wsdl, probe, not introspection
     * generated wsdl.
     * @param wsdlFileName filename or null to re-enable introspection
     */
    public void setWSDLFile(String wsdlFileName) {
        this.wsdlFileName = wsdlFileName;
    }

    public List getAllowedMethods() {
        return allowedMethods;
    }

    public void setAllowedMethods(List allowedMethods) {
        this.allowedMethods = allowedMethods;
    }

    public Class getImplClass() {
        return implClass;
    }

    /**
     * set the implementation class
     * <p>
     * Warning: You cannot call getInitializedServiceDesc() after setting this
     * as it uses this to indicate its work has already been done.
     *
     * @param implClass
     * @throws IllegalArgumentException if the implementation class is already
     *         set
     */
    public void setImplClass(Class implClass) {
        if (this.implClass != null)
            throw new IllegalArgumentException(
                    Messages.getMessage("implAlreadySet"));

        this.implClass = implClass;
        if (Skeleton.class.isAssignableFrom(implClass)) {
            isSkeletonClass = true;
            loadSkeletonOperations();
        }
    }

    private void loadSkeletonOperations() {
        Method method = null;
        try {
            method = implClass.getDeclaredMethod("getOperationDescs",
                                                 new Class [] {});
        } catch (NoSuchMethodException e) {
        } catch (SecurityException e) {
        }
        if (method == null) {
            // FIXME : Throw an error?
            return;
        }

        try {
            Collection opers = (Collection)method.invoke(implClass, null);
            for (Iterator i = opers.iterator(); i.hasNext();) {
                OperationDesc skelDesc = (OperationDesc)i.next();
                addOperationDesc(skelDesc);
            }
        } catch (IllegalAccessException e) {
            if(log.isDebugEnabled()) {
                log.debug(Messages.getMessage("exception00"), e);
            }
            return;
        } catch (IllegalArgumentException e) {
            if(log.isDebugEnabled()) {
                log.debug(Messages.getMessage("exception00"), e);
            }
            return;
        } catch (InvocationTargetException e) {
            if(log.isDebugEnabled()) {
                log.debug(Messages.getMessage("exception00"), e);
            }
            return;
        }
        haveAllSkeletonMethods = true;
    }

    public TypeMapping getTypeMapping() {
        if(tm == null) {
            return DefaultTypeMappingImpl.getSingletonDelegate();
//            throw new RuntimeException(Messages.getMessage("noDefaultTypeMapping00"));
        }
        return tm;
    }

    public void setTypeMapping(TypeMapping tm) {
        this.tm = tm;
    }

    /**
     * the name of the service
     */
    public String getName() {
        return name;
    }

    /**
     * the name of the service
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * get the documentation for the service
     */
	public String getDocumentation() {
    	return documentation; 
    }

    /**
     * set the documentation for the service
     */
	public void setDocumentation(String documentation) {
    	this.documentation = documentation;
    }

    public ArrayList getStopClasses() {
        return stopClasses;
    }

    public void setStopClasses(ArrayList stopClasses) {
        this.stopClasses = stopClasses;
    }

    public List getDisallowedMethods() {
        return disallowedMethods;
    }

    public void setDisallowedMethods(List disallowedMethods) {
        this.disallowedMethods = disallowedMethods;
    }

    public void removeOperationDesc(OperationDesc operation) {
        operations.remove(operation);
        operation.setParent(null);

        if (name2OperationsMap != null) {
            String name = operation.getName();
            ArrayList overloads = (ArrayList)name2OperationsMap.get(name);
            if (overloads != null) {
                overloads.remove(operation);
                if (overloads.size() == 0) {
                    name2OperationsMap.remove(name);
                }
            }
        }
        
        if (qname2OperationsMap != null) {
            QName qname = operation.getElementQName();
            ArrayList list = (ArrayList)qname2OperationsMap.get(qname);
            if (list != null) {
                list.remove(operation);
            }
        }
        
        if (method2OperationMap != null) {
            Method method = operation.getMethod();
            if (method != null) {
                method2OperationMap.remove(method);
            }
        }
    }
    
    public void addOperationDesc(OperationDesc operation)
    {
        operations.add(operation);
        operation.setParent(this);
        if (name2OperationsMap == null) {
            name2OperationsMap = new HashMap();
        }

        // Add name to name2Operations Map
        String name = operation.getName();
        ArrayList overloads = (ArrayList)name2OperationsMap.get(name);
        if (overloads == null) {
            overloads = new ArrayList();
            name2OperationsMap.put(name, overloads);
        } else if (JavaUtils.isTrue(
                AxisProperties.getProperty(Constants.WSIBP11_COMPAT_PROPERTY)) &&
                overloads.size() > 0) {
            throw new RuntimeException(Messages.getMessage("noOverloadedOperations", name));
        }
        overloads.add(operation);
    }

    /**
     * get all the operations as a list of OperationDescs.
     * this method triggers an evaluation of the valid operations by
     * introspection, so use sparingly
     * @return reference to the operations array. This is not a copy
     */
    public ArrayList getOperations()
    {
        loadServiceDescByIntrospection();  // Just in case...
        return operations;
    }

    /**
     * get all overloaded operations by name
     * @param methodName
     * @return null for no match, or an array of OperationDesc objects
     */
    public OperationDesc [] getOperationsByName(String methodName)
    {
        getSyncedOperationsForName(implClass, methodName);

        if (name2OperationsMap == null)
            return null;

        ArrayList overloads = (ArrayList)name2OperationsMap.get(methodName);
        if (overloads == null) {
            return null;
        }

        OperationDesc [] array = new OperationDesc [overloads.size()];
        return (OperationDesc[])overloads.toArray(array);
    }

    /**
     * Return an operation matching the given method name.  Note that if we
     * have multiple overloads for this method, we will return the first one.
     * @return null for no match
     */
    public OperationDesc getOperationByName(String methodName)
    {
        // If we need to load up operations from introspection data, do it.
        // This returns fast if we don't need to do anything, so it's not very
        // expensive.
        getSyncedOperationsForName(implClass, methodName);

        if (name2OperationsMap == null)
            return null;

        ArrayList overloads = (ArrayList)name2OperationsMap.get(methodName);
        if (overloads == null) {
            return null;
        }

        return (OperationDesc)overloads.get(0);
    }

    /**
     * Map an XML QName to an operation.  Returns the first one it finds
     * in the case of mulitple matches.
     * @return null for no match
     */
    public OperationDesc getOperationByElementQName(QName qname)
    {
        OperationDesc [] overloads = getOperationsByQName(qname);

        // Return the first one....
        if ((overloads != null) && overloads.length > 0)
            return overloads[0];

        return null;
    }

    /**
     * Return all operations which match this QName (i.e. get all the
     * overloads)
     * @return null for no match
     */
    public OperationDesc [] getOperationsByQName(QName qname)
    {
        // Look in our mapping of QNames -> operations.

        // But first, let's make sure we've initialized said mapping....
        initQNameMap();

        ArrayList overloads = (ArrayList)qname2OperationsMap.get(qname);
        if (overloads == null) {
            // Nothing specifically matching this QName.
            if (name2OperationsMap != null) {
                if ((isWrapped() ||
                     ((style == Style.MESSAGE) &&
                      (getDefaultNamespace() == null)))) {
                    // Try ignoring the namespace....?
                    overloads = (ArrayList) name2OperationsMap.get(qname.getLocalPart());
                } else {
                    // TODO the above code is weird: a JavaServiceDesc can  be document or rpc and
                    // still define a WSDL operation using a wrapper style  mapping.
                    // The following code handles this case.
                    Object ops = name2OperationsMap.get(qname.getLocalPart());
                    if (ops != null) {
                        overloads = new ArrayList((Collection) ops);
                        for (Iterator iter = overloads.iterator(); iter.hasNext();) {
                            OperationDesc operationDesc = (OperationDesc) iter.next();
                            if (Style.WRAPPED != operationDesc.getStyle()) {
                                iter.remove();
                            }
                        }
                    }
                }
            }
            // Handle the case where a single Message-style operation wants
            // to accept anything.
            if ((style == Style.MESSAGE) && (messageServiceDefaultOp != null))
                return new OperationDesc [] { messageServiceDefaultOp };

            if (overloads == null)
                return null;
        }

        getSyncedOperationsForName(implClass,
                                   ((OperationDesc)overloads.get(0)).getName());

        // Sort the overloads by number of arguments - prevents us calling methods
        // with more parameters than supplied in the request (with missing parameters
        // defaulted to null) when a perfectly good method exists with exactly the
        // supplied parameters.
        Collections.sort(overloads,
            new Comparator() {
                public int compare(Object o1, Object o2)
                {
                    Method meth1 = ((OperationDesc)o1).getMethod();
                    Method meth2 = ((OperationDesc)o2).getMethod();
                    return (meth1.getParameterTypes().length -
                                         meth2.getParameterTypes().length);
                }
            });

        OperationDesc [] array = new OperationDesc [overloads.size()];
        return (OperationDesc[])overloads.toArray(array);
    }

    private synchronized void initQNameMap() {
        if (qname2OperationsMap == null) {
            loadServiceDescByIntrospection();

            qname2OperationsMap = new HashMap();
            for (Iterator i = operations.iterator(); i.hasNext();) {
                OperationDesc operationDesc = (OperationDesc) i.next();
                QName qname = operationDesc.getElementQName();
                ArrayList list = (ArrayList)qname2OperationsMap.get(qname);
                if (list == null) {
                    list = new ArrayList();
                    qname2OperationsMap.put(qname, list);
                }
                list.add(operationDesc);
            }
        }
    }

    /**
     * Synchronize an existing OperationDesc to a java.lang.Method.
     *
     * This method is used when the deployer has specified operation metadata
     * and we want to match that up with a real java Method so that the
     * Operation-level dispatch carries us all the way to the implementation.
     * Search the declared methods on the implementation class to find one
     * with an argument list which matches our parameter list.
     */
    private void syncOperationToClass(OperationDesc oper, Class implClass)
    {
        // ------------------------------------------------
        // Developer Note:
        //
        // The goal of the sync code is to associate
        // the OperationDesc/ParamterDesc with the
        // target Method.  There are a number of ways to get to this
        // point depending on what information
        // is available.  Here are the main scenarios:
        //
        // A) Deployment with wsdd (non-skeleton):
        //   * OperationDesc/ParameterDesc loaded from deploy.wsdd
        //   * Loaded ParameterDesc does not have javaType,
        //     so it is discovered using the TypeMappingRegistry
        //     (also loaded via deploy.wsdd) and the
        //     typeQName specified by the ParameterDesc.
        //   * Sync occurs using the discovered
        //     javaTypes and the javaTypes of the Method
        //     parameters
        //
        // B) Deployment with no wsdd OperationDesc info (non-skeleton):
        //   * Implementation Class introspected to build
        //     OperationDesc/ParameterDesc.
        //   * ParameterDesc is known via introspection.
        //   * ParameterDesc are discovered using javaType
        //     and TypeMappingRegistry.
        //   * Sync occurs using the introspected
        //     javaTypes and the javaTypes of the Method
        //     parameters
        //
        // C) Deployment with wsdd (skeleton):
        //   * OperationDesc/ParameterDesc loaded from the Skeleton
        //   * In this scenario the ParameterDescs' already
        //     have javaTypes (see E below).
        //   * Sync occurs using the ParameterDesc
        //     javaTypes and the javaTypes of the Method
        //     parameters.
        //
        // D) Commandline Java2WSDL loading non-Skeleton Class/Interface
        //   * Class/Interface introspected to build
        //     OperationDesc/ParameterDesc.
        //   * The javaTypes of the ParameterDesc are set using introspection.
        //   * typeQNames are determined for built-in types using
        //     from the default TypeMappingRegistry.  Other
        //     typeQNames are guessed from the javaType.  Note
        //     that there is no loaded TypeMappingRegistry.
        //   * Sync occurs using the ParameterDesc
        //     javaTypes and the javaTypes of the Method
        //     parameters.
        //
        // E) Commandline Java2WSDL loading Skeleton Class
        //   * OperationDesc/ParameterDesc loaded from Skeleton
        //   * Each ParameterDesc has an appropriate typeQName
        //   * Each ParameterDesc also has a javaType, which is
        //     essential for sync'ing up with the
        //     method since there is no loaded TypeMappingRegistry.
        //   * Syncronization occurs using the ParameterDesc
        //     javaTypes and the javaTypes of the Method
        //     parameters.
        //
        // So in each scenario, the ultimate sync'ing occurs
        // using the javaTypes of the ParameterDescs and the
        // javaTypes of the Method parameters.
        //
        // ------------------------------------------------

        // If we're already mapped to a Java method, no need to do anything.
        if (oper.getMethod() != null)
            return;

        // Find the method.  We do this once for each Operation.
        
        Method[] methods = getMethods(implClass);
        // A place to keep track of possible matches
        Method possibleMatch = null;
        
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if (Modifier.isPublic(method.getModifiers()) &&
                    method.getName().equals(oper.getName()) &&
                    method2OperationMap.get(method) == null) {

                if (style == Style.MESSAGE) {
                    int messageOperType = checkMessageMethod(method);
                    if(messageOperType == OperationDesc.MSG_METHOD_NONCONFORMING) continue;
                    if (messageOperType == -1) {
                        throw new InternalException("Couldn't match method to any of the allowable message-style patterns!");
                    }
                    oper.setMessageOperationStyle(messageOperType);

                    // Don't bother checking params if we're message style
                    possibleMatch = method;
                    break;
                }

                // Check params
                Class [] paramTypes = method.getParameterTypes();
                if (paramTypes.length != oper.getNumParams())
                    continue;

                int j;
                boolean conversionNecessary = false;
                for (j = 0; j < paramTypes.length; j++) {
                    Class type = paramTypes[j];
                    Class actualType = type;
                    if (Holder.class.isAssignableFrom(type)) {
                        actualType = JavaUtils.getHolderValueType(type);
                    }
                    ParameterDesc param = oper.getParameter(j);
                    QName typeQName = param.getTypeQName();
                    if (typeQName == null) {
                        // No typeQName is available.  Set it using
                        // information from the actual type.
                        // (Scenarios B and D)
                        // There is no need to try and match with
                        // the Method parameter javaType because
                        // the ParameterDesc is being constructed
                        // by introspecting the Method.
                        typeQName = getTypeMapping().getTypeQName(actualType);
                        param.setTypeQName(typeQName);
                    } else {
                        // A type qname is available.
                        // Ensure that the ParameterDesc javaType
                        // is convertable to the Method parameter type
                        //
                        // Use the available javaType (Scenarios C and E)
                        // or get one from the TMR (Scenario A).
                        Class paramClass = param.getJavaType();
                        if (paramClass != null &&
                            JavaUtils.getHolderValueType(paramClass) != null) {
                            paramClass = JavaUtils.getHolderValueType(paramClass);
                        }
                        if (paramClass == null) {
                            paramClass = getTypeMapping().getClassForQName(param.getTypeQName(),
                                                                           type);
                        }

                        if (paramClass != null) {
                            // This is a match if the paramClass is somehow
                            // convertable to the "real" parameter type.  If not,
                            // break out of this loop.
                            if (!JavaUtils.isConvertable(paramClass, actualType)) {
                                break;
                            }
                            
                            if (!actualType.isAssignableFrom(paramClass)) {
                                // This doesn't fit without conversion
                                conversionNecessary = true;
                            }
                        }
                    }
                    // In all scenarios the ParameterDesc javaType is set to
                    // match the javaType in the corresponding parameter.
                    // This is essential.
                    param.setJavaType(type);
                }

                if (j != paramTypes.length) {
                    // failed.
                    continue;
                }
                
                // This is our latest possibility
                possibleMatch = method;

                // If this is exactly it, stop now.  Otherwise keep looking
                // just in case we find a better match.
                if (!conversionNecessary) {
                    break;
                }

            }
        }

        // At this point, we may or may not have a possible match.
        // FIXME : Should we prefer an exact match from a base class over
        //         a with-conversion match from the target class?  If so,
        //         we'll need to change the logic below.
        if (possibleMatch != null) {
            Class returnClass = possibleMatch.getReturnType();
            oper.setReturnClass(returnClass);
            
            QName returnType = oper.getReturnType();
            if (returnType == null) {
                oper.setReturnType(getTypeMapping().getTypeQName(returnClass));
            }

            // Do the faults
            createFaultMetadata(possibleMatch, oper);
                
            oper.setMethod(possibleMatch);
            method2OperationMap.put(possibleMatch, oper);
            return;
        }

        // Didn't find a match.  Try the superclass, if appropriate
        Class superClass = implClass.getSuperclass();
        if (superClass != null &&
                !superClass.getName().startsWith("java.") &&
                !superClass.getName().startsWith("javax.") &&
                (stopClasses == null ||
                          !stopClasses.contains(superClass.getName()))) {
            syncOperationToClass(oper, superClass);
        }

        // Exception if sync fails to find method for operation
        if (oper.getMethod() == null) {
            InternalException ie =
                new InternalException(Messages.getMessage("serviceDescOperSync00",
                                                           oper.getName(),
                                                           implClass.getName()));
            throw ie;
        }
    }

    private Method[] getMethods(Class implClass) {
        if (implClass.isInterface()){
            // only return methods that are not part of start classes
            List methodsList = new ArrayList();
            Method[] methods = implClass.getMethods();
            if (methods != null) {
                for (int i = 0; i < methods.length; i++) {
                    String declaringClass = methods[i].getDeclaringClass().getName();
                    if (!declaringClass.startsWith("java.") &&
                        !declaringClass.startsWith("javax.")) {
                        methodsList.add(methods[i]);
                    }
                }
            }
            return (Method[])methodsList.toArray(new Method[]{}); 
        } else {
            return implClass.getDeclaredMethods();
        }
    }

    private int checkMessageMethod(Method method) {
        // Collect the types so we know what we're dealing with in the target
        // method.
        Class [] params = method.getParameterTypes();

        if (params.length == 1) {
            if ((params[0] == Element[].class) &&
                    (method.getReturnType() == Element[].class)) {
                return OperationDesc.MSG_METHOD_ELEMENTARRAY;
            }

            if ((params[0] == SOAPBodyElement[].class) &&
                    (method.getReturnType() == SOAPBodyElement[].class)) {
                return OperationDesc.MSG_METHOD_BODYARRAY;
            }

            if ((params[0] == Document.class) &&
                    (method.getReturnType() == Document.class)) {
                return OperationDesc.MSG_METHOD_DOCUMENT;
            }
        } else if (params.length == 2) {
            if (((params[0] == SOAPEnvelope.class) &&
                    (params[1] == SOAPEnvelope.class)) || 
                ((params[0] == javax.xml.soap.SOAPEnvelope.class) &&
                    (params[1] == javax.xml.soap.SOAPEnvelope.class)) &&
                    (method.getReturnType() == void.class)){
                return OperationDesc.MSG_METHOD_SOAPENVELOPE;
            }
        }
        if( null != allowedMethods && !allowedMethods.isEmpty() )
          throw new InternalException (Messages.getMessage("badMsgMethodParams",
                                                         method.getName()));
        return    OperationDesc.MSG_METHOD_NONCONFORMING;                                              
    }

    /**
     * Fill in a service description by introspecting the implementation
     * class.
     */
    public void loadServiceDescByIntrospection()
    {
        loadServiceDescByIntrospection(implClass);

        // Setting this to null means there is nothing more to do, and it
        // avoids future string compares.
        completedNames = null;
    }

    /**
     * Fill in a service description by introspecting the implementation
     * class.
     */
    public void loadServiceDescByIntrospection(Class implClass) {
        if (introspectionComplete || implClass == null) {
            return;
        }

        // set the implementation class for the service description
        this.implClass = implClass;
        if (Skeleton.class.isAssignableFrom(implClass)) {
            isSkeletonClass = true;
            loadSkeletonOperations();
        }

        /** If the class knows what it should be exporting,
        * respect its wishes.
        */
        AxisServiceConfig axisConfig = null;
        try {
            Method method = implClass.getDeclaredMethod(
                    "getAxisServiceConfig", new Class [] {});
            if (method != null && Modifier.isStatic(method.getModifiers())) {
                axisConfig = (AxisServiceConfig)method.invoke(null, null);
            }
        } catch (Exception e) {
            // No problem, just continue without...
        }

        if (axisConfig != null) {
            String allowedMethodsStr = axisConfig.getAllowedMethods();
            if (allowedMethodsStr != null && !"*".equals(allowedMethodsStr)) {
                ArrayList methodList = new ArrayList();
                StringTokenizer tokenizer =
                        new StringTokenizer(allowedMethodsStr, " ,");
                while (tokenizer.hasMoreTokens()) {
                    methodList.add(tokenizer.nextToken());
                }
                setAllowedMethods(methodList);
            }
        }

        loadServiceDescByIntrospectionRecursive(implClass);

        // All operations should now be synchronized.  Check it.
        for (Iterator iterator = operations.iterator(); iterator.hasNext();) {
            OperationDesc operation = (OperationDesc) iterator.next();
            if (operation.getMethod() == null) {
                throw new InternalException(
                        Messages.getMessage("badWSDDOperation",
                                            operation.getName(),
                                            "" + operation.getNumParams()));
            }
        }

        if ((style == Style.MESSAGE) && operations.size() == 1) {
            messageServiceDefaultOp = (OperationDesc)operations.get(0);
        }

        introspectionComplete = true;
    }

    /**
     * Is this method from ServiceLifeCycle interface?
     * @param m
     * @return true if this method is from ServiceLifeCycle interface
     */ 
    private boolean isServiceLifeCycleMethod(Class implClass, Method m) {
        if(javax.xml.rpc.server.ServiceLifecycle.class.isAssignableFrom(implClass)) {
            String methodName = m.getName(); 

            if(methodName.equals("init")) {
                // Check if the method signature is 
                // "public abstract void init(Object context) throws ServiceException;"
                Class[] classes = m.getParameterTypes(); 
                if(classes != null && 
                   classes.length == 1 && 
                   classes[0] == Object.class && 
                   m.getReturnType() == Void.TYPE) {
                    return true;
                }
            } else if (methodName.equals("destroy")){
                // Check if the method signature is 
                // "public abstract void destroy();"
                Class[] classes = m.getParameterTypes(); 
                if(classes != null && 
                   classes.length == 0 &&
                   m.getReturnType() == Void.TYPE) {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Recursive helper class for loadServiceDescByIntrospection
     */
    private void loadServiceDescByIntrospectionRecursive(Class implClass)
    {
        if (Skeleton.class.equals(implClass)) {
            return;
        }

        Method [] methods = getMethods(implClass);

        for (int i = 0; i < methods.length; i++) {
            if (Modifier.isPublic(methods[i].getModifiers()) && !isServiceLifeCycleMethod(implClass, methods[i])) {
                getSyncedOperationsForName(implClass, methods[i].getName());
            }
        }

        if (implClass.isInterface()) {
            Class [] superClasses = implClass.getInterfaces();
            for (int i = 0; i < superClasses.length; i++) {
                Class superClass = superClasses[i];
                if (!superClass.getName().startsWith("java.") &&
                        !superClass.getName().startsWith("javax.") &&
                        (stopClasses == null ||
                        !stopClasses.contains(superClass.getName()))) {
                    loadServiceDescByIntrospectionRecursive(superClass);
                }
            }
        } else {
            Class superClass = implClass.getSuperclass();
            if (superClass != null &&
                    !superClass.getName().startsWith("java.") &&
                    !superClass.getName().startsWith("javax.") &&
                    (stopClasses == null ||
                        !stopClasses.contains(superClass.getName()))) {
                loadServiceDescByIntrospectionRecursive(superClass);
            }
        }
    }

    /**
     * Fill in a service description by introspecting the implementation
     * class.  This version takes the implementation class and the in-scope
     * TypeMapping.
     */
    public void loadServiceDescByIntrospection(Class cls, TypeMapping tm)
    {
        // Should we complain if the implClass changes???
        implClass = cls;
        this.tm = tm;

        if (Skeleton.class.isAssignableFrom(implClass)) {
            isSkeletonClass = true;
            loadSkeletonOperations();
        }

        loadServiceDescByIntrospection();
    }

    /**
     * Makes sure we have completely synchronized OperationDescs with
     * the implementation class.
     */
    private void getSyncedOperationsForName(Class implClass, String methodName)
    {
        // If we're a Skeleton deployment, skip the statics.
        if (isSkeletonClass) {
            if (methodName.equals("getOperationDescByName") ||
                methodName.equals("getOperationDescs"))
                return;
        }
        
        // If we have no implementation class, don't worry about it (we're
        // probably on the client)
        if (implClass == null)
            return;

        // If we're done introspecting, or have completed this method, return
        if (completedNames == null || completedNames.contains(methodName))
            return;

        // Skip it if it's not a sanctioned method name
        if ((allowedMethods != null) &&
            !allowedMethods.contains(methodName))
            return;

        if ((disallowedMethods != null) &&
            disallowedMethods.contains(methodName))
            return;

        // If we're a skeleton class, make sure we don't already have any
        // OperationDescs for this name (as that might cause conflicts),
        // then load them up from the Skeleton class.
        if (isSkeletonClass && !haveAllSkeletonMethods) {
            // FIXME : Check for existing ones and fault if found

            if (skelMethod == null) {
                // Grab metadata from the Skeleton for parameter info
                try {
                    skelMethod = implClass.getDeclaredMethod(
                                            "getOperationDescByName",
                                            new Class [] { String.class });
                } catch (NoSuchMethodException e) {
                } catch (SecurityException e) {
                }
                if (skelMethod == null) {
                    // FIXME : Throw an error?
                    return;
                }
            }
            try {
                List skelList =
                        (List)skelMethod.invoke(implClass,
                                new Object [] { methodName });
                if (skelList != null) {
                    Iterator i = skelList.iterator();
                    while (i.hasNext()) {
                        addOperationDesc((OperationDesc)i.next());
                    }
                }
            } catch (IllegalAccessException e) {
                if(log.isDebugEnabled()) {
                    log.debug(Messages.getMessage("exception00"), e);
                }
                return;
            } catch (IllegalArgumentException e) {
                if(log.isDebugEnabled()) {
                    log.debug(Messages.getMessage("exception00"), e);
                }
                return;
            } catch (InvocationTargetException e) {
                if(log.isDebugEnabled()) {
                    log.debug(Messages.getMessage("exception00"), e);
                }
                return;
            }
        }

        // OK, go find any current OperationDescs for this method name and
        // make sure they're synced with the actual class.
        if (name2OperationsMap != null) {
            ArrayList currentOverloads =
                    (ArrayList)name2OperationsMap.get(methodName);
            if (currentOverloads != null) {
                // For each one, sync it to the implementation class' methods
                for (Iterator i = currentOverloads.iterator(); i.hasNext();) {
                    OperationDesc oper = (OperationDesc) i.next();
                    if (oper.getMethod() == null) {
                        syncOperationToClass(oper, implClass);
                    }
                }
            }
        }

        // Now all OperationDescs from deployment data have been completely
        // filled in.  So we now make new OperationDescs for any method
        // overloads which were not covered above.
        // NOTE : This is the "lenient" approach, which allows you to
        // specify one overload and still get the others by introspection.
        // We could equally well return above if we found OperationDescs,
        // and have a rule that if you specify any overloads, you must specify
        // all the ones you want accessible.

        createOperationsForName(implClass, methodName);

        // Note that we never have to look at this method name again.
        completedNames.add(methodName);
    }

    private String getUniqueOperationName(String name) {
        int i = 1;
        String candidate;
        do {
            candidate = name + i++;
        } while (name2OperationsMap.get(candidate) != null);

        return candidate;
    }

    /**
     * Look for methods matching this name, and for each one, create an
     * OperationDesc (if it's not already in our list).
     *
     * TODO: Make this more efficient
     */
    private void createOperationsForName(Class implClass, String methodName)
    {
        // If we're a Skeleton deployment, skip the statics.
        if (isSkeletonClass) {
            if (methodName.equals("getOperationDescByName") ||
                methodName.equals("getOperationDescs"))
                return;
        }
        
        Method [] methods = getMethods(implClass);

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if (Modifier.isPublic(method.getModifiers()) &&
                method.getName().equals(methodName) &&
                !isServiceLifeCycleMethod(implClass, method)) {
                createOperationForMethod(method);
            }
        }

        Class superClass = implClass.getSuperclass();
        if (superClass != null &&
                !superClass.getName().startsWith("java.") &&
                !superClass.getName().startsWith("javax.") &&
                    (stopClasses == null ||
                        !stopClasses.contains(superClass.getName()))) {
            createOperationsForName(superClass, methodName);
        }
    }

    /**
     * Make an OperationDesc from a Java method.
     *
     * In the absence of deployment metadata, this code will introspect a
     * Method and create an appropriate OperationDesc.  If the class
     * implements the Skeleton interface, we will use the metadata from there
     * in constructing the OperationDesc.  If not, we use parameter names
     * from the bytecode debugging info if available, or "in0", "in1", etc.
     * if not.
     */
    private void createOperationForMethod(Method method) {
        // If we've already got it, never mind
        if (method2OperationMap.get(method) != null) {
            return;
        }

        Class [] paramTypes = method.getParameterTypes();

        // And if we've already got an exact match (i.e. an override),
        // never mind

        ArrayList overloads = name2OperationsMap == null ? null :
                (ArrayList)name2OperationsMap.get(method.getName());
        if (overloads != null && !overloads.isEmpty()) {
            // Search each OperationDesc that already has a Method
            // associated with it, and check for parameter type equivalence.
            for (int i = 0; i < overloads.size(); i++) {
                OperationDesc op = (OperationDesc)overloads.get(i);
                Method checkMethod = op.getMethod();
                if (checkMethod != null) {
                    Class [] others = checkMethod.getParameterTypes();
                    if (paramTypes.length == others.length) {
                        int j = 0;
                        for (; j < others.length; j++) {
                            if (!others[j].equals(paramTypes[j]))
                                break;
                        }
                        // If we got all the way through, we have a match.
                        if (j == others.length)
                            return;
                    }
                }
            }
        }

        boolean isWSICompliant = JavaUtils.isTrue(
                AxisProperties.getProperty(Constants.WSIBP11_COMPAT_PROPERTY));
        
        // Make an OperationDesc, fill in common stuff
        OperationDesc operation = new OperationDesc();
        
        // If we're WS-I compliant, we can't have overloaded operation names.
        // If we find duplicates, we generate unique names for them and map
        // those names to the correct Method.
        String name = method.getName();
        if (isWSICompliant && name2OperationsMap != null) {
            Collection methodNames = name2OperationsMap.keySet();
            name = JavaUtils.getUniqueValue(methodNames, name);
        }
        operation.setName(name);
        String defaultNS = "";
        if (namespaceMappings != null && !namespaceMappings.isEmpty()) {
            // If we have a default namespace mapping, require callers to
            // use that namespace.
            defaultNS = (String)namespaceMappings.get(0);
        }
        if(defaultNS.length() == 0) {
            defaultNS = Namespaces.makeNamespace(method.getDeclaringClass().getName());
        }
        operation.setElementQName(new QName(defaultNS, name));
        operation.setMethod(method);

        // If this is a MESSAGE style service, set up the OperationDesc
        // appropriately.
        if (style == Style.MESSAGE) {
            int messageOperType = checkMessageMethod(method);
            if(messageOperType == OperationDesc.MSG_METHOD_NONCONFORMING) return;
            if (messageOperType == -1) {
                throw new InternalException("Couldn't match method to any of the allowable message-style patterns!");
            }
            operation.setMessageOperationStyle(messageOperType);
            operation.setReturnClass(Object.class);
            operation.setReturnType(Constants.XSD_ANYTYPE);
        } else {
            // For other styles, continue here.
            Class retClass = method.getReturnType();
            operation.setReturnClass(retClass);
            QName typeQName = getTypeQName(retClass);
            operation.setReturnType(typeQName);

            String [] paramNames = getParamNames(method);

            for (int k = 0; k < paramTypes.length; k++) {
                Class type = paramTypes[k];
                ParameterDesc paramDesc = new ParameterDesc();
                // param should be unqualified if we're using rpc style,
                // or should use the operation's namespace if its document style
                String paramNamespace = (this.style == Style.RPC ? "" : operation.getElementQName().getNamespaceURI());

                // If we have a name for this param, use it, otherwise call
                // it "in*"
                if (paramNames != null && paramNames[k] != null &&
                        paramNames[k].length()>0) {
                    paramDesc.setQName(new QName(paramNamespace, paramNames[k]));
                } else {
                    paramDesc.setQName(new QName(paramNamespace, "in" + k));
                }

                // If it's a Holder, mark it INOUT, and set the XML type QName
                // to the held type.  Otherwise it's IN.

                Class heldClass = JavaUtils.getHolderValueType(type);
                if (heldClass != null) {
                    paramDesc.setMode(ParameterDesc.INOUT);
                    paramDesc.setTypeQName(getTypeQName(heldClass));
                } else {
                    paramDesc.setMode(ParameterDesc.IN);
                    paramDesc.setTypeQName(getTypeQName(type));
                }
                paramDesc.setJavaType(type);
                operation.addParameter(paramDesc);
            }
        }

        createFaultMetadata(method, operation);

        addOperationDesc(operation);
        method2OperationMap.put(method, operation);
    }

    private QName getTypeQName(Class javaClass) {
        QName typeQName;
        TypeMapping tm = getTypeMapping();
        if (style == Style.RPC) {
            typeQName = tm.getTypeQName(javaClass);
        } else {
            typeQName = tm.getTypeQNameExact(javaClass);
            if (typeQName == null && javaClass.isArray()) {
                typeQName = tm.getTypeQName(javaClass.getComponentType());
            } else {
                typeQName = tm.getTypeQName(javaClass);
            }
        }
        return typeQName;
    }

    private void createFaultMetadata(Method method, OperationDesc operation) {
        // Create Exception Types
        Class[] exceptionTypes = method.getExceptionTypes();

        for (int i=0; i < exceptionTypes.length; i++) {
            // Every remote method declares a java.rmi.RemoteException
            // Only interested in application specific exceptions.
            // Ignore java and javax package exceptions.
            Class ex = exceptionTypes[i];
            if (ex != java.rmi.RemoteException.class &&
                ex != org.apache.axis.AxisFault.class &&
                !ex.getName().startsWith("java.") &&
                !ex.getName().startsWith("javax.")) {

                // For JSR 101 v.1.0, there is a simple fault mapping
                // and a complexType fault mapping...both mappings
                // generate a class that extends (directly or indirectly)
                // Exception.
                // When converting java back to wsdl it is not possible
                // to determine which way to do the mapping,
                // so it is always mapped back using the complexType
                // fault mapping because it is more useful (i.e. it
                // establishes a hierarchy of exceptions).  Note that this
                // will not cause any roundtripping problems.
                // Rich


                /* Old Simple Type Mode
                Field[] f = ex.getDeclaredFields();
                ArrayList exceptionParams = new ArrayList();
                for (int j = 0; j < f.length; j++) {
                    int mod = f[j].getModifiers();
                    if (Modifier.isPublic(mod) &&
                         !Modifier.isStatic(mod)) {
                        QName qname = new QName("", f[j].getName());
                        QName typeQName = tm.getTypeQName(f[j].getType());
                        ParameterDesc param = new ParameterDesc(qname,
                                                                ParameterDesc.IN,
                                                                typeQName);
                        param.setJavaType(f[j].getType());
                        exceptionParams.add(param);
                    }
                }
                String pkgAndClsName = ex.getName();
                FaultDesc fault = new FaultDesc();
                fault.setName(pkgAndClsName);
                fault.setParameters(exceptionParams);
                operation.addFault(fault);
                */

                FaultDesc fault = operation.getFaultByClass(ex, false);
                boolean isNew;
                
                // If we didn't find one, create a new one
                if (fault == null) {
                    fault = new FaultDesc();
                    isNew = true;
                } else {
                    isNew = false;
                }
                
                // Try to fil in any parts of the faultDesc that aren't there
                
                // XMLType
                QName xmlType = fault.getXmlType();
                if (xmlType == null) {
                    fault.setXmlType(getTypeMapping().getTypeQName(ex));
                }
                
                // Name and Class Name
                String pkgAndClsName = ex.getName();
                if (fault.getClassName() == null) {
                    fault.setClassName(pkgAndClsName);
                }
                if (fault.getName() == null) {
                    String name = pkgAndClsName.substring(
                            pkgAndClsName.lastIndexOf('.') + 1,
                            pkgAndClsName.length());
                    fault.setName(name);
                }
                
                // Parameters
                // We add a single parameter which points to the type
                if (fault.getParameters() == null) {
                    if (xmlType == null) {
                        xmlType = getTypeMapping().getTypeQName(ex);
                    }
                    QName qname = fault.getQName();
                    if (qname == null) {
                        qname = new QName("", "fault");
                    }
                    ParameterDesc param = new ParameterDesc(
                            qname,
                            ParameterDesc.IN,
                            xmlType);
                    param.setJavaType(ex);
                    ArrayList exceptionParams = new ArrayList();
                    exceptionParams.add(param);
                    fault.setParameters(exceptionParams);
                }
                
                // QName
                if (fault.getQName() == null) {
                    fault.setQName(new QName(pkgAndClsName));
                }

                if (isNew) {
                    // Add the fault to the operation
                    operation.addFault(fault);
                }
            }
        }
    }

    private String[] getParamNames(Method method) {
        synchronized (method2ParamsMap) {
            String [] paramNames = (String []) method2ParamsMap.get(method);
            if(paramNames != null)
                return paramNames;
            paramNames = ParamNameExtractor.getParameterNamesFromDebugInfo(method);
            method2ParamsMap.put(method, paramNames);
            return paramNames;
        }
    }

    public void setNamespaceMappings(List namespaces) {
        namespaceMappings = namespaces;
    }

    public String getDefaultNamespace() {
        if (namespaceMappings == null || namespaceMappings.isEmpty())
            return null;
        return (String)namespaceMappings.get(0);
    }

    public void setDefaultNamespace(String namespace) {
        if (namespaceMappings == null)
            namespaceMappings = new ArrayList();
        namespaceMappings.add(0, namespace);
    }

    public void setProperty(String name, Object value) {
        if (properties == null) {
            properties = new HashMap();
        }
        properties.put(name, value);
    }

    public Object getProperty(String name) {
        if (properties == null)
            return null;

        return properties.get(name);
    }

    public String getEndpointURL() {
        return endpointURL;
    }

    public void setEndpointURL(String endpointURL) {
        this.endpointURL = endpointURL;
    }

    public TypeMappingRegistry getTypeMappingRegistry() {
        if (tmr == null) {
            tmr = new TypeMappingRegistryImpl(false);
        }
        return tmr;
    }

    public void setTypeMappingRegistry(TypeMappingRegistry tmr) {
        this.tmr = tmr;
    }

    public boolean isInitialized() {
        return implClass != null;
    }
}
