/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Axis" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
package org.jboss.axis.description;

import org.jboss.axis.AxisServiceConfig;
import org.jboss.axis.Constants;
import org.jboss.axis.InternalException;
import org.jboss.axis.encoding.DefaultTypeMappingImpl;
import org.jboss.axis.encoding.TypeMapping;
import org.jboss.axis.encoding.TypeMappingRegistry;
import org.jboss.axis.encoding.TypeMappingRegistryImpl;
import org.jboss.axis.enums.Style;
import org.jboss.axis.enums.Use;
import org.jboss.axis.message.SOAPBodyElementAxisImpl;
import org.jboss.axis.message.SOAPEnvelopeAxisImpl;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.axis.utils.bytecode.ParamNameExtractor;
import org.jboss.axis.wsdl.Skeleton;
import org.jboss.axis.wsdl.fromJava.Namespaces;
import org.jboss.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import javax.xml.rpc.holders.Holder;
import javax.xml.rpc.server.ServiceLifecycle;
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.
 * <p/>
 * 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 ServiceDesc
{

   private static Logger log = Logger.getLogger(ServiceDesc.class.getName());

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

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

   /**
    * List if disallowed methods
    */
   private List disallowedMethods = 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;

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

   /**
    * 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;

   /**
    * 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 Method skelMethod = null;

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

   /**
    * Lookup caches
    */
   private HashMap name2OperationsMap = null;
   private HashMap qname2OperationsMap = null;
   private HashMap method2OperationMap = new HashMap();
   private 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 = DefaultTypeMappingImpl.getSingleton();

   private TypeMappingRegistry tmr;

   private boolean haveAllSkeletonMethods;
   private boolean introspectionComplete;

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

   /**
    * 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)
      {
         return;
      }
      catch (IllegalArgumentException e)
      {
         return;
      }
      catch (InvocationTargetException e)
      {
         return;
      }
      haveAllSkeletonMethods = true;
   }

   public TypeMapping getTypeMapping()
   {
      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;
   }

   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 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);
      }
      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 ((isWrapped() || ((style == Style.MESSAGE) && (getDefaultNamespace() == null))) && (name2OperationsMap != null))
         {
            // Try ignoring the namespace....?
            overloads = (ArrayList)name2OperationsMap.get(qname.getLocalPart());
         }

         // 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.
    * <p/>
    * 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 opDesc, Class implClass)
   {
      log.debug("Enter: syncOperationToClass\n" + opDesc);

      // ------------------------------------------------
      // 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 (opDesc.getMethod() != null)
         return;

      // Find the method.  We do this once for each Operation.

      Method[] methods = implClass.getDeclaredMethods();
      // 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(opDesc.getName()))
         {

            log.debug("Sync method: " + method);

            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!");
               }
               opDesc.setMessageOperationStyle(messageOperType);
            }

            // Check params
            Class[] methodParamTypes = method.getParameterTypes();
            if (methodParamTypes.length != opDesc.getNumParams())
            {
               log.debug("Number of parameters don't match");
               if (style == Style.DOCUMENT && opDesc.getNumParams() == 1)
               {
                  ParameterDesc paramDesc = opDesc.getParameter(0);
                  QName typeQName = paramDesc.getTypeQName();
                  Class javaType = tm.getClassForQName(typeQName);
                  if (javaType == null)
                     throw new IllegalStateException("Cannot obtain java type for: " + typeQName);
                  methodParamTypes = new Class[] { javaType };
               }
               else
               {
                  continue;
               }
            }

            int j;
            boolean conversionNecessary = false;
            for (j = 0; j < methodParamTypes.length; j++)
            {
               Class methodParamType = methodParamTypes[j];
               Class actualType = methodParamType;

               log.debug("Sync param: " + j);

               if (Holder.class.isAssignableFrom(methodParamType))
                  actualType = JavaUtils.getHolderValueType(methodParamType);

               ParameterDesc param = opDesc.getParameter(j);
               Class javaType = param.getJavaType();

               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 = tm.getTypeQName(methodParamType);
                  log.debug("Setting param TypeQName: " + typeQName);
                  param.setTypeQName(typeQName);
                  log.debug("Setting param class: " + actualType);
                  param.setJavaType(actualType);
               }
               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).
                  if (javaType != null && JavaUtils.getHolderValueType(javaType) != null)
                  {
                     javaType = JavaUtils.getHolderValueType(javaType);
                     log.debug("Setting param class to holder type: " + javaType);
                  }
                  if (javaType == null)
                  {
                     javaType = tm.getClassForQName(typeQName);
                     if (javaType != null)
                     {
                        if (param.getMode() == ParameterDesc.IN || JavaUtils.getHolderValueType(javaType) != null)
                        {
                           log.debug("Setting param class from typeQName: " + javaType);
                           param.setJavaType(javaType);
                        }
                     }
                     else
                     {
                        log.debug("Setting param class from methodParamType: " + methodParamType);
                        param.setJavaType(methodParamType);
                     }
                  }

                  if (javaType != null && style != Style.DOCUMENT)
                  {
                     // 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(javaType, actualType, true) == false)
                     {
                        log.debug("Param class is not convertible: [param=" + javaType + ",actual=" + actualType + "]");
                        break;
                     }

                     if (!actualType.isAssignableFrom(javaType))
                     {
                        // This doesn't fit without conversion
                        conversionNecessary = true;
                        log.debug("Actual type is not assignable from param class: [param=" + javaType + ",actual=" + actualType + "]");
                     }
                  }
                  
                  javaType = param.getJavaType();
                  if (javaType == null || javaType.equals(methodParamType) == false)
                  {
                     if (style != Style.DOCUMENT)
                     {
                        log.debug("Setting param class from methodParamType: " + methodParamType);
                        param.setJavaType(methodParamType);
                     }
                  }
               }
            }

            if (j != methodParamTypes.length)
            {
               // failed.
               continue;
            }

            // This is our latest possibility
            possibleMatch = method;
            log.debug("Possible match: " + possibleMatch);

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

            log.debug("Conversion still necessary");
         }
      }

      // 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)
      {
         ParameterDesc retParamDesc = opDesc.getReturnParamDesc();
         QName retTypeQName = retParamDesc.getTypeQName();
         Class retType = retParamDesc.getJavaType();
         if (retTypeQName != null && retType == null)
            retType = tm.getClassForQName(retTypeQName);

         if (retType != null)
         {
            log.debug("Setting return type: " + retType);
            opDesc.setReturnClass(retType);
         }

         if (possibleMatch != null)
         {
            // Do the faults
            createFaultMetadata(possibleMatch, opDesc);

            opDesc.setMethod(possibleMatch);
            method2OperationMap.put(possibleMatch, opDesc);
            log.debug("Setting operation method: " + possibleMatch);
         }
      }

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

            log.debug("No match found, trying super class");
            syncOperationToClass(opDesc, superClass);
         }
      }

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

      log.debug("Exit: syncOperationToClass");
   }

   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] == SOAPBodyElementAxisImpl[].class) && (method.getReturnType() == SOAPBodyElementAxisImpl[].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] == SOAPEnvelopeAxisImpl.class) && (params[1] == SOAPEnvelopeAxisImpl.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 (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 = implClass.getDeclaredMethods();

      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)
         {
            return;
         }
         catch (IllegalArgumentException e)
         {
            return;
         }
         catch (InvocationTargetException 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);
   }

   /**
    * Look for methods matching this name, and for each one, create an
    * OperationDesc (if it's not already in our list).
    * <p/>
    * 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 = implClass.getDeclaredMethods();

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

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

   /**
    * Make an OperationDesc from a Java method.
    * <p/>
    * 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;
               }
            }
         }
      }

      // Make an OperationDesc, fill in common stuff
      OperationDesc operation = new OperationDesc();
      operation.setName(method.getName());
      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, method.getName()));
      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);
         operation.setReturnType(tm.getTypeQName(method.getReturnType()));

         String[] paramNames = getParamNames(method);

         for (int k = 0; k < paramTypes.length; k++)
         {
            Class type = paramTypes[k];
            ParameterDesc paramDesc = new ParameterDesc();
            String opNamespace = 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(opNamespace, paramNames[k]));
            }
            else
            {
               paramDesc.setQName(new QName(opNamespace, "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(tm.getTypeQName(heldClass));
            }
            else
            {
               paramDesc.setMode(ParameterDesc.IN);
               paramDesc.setTypeQName(tm.getTypeQName(type));
            }
            paramDesc.setJavaType(type);
            operation.addParameter(paramDesc);
         }
      }

      createFaultMetadata(method, operation);

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

   private void createFaultMetadata(Method method, OperationDesc operation)
   {

      // Create Exception Types
      Class[] exceptionTypes = new Class[method.getExceptionTypes().length];
      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.jboss.axis.AxisFault.class && !ex.getName().startsWith("java.") && !ex.getName().startsWith("javax."))
         {

            log.debug("createFaultMetadata: " + ex);

            // 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);

            // If we didn't find one, create a new one
            if (fault == null)
            {
               fault = new FaultDesc();
               log.debug("Creating new fault desc: " + fault);
            }

            // Try to fil in any parts of the faultDesc that aren't there

            // XMLType
            QName xmlType = fault.getXmlType();
            if (xmlType == null)
            {
               QName typeQName = tm.getTypeQName(ex);
               fault.setXmlType(typeQName);
               log.debug("Setting XMLType: " + typeQName);
            }

            // Name and Class Name
            String pkgAndClsName = ex.getName();
            if (fault.getClassName() == null)
            {
               fault.setClassName(pkgAndClsName);
               log.debug("Setting ClassName: " + pkgAndClsName);
            }

            if (fault.getName() == null)
            {
               String name = pkgAndClsName.substring(pkgAndClsName.lastIndexOf('.') + 1, pkgAndClsName.length());
               fault.setName(name);
               log.debug("Setting Name: " + name);
            }

            // Parameters
            // We add a single parameter which points to the type
            if (fault.getParameters() == null)
            {
               if (xmlType == null)
               {
                  xmlType = tm.getTypeQName(ex);
               }
               QName qname = fault.getQName();
               if (qname == null)
               {
                  qname = new QName("", "fault");
               }
               ParameterDesc param = new ParameterDesc(qname, ParameterDesc.IN, xmlType, null, false, false);
               param.setJavaType(ex);
               ArrayList exceptionParams = new ArrayList();
               exceptionParams.add(param);
               fault.setParameters(exceptionParams);
            }

            // QName
            if (fault.getQName() == null)
            {
               fault.setQName(new QName(pkgAndClsName));
            }

            // 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();
      }
      return tmr;
   }

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