/**
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.webservice.deployment;

// $Id: ServiceDescription.java,v 1.40.2.24 2005/07/04 17:14:46 tdiesler Exp $

import org.jboss.axis.Constants;
import org.jboss.axis.encoding.DefaultTypeMappingImpl;
import org.jboss.axis.encoding.TypeMapping;
import org.jboss.axis.enums.Style;
import org.jboss.axis.enums.Use;
import org.jboss.logging.Logger;
import org.jboss.webservice.metadata.jaxrpcmapping.ExceptionMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaWsdlMappingFactory;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaXmlTypeMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.MethodParamPartsMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.ServiceEndpointInterfaceMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.ServiceEndpointMethodMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.WsdlMessageMapping;
import org.jboss.webservice.metadata.wsdl.WSDL11DefinitionFactory;
import org.jboss.util.xml.DOMUtils;
import org.jboss.xb.binding.NamespaceRegistry;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.wsdl.Binding;
import javax.wsdl.BindingInput;
import javax.wsdl.BindingOperation;
import javax.wsdl.BindingOutput;
import javax.wsdl.Definition;
import javax.wsdl.Input;
import javax.wsdl.Message;
import javax.wsdl.Output;
import javax.wsdl.Part;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.Service;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.wsdl.extensions.soap.SOAPBody;
import javax.wsdl.extensions.soap.SOAPHeader;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLWriter;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.rpc.ServiceException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * Abstracts an Axis service description
 *
 * @author thomas.diesler@jboss.org
 * @since 08-June-2004
 */
public class ServiceDescription
{
   // provide logging
   private final Logger log = Logger.getLogger(ServiceDescription.class);

   private Definition wsdlDefinition;
   private Service wsdlService;
   private Binding wsdlBinding;
   private JavaWsdlMapping javaWsdlMapping;

   private Style style;
   private Use use;
   private HashMap operations = new HashMap();
   private HashMap typeMappings = new HashMap();

   // Maps namespace uri to prefix
   private NamespaceRegistry nsRegistry = new NamespaceRegistry();

   // Set<QName> user type qnames
   private HashSet userTypes = new HashSet();

   private Properties callProperties;

   /**
    * Construct the service description from a given wsdl and jaxrpc-mapping.xml
    */
   public ServiceDescription(Definition wsdlDefinition, JavaWsdlMapping javaMapping, URL ws4eeMetaData, String portName) throws ServiceException
   {
      this.wsdlDefinition = wsdlDefinition;
      this.wsdlService = getWsdlService(wsdlDefinition, portName);
      this.wsdlBinding = getWsdlBinding(wsdlService, portName);

      this.javaWsdlMapping = javaMapping;

      initServiceDescription();

      mergeDeploymentMetaData(ws4eeMetaData, portName);
   }

   /**
    * This is mainly for testing, the WSDDGenerator has a main entry
    */
   public ServiceDescription(URL wsdlLocation, URL jaxrpcLocation, String portName) throws Exception
   {
      WSDL11DefinitionFactory wsdlFactory = WSDL11DefinitionFactory.newInstance();
      wsdlFactory.setFeature(WSDL11DefinitionFactory.FEATURE_VERBOSE, true);

      this.wsdlDefinition = wsdlFactory.parse(wsdlLocation);
      this.wsdlService = getWsdlService(wsdlDefinition, portName);
      this.wsdlBinding = getWsdlBinding(wsdlService, portName);

      if (jaxrpcLocation != null)
      {
         JavaWsdlMappingFactory mappingFactory = JavaWsdlMappingFactory.newInstance();
         this.javaWsdlMapping = mappingFactory.parse(jaxrpcLocation);
      }
      initServiceDescription();
   }

   /**
    * Initialize this service description
    */
   private void initServiceDescription() throws ServiceException
   {
      initServiceStyle();
      initServiceUse();
      initOperations();
      initTypeMappings();
   }

   public Definition getWsdlDefinition()
   {
      return wsdlDefinition;
   }

   public JavaWsdlMapping getJavaWsdlMapping()
   {
      return javaWsdlMapping;
   }

   public Service getWsdlService()
   {
      return wsdlService;
   }

   public Binding getWsdlBinding()
   {
      return wsdlBinding;
   }

   public NamespaceRegistry getNamespaceRegistry()
   {
      return nsRegistry;
   }

   public Style getStyle()
   {
      return style;
   }

   public Use getUse()
   {
      return use;
   }

   public String[] getOperationNames()
   {
      String[] names = new String[operations.size()];
      return (String[])operations.keySet().toArray(names);
   }

   public OperationDescription getOperation(String name)
   {
      return (OperationDescription)operations.get(name);
   }

   public Iterator getOperations()
   {
      return operations.values().iterator();
   }

   public QName[] getTypMappingNames()
   {
      QName[] names = new QName[typeMappings.size()];
      return (QName[])typeMappings.keySet().toArray(names);
   }

   public Iterator getTypMappings()
   {
      return typeMappings.values().iterator();
   }

   public TypeMappingDescription getTypeMapping(QName qname)
   {
      return (TypeMappingDescription)typeMappings.get(qname);
   }

   public void dumpWsdlDefinition(OutputStream out) throws WSDLException, IOException
   {
      WSDLFactory wsdlFactory = WSDLFactory.newInstance();
      WSDLWriter wsdlWriter = wsdlFactory.newWSDLWriter();
      wsdlWriter.writeWSDL(wsdlDefinition, out);
   }

   public Properties getCallProperties()
   {
      return callProperties;
   }

   public void setCallProperties(Properties callProperties)
   {
      this.callProperties = callProperties;
   }

   /**
    * Merge an axis style wsdd with this service description
    */
   private void mergeDeploymentMetaData(URL ws4eeMetaData, String portName) throws ServiceException
   {
      if (ws4eeMetaData == null)
      {
         log.debug("No ws4ee deployment meta data available");
         return;
      }

      log.debug("Merging with ws4ee deployment meta data: " + ws4eeMetaData);

      try
      {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         factory.setNamespaceAware(true);
         DocumentBuilder builder = factory.newDocumentBuilder();

         Element rootElement = null;
         InputStream is = ws4eeMetaData.openStream();
         try
         {
            rootElement = builder.parse(is).getDocumentElement();
         }
         finally
         {
            is.close();
         }

         // The root element can have zero ore more port elements
         Element portElement = null;
         NodeList nlistRoot = rootElement.getElementsByTagName("port");
         for (int i = 0; portElement == null && i < nlistRoot.getLength(); i++)
         {
            Element el = (Element)nlistRoot.item(i);
            String name = el.getAttribute("name");
            if (name.equals(portName))
               portElement = el;
         }

         // Check if we have an import
         if (portElement != null)
         {
            log.debug("Using port element: " + portName);
            Element importElement = DOMUtils.getFirstChildElement(portElement);
            if (importElement.getLocalName().equals("import"))
            {
               String importFile = importElement.getFirstChild().getNodeValue();
               log.debug("Using import: " + importFile);

               String resource = ws4eeMetaData.toExternalForm();
               resource = resource.substring(0, resource.lastIndexOf("/")) + "/" + importFile;
               InputStream isImport = new URL(resource).openStream();
               try
               {
                  portElement = builder.parse(isImport).getDocumentElement();
               }
               finally
               {
                  is.close();
               }
            }
         }

         // There was no port element, so use the root
         if (portElement == null)
            portElement = rootElement;

         mergeTypeMappings(portElement);

         mergeOperations(portElement);

      }
      catch (Exception e)
      {
         throw new ServiceException(e);
      }
   }

   private void mergeTypeMappings(Element portElement)
   {
      ArrayList mergedTypeMappings = new ArrayList();

      NodeList nlistPort = portElement.getElementsByTagName("typeMapping");
      for (int i = 0; i < nlistPort.getLength(); i++)
      {
         Element el = (Element)nlistPort.item(i);
         QName qname = DOMUtils.getAttributeValueAsQName(el, "qname");
         String javaType = DOMUtils.getAttributeValue(el, "type");

         if (mergedTypeMappings.contains(qname))
            throw new IllegalArgumentException("Cannot add/replace the same type mapping twice: " + qname);

         mergedTypeMappings.add(qname);

         if (javaType.startsWith("java:"))
            javaType = javaType.substring(5);

         TypeMappingDescription tm = (TypeMappingDescription)typeMappings.get(qname);
         if (tm != null)
            log.debug("Replacing typeMapping: " + qname);
         else
            log.debug("Addinging typeMapping: " + qname);

         tm = new TypeMappingDescription(qname, null, javaType, use, null);
         tm.setEncodingURI(el.getAttribute("encodingStyle"));
         tm.setSerializerFactoryName(el.getAttribute("serializer"));
         tm.setDeserializerFactoryName(el.getAttribute("deserializer"));
         tm.setUserDefined(true);

         typeMappings.put(qname, tm);

         BeanXMLMetaData metaData = BeanXMLMetaData.parse(DOMUtils.getFirstChildElement(el));
         tm.setMetaData(metaData);
      }
   }

   private void mergeOperations(Element portElement)
   {
      ArrayList mergedOperations = new ArrayList();

      NodeList nlistOperation = portElement.getElementsByTagName("operation");
      for (int i = 0; i < nlistOperation.getLength(); i++)
      {
         Element elOp = (Element)nlistOperation.item(i);
         String opName = elOp.getAttribute("name");
         QName opQName = DOMUtils.getAttributeValueAsQName(elOp, "qname");
         QName returnQName = DOMUtils.getAttributeValueAsQName(elOp, "returnQName");
         QName returnType = DOMUtils.getAttributeValueAsQName(elOp, "returnType");

         if (mergedOperations.contains(opName))
            throw new IllegalArgumentException("Cannot add/replace the same operation twice: " + opName);

         mergedOperations.add(opName);

         OperationDescription op = (OperationDescription)operations.get(opName);
         if (op != null)
            log.debug("Replacing operation: " + opName);
         else
            log.debug("Addinging operation: " + opName);


         if (opQName == null)
            opQName = new QName(opName);

         op = new OperationDescription(opName, opQName);
         op.setReturnQName(returnQName);
         op.setReturnType(returnType);
         operations.put(opName, op);

         NodeList paramList = elOp.getElementsByTagName("parameter");
         for (int j = 0; j < paramList.getLength(); j++)
         {
            Element elParam = (Element)paramList.item(j);
            String paramName = elParam.getAttribute("name");
            QName paramQName = DOMUtils.getAttributeValueAsQName(elParam, "qname");
            String mode = elParam.getAttribute("mode");
            QName type = DOMUtils.getAttributeValueAsQName(elParam, "type");
            boolean inHeader = DOMUtils.getAttributeValueAsBoolean(elParam, "inHeader");
            boolean outHeader = DOMUtils.getAttributeValueAsBoolean(elParam, "outHeader");

            if (paramQName == null)
               paramQName = new QName(paramName);

            OperationDescription.Parameter param = new OperationDescription.Parameter(paramName, paramQName, mode, type);
            param.setInHeader(inHeader);
            param.setOutHeader(outHeader);
            op.addParameter(param);
         }

         NodeList faultList = elOp.getElementsByTagName("fault");
         for (int j = 0; j < faultList.getLength(); j++)
         {
            Element elFault = (Element)faultList.item(j);
            String faultName = DOMUtils.getAttributeValue(elFault, "name");
            QName faultQName = DOMUtils.getAttributeValueAsQName(elFault, "qname");
            String javaType = DOMUtils.getAttributeValue(elFault, "class");
            QName faultType = DOMUtils.getAttributeValueAsQName(elFault, "type");

            OperationDescription.Fault fault = new OperationDescription.Fault(faultName, faultQName, javaType, faultType);
            op.addFault(fault);
         }
      }
   }

   /**
    * Get the service style, should be 'rpc' or 'document'
    */
   private void initServiceStyle()
   {
      Iterator itBinding = wsdlBinding.getExtensibilityElements().iterator();
      while (itBinding.hasNext())
      {
         ExtensibilityElement exElement = (ExtensibilityElement)itBinding.next();
         if (exElement instanceof SOAPBinding)
         {
            SOAPBinding soapBinding = (SOAPBinding)exElement;
            Style bindingStyle = Style.getStyle(soapBinding.getStyle());
            if (style == null && bindingStyle != null)
               style = bindingStyle;

            if (style != null && bindingStyle != null && style.equals(bindingStyle) == false)
               throw new IllegalArgumentException("Unsupported mix of style attributes " + style + "/" + bindingStyle);
         }
      }

      if (style == null)
      {
         log.warn("Cannot find any style attribute for binding: " + wsdlBinding.getQName());
         style = Style.RPC;
      }
   }

   /**
    * Get the service use, should be 'encoded' or 'literal'
    */
   private void initServiceUse()
   {
      Iterator itOp = wsdlBinding.getBindingOperations().iterator();
      while (itOp.hasNext())
      {
         BindingOperation wsdlBindingOperation = (BindingOperation)itOp.next();

         BindingInput wsdlBindingInput = wsdlBindingOperation.getBindingInput();
         if (wsdlBindingInput != null)
         {
            Iterator itIn = wsdlBindingInput.getExtensibilityElements().iterator();
            while (itIn.hasNext())
            {
               Object obj = itIn.next();
               if (obj instanceof SOAPBody)
               {
                  SOAPBody soapBody = (SOAPBody)obj;
                  Use soapBodyUse = Use.getUse(soapBody.getUse());
                  if (use == null)
                  {
                     if (soapBodyUse != null)
                     {
                        use = soapBodyUse;
                     }
                     else
                     {
                        use = Use.getUse("literal");
                     }
                  }
                  if (use != null && soapBodyUse != null && use.equals(soapBodyUse) == false)
                     throw new IllegalArgumentException("Unsupported mix of use attributes " + use + "/" + soapBodyUse);
               }
            }
         }

         BindingOutput wsdlBindingOutput = wsdlBindingOperation.getBindingOutput();
         if (wsdlBindingOutput != null)
         {
            Iterator itOut = wsdlBindingOutput.getExtensibilityElements().iterator();
            while (itOut.hasNext())
            {
               Object obj = itOut.next();
               if (obj instanceof SOAPBody)
               {
                  SOAPBody soapBody = (SOAPBody)obj;
                  Use soapBodyUse = Use.getUse(soapBody.getUse());
                  if (use == null)
                  {
                     if (soapBodyUse != null)
                     {
                        use = soapBodyUse;
                     }
                     else
                     {
                        use = Use.getUse("literal");
                     }
                  }
                  if (use != null && soapBodyUse != null && use.equals(soapBodyUse) == false)
                     throw new IllegalArgumentException("Unsupported mix of use attributes " + use + "/" + soapBodyUse);
               }
            }
         }
      }

      if (use == null)
      {
         log.warn("Cannot find any use attribute for binding: " + wsdlBinding.getQName());
         use = Use.LITERAL;
      }
   }

   /**
    * Init the Service operations
    */
   private void initOperations() throws ServiceException
   {
      PortType wsdlPortType = wsdlBinding.getPortType();
      QName portTypeQName = wsdlPortType.getQName();

      Iterator itOp = wsdlPortType.getOperations().iterator();
      while (itOp.hasNext())
      {
         javax.wsdl.Operation wsdlOperation = (javax.wsdl.Operation)itOp.next();
         String opWsdlName = wsdlOperation.getName();
         String opJavaName = opWsdlName;

         QName opQName = null;

         // Use the namespace uri from the port type
         if (portTypeQName.getNamespaceURI().length() > 0)
         {
            opQName = new QName(portTypeQName.getNamespaceURI(), opWsdlName);
            opQName = nsRegistry.registerQName(opQName);
         }
         else
         {
            opQName = new QName(opWsdlName);
         }

         // Replace the opName with the java method name from jaxrpc-mapping.xml
         ServiceEndpointMethodMapping seMethodMapping = getServiceEndpointMethodMapping(wsdlPortType, opWsdlName);
         if (seMethodMapping != null)
         {
            opJavaName = seMethodMapping.getJavaMethodName();
         }

         OperationDescription opDesc = new OperationDescription(opJavaName, opQName);
         operations.put(opJavaName, opDesc);

         Output wsdlOutput = wsdlOperation.getOutput();
         Input wsdlInput = wsdlOperation.getInput();

         if (wsdlInput != null && wsdlOutput == null)
         {
            log.debug("Using one-way call semantics: " + opWsdlName);
            opDesc.setOneWay(true);
         }

         // [TDI] Workaround for document style operations that don't have a parameter
         // http://jira.jboss.com/jira/browse/JBWS-70
         if (style == Style.DOCUMENT && seMethodMapping != null)
         {
            boolean inParams = seMethodMapping.getMethodParamPartsMappings().length > 0;
            if (wsdlInput != null && wsdlInput.getMessage().getParts().size() > 0 && inParams == false)
            {
               log.debug("jaxrpc-mapping does not have any <method-param-parts-mapping>, ignoring wsdl parts");
               wsdlInput = null;
            }

            boolean outParams = seMethodMapping.getWsdlReturnValueMapping() != null;
            if (wsdlOutput != null && wsdlOutput.getMessage().getParts().size() > 0 && outParams == false)
            {
               log.debug("jaxrpc-mapping does not have any <wsdl-return-value-mapping>, ignoring wsdl parts");
               wsdlOutput = null;
            }
         }

         if (wsdlOutput != null)
         {
            Message wsdlMessageOut = wsdlOutput.getMessage();
            Iterator outParts = wsdlMessageOut.getOrderedParts(null).iterator();
            while (outParts.hasNext())
            {
               Part wsdlPart = (Part)outParts.next();
               String paramName = wsdlPart.getName();
               QName typeQName = wsdlPart.getTypeName();

               boolean outHeader = isHeaderParam(opWsdlName, paramName);

               QName element = wsdlPart.getElementName();
               if (typeQName == null && element != null)
                  typeQName = element;

               if (typeQName != null)
               {
                  typeQName = nsRegistry.registerQName(typeQName);
                  userTypes.add(typeQName);
               }

               // Get the param mode from jaxrpc-mapping
               String paramMode = null;
               if (seMethodMapping != null)
               {
                  MethodParamPartsMapping[] mppMappings = seMethodMapping.getMethodParamPartsMappings();
                  for (int i = 0; paramMode == null && i < mppMappings.length; i++)
                  {
                     WsdlMessageMapping wmMapping = mppMappings[i].getWsdlMessageMapping();
                     if (paramName.equals(wmMapping.getWsdlMessagePartName()))
                     {
                        paramMode = wmMapping.getParameterMode();
                     }
                  }
               }

               // The first out part is the return if it is not also an input
               // and if there is no parameter mode given for it in jaxrpc-mapping
               if (opDesc.getReturnType() == null && wsdlOperation.getInput().getMessage().getPart(paramName) == null && paramMode == null)
               {
                  QName returnQName = getParameterQName(wsdlPart);
                  opDesc.setReturnQName(returnQName);
                  opDesc.setReturnType(typeQName);
               }

               // If this was not the return parameter, add it as operation parameter
               else
               {
                  if (paramMode == null)
                     paramMode = "OUT";

                  QName paramQName = getParameterQName(wsdlPart);
                  OperationDescription.Parameter param = new OperationDescription.Parameter(paramName, paramQName, paramMode, typeQName);
                  param.setOutHeader(outHeader);
                  opDesc.addParameter(param);
               }
            }
         }

         // Process the IN parts
         if (wsdlInput != null)
         {
            Message wsdlMessageIn = wsdlInput.getMessage();
            Iterator inParts = wsdlMessageIn.getOrderedParts(null).iterator();
            while (inParts.hasNext())
            {
               Part wsdlPart = (Part)inParts.next();
               String paramName = wsdlPart.getName();
               QName typeQName = wsdlPart.getTypeName();

               boolean inHeader = isHeaderParam(opWsdlName, paramName);

               if (typeQName == null && wsdlPart.getElementName() != null)
                  typeQName = wsdlPart.getElementName();

               if (typeQName != null)
               {
                  typeQName = nsRegistry.registerQName(typeQName);
                  userTypes.add(typeQName);
               }

               OperationDescription.Parameter param = opDesc.getParameterForName(paramName);
               if (param != null)
               {
                  param.setMode("INOUT");
                  param.setInHeader(inHeader);
               }
               else
               {
                  QName paramQName = getParameterQName(wsdlPart);
                  param = new OperationDescription.Parameter(paramName, paramQName, "IN", typeQName);
                  param.setInHeader(inHeader);
                  opDesc.addParameter(param);
               }
            }
         }

         // Reorder the parameters
         reorderOperationParameters(opDesc, wsdlOperation);

         // Process the fault parts
         Iterator inFaults = wsdlOperation.getFaults().values().iterator();
         while (inFaults.hasNext())
         {
            javax.wsdl.Fault wsdlFault = (javax.wsdl.Fault)inFaults.next();
            Part wsdlPart = (Part)wsdlFault.getMessage().getParts().values().iterator().next();
            String partName = wsdlPart.getName();
            QName typeQName = wsdlPart.getTypeName();
            QName faultQName = wsdlPart.getElementName();

            if (typeQName == null && faultQName != null)
               typeQName = faultQName;

            if (typeQName != null)
               typeQName = nsRegistry.registerQName(typeQName);

            if (faultQName != null)
               faultQName = nsRegistry.registerQName(faultQName);

            String javaType = null;
            if (javaWsdlMapping != null)
            {
               // Try exception mapping first
               QName wsdlMessageName = wsdlFault.getMessage().getQName();
               ExceptionMapping exceptionMapping = javaWsdlMapping.getExceptionMappingForMessageQName(wsdlMessageName);
               if (exceptionMapping != null)
               {
                  javaType = exceptionMapping.getExceptionType();
               }
               // Try type mapping next
               else
               {
                  JavaXmlTypeMapping javaMapping = javaWsdlMapping.getTypeMappingForQName(typeQName);
                  if (javaMapping != null)
                     javaType = javaMapping.getJavaType();
               }
            }

            // Try axis default type mapping
            if (javaType == null)
            {
               TypeMapping defMapping = DefaultTypeMappingImpl.getSingleton();
               Class typeClass = defMapping.getClassForQName(typeQName);
               if (typeClass != null)
                  javaType = typeClass.getName();
            }

            if (javaType == null)
            {
               String packageName = getPackageName(typeQName);
               javaType = packageName + "." + typeQName.getLocalPart();
               log.warn("Guessing fault java type from qname: " + javaType);
            }

            OperationDescription.Fault fault = new OperationDescription.Fault(partName, faultQName, javaType, typeQName);
            opDesc.addFault(fault);
         }
      }

      if (operations.size() == 0)
         log.warn("Cannot find any operations for portType: " + wsdlPortType.getQName());
   }

   /** Reorder the parameters */
   private void reorderOperationParameters(OperationDescription opDesc, javax.wsdl.Operation wsdlOperation)
   {
      // Order according to the parameterOrder from the wsdl portType operation
      List wsdlParamOrder = wsdlOperation.getParameterOrdering();
      if (wsdlParamOrder != null && wsdlParamOrder.size() > 0)
      {
         ArrayList orderedParams = new ArrayList();
         Iterator it = wsdlParamOrder.iterator();
         while (it.hasNext())
         {
            String paramName = (String)it.next();
            OperationDescription.Parameter opParam = opDesc.getParameterForName(paramName);
            if (opParam == null)
            {
               throw new IllegalArgumentException("Operation paramerter appears in wsdl paramOrder, " +
                       "but not in operation: " + paramName);
            }
            orderedParams.add(opParam);
         }
         opDesc.setParameters(orderedParams);
      }
   }

   /** Get the paramter QName
    * @param wsdlPart
    */
   private QName getParameterQName(Part wsdlPart)
   {
      String partName = wsdlPart.getName();
      QName elementName = wsdlPart.getElementName();
      QName paramQName = (elementName != null ? elementName : new QName(partName));

      if (paramQName.getNamespaceURI().equals("") == false)
         paramQName = nsRegistry.registerQName(paramQName);

      return paramQName;
   }

   /**
    * Get the service endpoint method mapping from jaxrpc-mapping.xml for a given port type and operation name.
    */
   private ServiceEndpointMethodMapping getServiceEndpointMethodMapping(PortType wsdlPortType, String opWsdlName)
   {
      ServiceEndpointMethodMapping seiMethodMapping = null;
      if (javaWsdlMapping != null)
      {
         ServiceEndpointInterfaceMapping seiMapping = javaWsdlMapping.getServiceEndpointInterfaceMappingByPortType(wsdlPortType.getQName());
         if (seiMapping != null)
         {
            seiMethodMapping = seiMapping.getServiceEndpointMethodMappingByWsdlOperation(opWsdlName);
         }
      }
      return seiMethodMapping;
   }

   private boolean isHeaderParam(String opName, String paramName)
   {
      boolean inHeader = false;

      BindingOperation wsdlBindingOperation = wsdlBinding.getBindingOperation(opName, null, null);
      if (wsdlBindingOperation != null)
      {
         BindingInput bindingInput = wsdlBindingOperation.getBindingInput();
         if (bindingInput != null)
         {
            Iterator itIn = bindingInput.getExtensibilityElements().iterator();
            while (inHeader == false && itIn.hasNext())
            {
               ExtensibilityElement exElement = (ExtensibilityElement)itIn.next();
               if (exElement instanceof SOAPHeader)
               {
                  SOAPHeader soapHeader = (SOAPHeader)exElement;
                  inHeader = soapHeader.getPart().equals(paramName);
               }
            }
         }

         BindingOutput bindingOutput = wsdlBindingOperation.getBindingOutput();
         if (bindingOutput != null)
         {
            Iterator itOut = bindingOutput.getExtensibilityElements().iterator();
            while (inHeader == false && itOut.hasNext())
            {
               ExtensibilityElement exElement = (ExtensibilityElement)itOut.next();
               if (exElement instanceof SOAPHeader)
               {
                  SOAPHeader soapHeader = (SOAPHeader)exElement;
                  inHeader = soapHeader.getPart().equals(paramName);
               }
            }
         }
      }
      else
      {
         log.warn("Cannot obtain binding operation for: " + opName);
      }

      return inHeader;
   }

   /**
    * Append the typeMapping information, which is taken from the jaxrpc-mapping.xml
    */
   private void initTypeMappings() throws ServiceException
   {
      // Remove the QNames for which we don't need an explicit type mapping
      Iterator itRem = userTypes.iterator();
      while (itRem.hasNext())
      {
         QName typeQName = (QName)itRem.next();
         String typeURI = typeQName.getNamespaceURI();

         // No typeMapping for xsd:foo and soap:bar
         if (typeURI.equals(Constants.URI_DEFAULT_SCHEMA_XSD) || typeURI.equals(Constants.URI_SOAP11_ENC))
            itRem.remove();
      }

      // There is a chance that the custom type is not expicitly mapped in jaxrpc-mapping.xml
      // Walk through all the remaining userTypes and try to find the corresponding class
      Iterator it = userTypes.iterator();
      while (it.hasNext())
      {
         QName typeQName = (QName)it.next();
         String typeURI = typeQName.getNamespaceURI();

         String prefix = (String)nsRegistry.getPrefix(typeURI);
         if (prefix == null)
            throw new IllegalStateException("Cannot find uri in registry: " + typeURI);

         QName anonymousQName = null;
         String javaType = null;

         // Lookup the java type from jaxrpc-mapping
         JavaXmlTypeMapping javaTypeMapping = null;
         if (javaWsdlMapping != null)
         {
            javaTypeMapping = javaWsdlMapping.getTypeMappingForQName(typeQName);
            if (javaTypeMapping != null)
            {
               anonymousQName = javaTypeMapping.getAnonymousTypeQName();
               javaType = javaTypeMapping.getJavaType();
            }
         }

         if (javaType == null)
         {
            // Guess the java type from the qname
            String packageName = getPackageName(typeQName);
            String localPart = typeQName.getLocalPart();
            javaType = packageName + "." + localPart;
            log.debug("Guessing the javaType from typeQName: " + typeQName + " -> " + javaType);
         }

         TypeMappingDescription typeMapping = new TypeMappingDescription(typeQName, anonymousQName, javaType, use, javaTypeMapping);
         typeMappings.put(typeQName, typeMapping);
         it.remove();
      }

      // Add the type mappings from jaxrpc-mapping that are not top level types
      if (javaWsdlMapping != null)
      {
         JavaXmlTypeMapping[] javaXmlTypeMappings = javaWsdlMapping.getJavaXmlTypeMappings();
         for (int i = 0; i < javaXmlTypeMappings.length; i++)
         {
            JavaXmlTypeMapping javaTypeMapping = javaXmlTypeMappings[i];
            QName anonymousQName = javaTypeMapping.getAnonymousTypeQName();
            String javaType = javaTypeMapping.getJavaType();
            QName typeQName = javaTypeMapping.getRootTypeQName();

            // If the RootTypeQName is not given, try the AnonymousTypeQName
            if (typeQName == null && anonymousQName != null)
               typeQName = anonymousQName;

            typeQName = nsRegistry.registerQName(typeQName);
            TypeMapping defaultTm = DefaultTypeMappingImpl.getSingleton();
            if (typeQName != null && typeMappings.get(typeQName) == null && defaultTm.getClassForQName(typeQName) == null)
            {
               TypeMappingDescription typeMapping = new TypeMappingDescription(typeQName, anonymousQName, javaType, use, javaTypeMapping);
               typeMappings.put(typeQName, typeMapping);
            }
         }
      }
   }

   private String getPackageName(QName typeQName) throws ServiceException
   {
      String packageName = null;

      if (javaWsdlMapping != null)
      {
         packageName = javaWsdlMapping.getPackageTypeForURI(typeQName.getNamespaceURI());
         if (packageName == null)
            throw new IllegalArgumentException("Cannot find package type for: " + typeQName);
      }
      else
      {
         try
         {
            URI uri = new URI(typeQName.getNamespaceURI());
            String reverse = uri.getHost();
            StringTokenizer st = new StringTokenizer(reverse, ".");

            while (st.hasMoreTokens())
            {
               if (packageName == null)
                  packageName = st.nextToken();
               else
                  packageName = st.nextToken() + "." + packageName;
            }

            log.debug("Using type uri to obtain package: " + uri + " -> " + packageName);
         }
         catch (URISyntaxException e)
         {
            throw new ServiceException(e);
         }
      }

      return packageName;
   }


   /**
    * Get the wsdl service that corresponds to the portName
    * If portName is null, it assumes there is only one service.
    */
   private Service getWsdlService(Definition wsdlDefinition, String portName)
   {
      Service wsdlService = null;

      if (portName == null)
      {
         if (wsdlDefinition.getServices().values().size() != 1)
            throw new IllegalArgumentException("Unsupported number of service elements");

         wsdlService = (Service)wsdlDefinition.getServices().values().iterator().next();
      }
      else
      {
         Iterator it = wsdlDefinition.getServices().values().iterator();
         while (wsdlService == null && it.hasNext())
         {
            Service service = (Service)it.next();
            if (service.getPort(portName) != null)
               wsdlService = service;
         }
      }

      if (wsdlService == null)
         throw new IllegalArgumentException("Cannot find wsdl service for port: " + portName);

      return wsdlService;
   }


   /**
    * Get the wsdl binding with for the given service and portName
    * If portName is null, it iterates over the available bindings and checks that their are not multiple
    * definitions.
    */
   private Binding getWsdlBinding(Service wsdlService, String portName)
   {
      Binding wsdlBinding = null;

      if (portName != null)
      {
         Port port = wsdlService.getPort(portName);
         if (port == null)
            throw new IllegalArgumentException("Cannot find wsdl port for: " + portName);

         wsdlBinding = port.getBinding();
      }
      else
      {
         Iterator it = wsdlService.getPorts().values().iterator();
         while (it.hasNext())
         {
            Port port = (Port)it.next();
            Binding binding = port.getBinding();

            if (wsdlBinding != null && wsdlBinding.getQName().equals(binding.getQName()) == false)
               throw new IllegalArgumentException("Multiple bindings not supported for service: " + wsdlService.getQName());

            if (wsdlBinding == null)
               wsdlBinding = binding;
         }
      }

      if (wsdlBinding == null)
         throw new IllegalArgumentException("Cannot find wsdl binding for: " + wsdlService.getQName());

      return wsdlBinding;
   }
}
