/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.axis2.jaxws.spi;


import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.concurrent.Executor;

import javax.activation.DataSource;
import javax.xml.bind.JAXBContext;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.Source;
import javax.xml.ws.Dispatch;
import javax.xml.ws.EndpointReference;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.WebServiceFeature;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.handler.HandlerResolver;

import org.apache.axiom.om.OMElement;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.java.security.AccessController;
import org.apache.axis2.jaxws.ExceptionFactory;
import org.apache.axis2.jaxws.addressing.util.EndpointReferenceUtils;
import org.apache.axis2.jaxws.client.PropertyMigrator;
import org.apache.axis2.jaxws.client.dispatch.JAXBDispatch;
import org.apache.axis2.jaxws.client.dispatch.XMLDispatch;
import org.apache.axis2.jaxws.client.proxy.JAXWSProxyHandler;
import org.apache.axis2.jaxws.context.listener.ProviderOMContextListener;
import org.apache.axis2.jaxws.description.DescriptionFactory;
import org.apache.axis2.jaxws.description.EndpointDescription;
import org.apache.axis2.jaxws.description.ServiceDescription;
import org.apache.axis2.jaxws.description.ServiceDescriptionWSDL;
import org.apache.axis2.jaxws.description.builder.DescriptionBuilderComposite;
import org.apache.axis2.jaxws.handler.HandlerResolverImpl;
import org.apache.axis2.jaxws.i18n.Messages;
import org.apache.axis2.jaxws.registry.FactoryRegistry;
import org.apache.axis2.jaxws.spi.migrator.ApplicationContextMigratorUtil;
import org.apache.axis2.jaxws.util.WSDLWrapper;
import org.apache.axis2.jaxws.utility.ExecutorFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The ServiceDelegate serves as the backing implementation for all of the methods in the {@link
 * javax.xml.ws.Service} API.  This is the plug point for the client implementation.
 */
public class ServiceDelegate extends javax.xml.ws.spi.ServiceDelegate {
    private static final Log log = LogFactory.getLog(ServiceDelegate.class);
    private static ThreadLocal<DescriptionBuilderComposite> sparseServiceCompositeThreadLocal = new ThreadLocal<DescriptionBuilderComposite>();
    private static ThreadLocal<DescriptionBuilderComposite> sparsePortCompositeThreadLocal = new ThreadLocal<DescriptionBuilderComposite>();
    
    private Executor executor;

    private ServiceDescription serviceDescription;
    private QName serviceQname;
    private ServiceClient serviceClient = null;

    private HandlerResolver handlerResolver = null;
    
    /**
     * NON-STANDARD SPI! Set any metadata to be used on the creation of the NEXT Service by this thread.
     * NOTE that this uses ThreadLocal to store the metadata, and that ThreadLocal is cleared after it is
     * used to create a Service.  That means:
     * 1) The thread that sets the metadata to use MUST be the thread that creates the Service
     * 2) Creation of the Service should be the very next thing the thread does
     * 3) The metadata will be set to null when the Service is created, so to create another 
     *    service with the same metadata, it will need to be set again prior to creating the
     *    service
     * 4) The metadata can be set prior to creating both generic Service and generated Service 
     *    instances.      
     * 
     * This allows creating a generic Service (javax.xml.ws.Service) or a generated Service
     * (subclass of javax.xml.ws.Service) specifying additional metadata via a
     * sparse composite.  This can be used by a runtime to create a Service for a requester using
     * additional metadata such as might come from a deployment descriptor or from resource
     * injection processing of @Resource or @WebServiceRef(s) annotations.  Additional metadata
     * may include things like @WebServiceClient.wsdlLocation or a @HandlerChain specification.
     * 
     *    @see javax.xml.ws.Service#create(QName)
     *    @see javax.xml.ws.Service#create(URL, QName)
     * 
     * @param composite Additional metadata (if any) to be used in creation of the service
     */
    static public void setServiceMetadata(DescriptionBuilderComposite composite) {
        sparseServiceCompositeThreadLocal.set(composite);
    }
    
    /**
     * NON-STANDARD SPI! Returns the composite that will be used on the creation of the next 
     * Service by this thread.
     * 
     * @see #setServiceMetadata(DescriptionBuilderComposite)
     * 
     * @return composite that will be used on the creation of the next Service by this thread, or null
     *         if no composite is to be used.
     */
    static DescriptionBuilderComposite getServiceMetadata() {
        return sparseServiceCompositeThreadLocal.get();
    }
    
    /**
     * Remove any composite so that creation of the next Service by this thread will NOT be 
     * affected by any additional metadata.
     * 
     * @see #setServiceMetadata(DescriptionBuilderComposite)
     * 
     */
    static void resetServiceMetadata() {
        sparseServiceCompositeThreadLocal.set(null);
    }
    
    /**
     * NON-STANDARD SPI! Set any metadata to be used on the creation of the NEXT Port by this thread.
     * NOTE that this uses ThreadLocal to store the metadata, and that ThreadLocal is cleared after it is
     * used to create a Port.  That means:
     * 1) The thread that sets the metadata to use MUST be the thread that creates the Port
     * 2) Creation of the Port should be the very next thing the thread does
     * 3) The metadata will be set to null when the Port is created, so to create another 
     *    Port with the same metadata, it will need to be set again prior to creating the
     *    Port
     * 4) The metadata can be set prior to creating Port which specifies a QName via 
     *    Service.getPort(QName, Class) or one that only specifies the SEI class via 
     *    Service.getPort(Class)
     * 5) Metadata can not be specified for dynamic ports, i.e. those added via 
     *    Service.addPort(...).
     * 6) Metadata can not be specfied when creating a dispatch client, i.e. via 
     *    Service.createDispatch(...)
     * 7) The Service used to create the port can be the generic service or a generated 
     *    service.      
     * 
     * This allows creating Port specifying additional metadata via a sparse composite.  
     * This can be used by a runtime to create a Port for a requester using
     * additional metadata such as might come from a deployment descriptor or from resource
     * injection processing.  Additional metadata might include things like 
     * a @HandlerChain specification.
     * 
     *    @see javax.xml.ws.Service#getPort(Class)
     *    @see javax.xml.ws.Service#getPort(QName, Class)
     * 
     * @param composite Additional metadata (if any) to be used in creation of the port
     */
    static public void setPortMetadata(DescriptionBuilderComposite composite) {
        sparsePortCompositeThreadLocal.set(composite);
    }

    /**
     * NON-STANDARD SPI! Returns the composite that will be used on the creation of the next 
     * Port by this thread.
     * 
     * @see #setPortMetadata(DescriptionBuilderComposite)
     * 
     * @return composite that will be used on the creation of the next Port by this thread, or null
     *         if no composite is to be used.
     */
    static DescriptionBuilderComposite getPortMetadata() {
        return sparsePortCompositeThreadLocal.get();
    }
    
    /**
     * Remove any composite so that creation of the next Port by this thread will NOT be 
     * affected by any additional metadata.
     * 
     * @see #setPortMetadata(DescriptionBuilderComposite)
     * 
     */
    static void resetPortMetadata() {
       sparsePortCompositeThreadLocal.set(null);
    }
    
    public ServiceDelegate(URL url, QName qname, Class clazz, WebServiceFeature... features) throws WebServiceException {
        super();
        this.serviceQname = qname;

        if (!isValidServiceName()) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("serviceDelegateConstruct0", ""));
        }
        
        if ((features != null) && (features.length != 0)) {
          throw ExceptionFactory
                  .makeWebServiceException(Messages.getMessage("serviceDelegateConstruct2", serviceQname.toString()));
        }
        
        // Get any metadata that is to be used to build up this service, then reset it so it isn't used
        // to create any other services.
        DescriptionBuilderComposite sparseComposite = getServiceMetadata();
        resetServiceMetadata();
        if (sparseComposite != null) {
            serviceDescription = DescriptionFactory.createServiceDescription(url, serviceQname, clazz, sparseComposite, this);
        } else {
            serviceDescription = DescriptionFactory.createServiceDescription(url, serviceQname, clazz);
        }
        // TODO: This check should be done when the Service Description is created above; that should throw this exception.
        // That is because we (following the behavior of the RI) require the WSDL be fully specified (not partial) on the client
        if (isValidWSDLLocation()) {
            if (!isServiceDefined(serviceQname)) {
                throw ExceptionFactory.makeWebServiceException(Messages.getMessage(
                        "serviceDelegateConstruct0", serviceQname.toString(), url.toString()));
            }
        }
        
        //TODO: Is this the best place for this code?
        ConfigurationContext context = serviceDescription.getAxisConfigContext();

        // Register the necessary ApplicationContextMigrators
        ApplicationContextMigratorUtil.addApplicationContextMigrator(context,
                Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID, new PropertyMigrator());
    }
    
    //================================================
    // JAX-WS API methods
    //================================================

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#addPort(javax.xml.namespace.QName, java.lang.String, java.lang.String)
    */
    // Creates a DISPATCH ONLY port.  Per JAXWS Sec 4.1 javax.xm..ws.Service, p. 49, ports added via addPort method
    // are only suitibale for creating Distpach instances.
    public void addPort(QName portName, String bindingId, String endpointAddress)
            throws WebServiceException {
        verifyServiceDescriptionActive();
        if (log.isDebugEnabled()) {
            log.debug("Calling addPort : ("
                      + portName + "," + bindingId + "," + endpointAddress + ")");
        }
    	if(endpointAddress!=null && endpointAddress.trim().length()==0){
    		ExceptionFactory.makeWebServiceException(Messages.getMessage("addPortErr1", (portName!=null)?portName.getLocalPart():"", endpointAddress));
    	}
        EndpointDescription endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, null, portName,
                                                  DescriptionFactory.UpdateType.ADD_PORT, this, bindingId, endpointAddress);
        // TODO: Need to set endpointAddress and set or check bindingId on the EndpointDesc
        endpointDesc.setEndpointAddress(endpointAddress);
        endpointDesc.setClientBindingID(bindingId);
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#createDispatch(javax.xml.namespace.QName, java.lang.Class, javax.xml.ws.Service.Mode)
    */
    public <T> Dispatch<T> createDispatch(QName portName, Class<T> type, Mode mode)
            throws WebServiceException {
        return createDispatch(portName, type, mode, (WebServiceFeature[]) null);
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#createDispatch(javax.xml.namespace.QName, javax.xml.bind.JAXBContext, javax.xml.ws.Service.Mode)
    */
    public Dispatch<java.lang.Object> createDispatch(QName portName, JAXBContext context, Mode mode) {
        return createDispatch(portName, context, mode, (WebServiceFeature[]) null);
    }

    @Override
    public <T> Dispatch<T> createDispatch(EndpointReference jaxwsEPR, Class<T> type, Mode mode, WebServiceFeature... features) {
        if (log.isDebugEnabled()) {
            log.debug("Create Dispatch with epr: " + jaxwsEPR);
        }
        
        verifyServiceDescriptionActive();
        if (jaxwsEPR == null) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchNoEndpointReference"));
        }
        
        if (!isValidDispatchType(type)) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchInvalidType"));
        }
        
        if (!isValidDispatchTypeWithMode(type, mode)) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchInvalidTypeWithMode"));
        }
        
        org.apache.axis2.addressing.EndpointReference axis2EPR =
            EndpointReferenceUtils.createAxis2EndpointReference("");
        String addressingNamespace = null;
        
        try {
            addressingNamespace = EndpointReferenceUtils.convertToAxis2(axis2EPR, jaxwsEPR);
        }
        catch (Exception e) {
            throw ExceptionFactory.
               makeWebServiceException(Messages.getMessage("invalidEndpointReference", 
                                                           e.toString()));
        }
        
        EndpointDescription endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, null, axis2EPR,
                                                  addressingNamespace,
                                                  DescriptionFactory.UpdateType.CREATE_DISPATCH,
                                                  this);
        if (endpointDesc == null) {
            throw ExceptionFactory.makeWebServiceException(
                   Messages.getMessage("endpointDescriptionConstructionFailure", 
                    jaxwsEPR.toString())); 
        }

        XMLDispatch<T> dispatch = new XMLDispatch<T>(this, endpointDesc, axis2EPR, addressingNamespace, features);

        if (mode != null) {
            dispatch.setMode(mode);
        } else {
            dispatch.setMode(Service.Mode.PAYLOAD);
        }

        if (serviceClient == null)
            serviceClient = getServiceClient(endpointDesc.getPortQName());
        
        if(type == OMElement.class) {
            if (log.isDebugEnabled()) {
                log.debug("This a Dispatch<OMElement>. The custom builder is installed.");
            }
            ProviderOMContextListener.create(serviceClient.getServiceContext());
        }

        dispatch.setServiceClient(serviceClient);
        dispatch.setType(type);
        return dispatch;
    }

    @Override
    public Dispatch<Object> createDispatch(EndpointReference jaxwsEPR, JAXBContext context, Mode mode, WebServiceFeature... features) {
        if (log.isDebugEnabled()) {
            log.debug("Create Dispatch with epr 2: " + jaxwsEPR);
        }
        verifyServiceDescriptionActive();
        if (jaxwsEPR == null) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchNoEndpointReference"));
        }
        
        org.apache.axis2.addressing.EndpointReference axis2EPR =
            EndpointReferenceUtils.createAxis2EndpointReference("");
        String addressingNamespace = null;
        
        try {
            addressingNamespace = EndpointReferenceUtils.convertToAxis2(axis2EPR, jaxwsEPR);
        }
        catch (Exception e) {
            throw ExceptionFactory.
                makeWebServiceException(Messages.getMessage("invalidEndpointReference", 
                                                            e.toString()));
        }
        
        EndpointDescription endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, null, axis2EPR,
                                                  addressingNamespace,
                                                  DescriptionFactory.UpdateType.CREATE_DISPATCH,
                                                  this);
        if (endpointDesc == null) {
            throw ExceptionFactory.
              makeWebServiceException(Messages.getMessage("endpointDescriptionConstructionFailure", 
                                      jaxwsEPR.toString())); 
        }

        JAXBDispatch<Object> dispatch = new JAXBDispatch(this, endpointDesc, axis2EPR, addressingNamespace, features);

        if (mode != null) {
            dispatch.setMode(mode);
        } else {
            dispatch.setMode(Service.Mode.PAYLOAD);
        }

        if (serviceClient == null)
            serviceClient = getServiceClient(endpointDesc.getPortQName());

        dispatch.setJAXBContext(context);
        dispatch.setServiceClient(serviceClient);

        return dispatch;
    }

    @Override
    public <T> Dispatch<T> createDispatch(QName portName, Class<T> type, Mode mode, WebServiceFeature... features) {
        
        if (log.isDebugEnabled()) {
            log.debug("Create Dispatch with portName: " + portName);
        }
        verifyServiceDescriptionActive();
        if (portName == null) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("createDispatchFail0"));
        }
        
        if (!isValidDispatchType(type)) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchInvalidType"));
        }

        if (!isValidDispatchTypeWithMode(type, mode)) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("dispatchInvalidTypeWithMode"));
        }

        EndpointDescription endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, 
                								  null, 
                                                  portName,
                                                  DescriptionFactory.UpdateType.CREATE_DISPATCH,
                                                  this);

        if (endpointDesc == null) {
            throw ExceptionFactory.makeWebServiceException(
                    Messages.getMessage("createDispatchFail2", portName.toString()));
        }

        XMLDispatch<T> dispatch = new XMLDispatch<T>(this, endpointDesc, features);

        if (mode != null) {
            dispatch.setMode(mode);
        } else {
            dispatch.setMode(Service.Mode.PAYLOAD);
        }

        if (serviceClient == null)
            serviceClient = getServiceClient(portName);

        if(type == OMElement.class) {
            if (log.isDebugEnabled()) {
                log.debug("This a Dispatch<OMElement>. The custom builder is installed.");
            }
            ProviderOMContextListener.create(serviceClient.getServiceContext());
        }
        dispatch.setServiceClient(serviceClient);
        dispatch.setType(type);
        return dispatch;
    }

    @Override
    public Dispatch<Object> createDispatch(QName portName, JAXBContext context, Mode mode, WebServiceFeature... features) {
        if (log.isDebugEnabled()) {
            log.debug("Create Dispatch with jaxbcontext and portName: " + portName);
        }
        
        verifyServiceDescriptionActive();
        if (portName == null) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("createDispatchFail0"));
        }

        EndpointDescription endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, 
                								  null, 
                                                  portName,
                                                  DescriptionFactory.UpdateType.CREATE_DISPATCH,
                                                  this);

        if (endpointDesc == null) {
            throw ExceptionFactory.makeWebServiceException(
                    Messages.getMessage("createDispatchFail2", portName.toString()));
        }

        JAXBDispatch<Object> dispatch = new JAXBDispatch(this, endpointDesc, features);

        if (mode != null) {
            dispatch.setMode(mode);
        } else {
            dispatch.setMode(Service.Mode.PAYLOAD);
        }

        if (serviceClient == null)
            serviceClient = getServiceClient(portName);

        dispatch.setJAXBContext(context);
        dispatch.setServiceClient(serviceClient);

        return dispatch;
    }

    /*
     * (non-Javadoc)
     * @see javax.xml.ws.spi.ServiceDelegate#getPort(java.lang.Class)
     */
    public <T> T getPort(Class<T> sei) throws WebServiceException {
        return getPort((QName) null, sei, (WebServiceFeature[]) null);
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getPort(javax.xml.namespace.QName, java.lang.Class)
    */
    public <T> T getPort(QName portName, Class<T> sei) throws WebServiceException {
        return getPort(portName, sei, (WebServiceFeature[]) null);
    }

    @Override
    public <T> T getPort(Class<T> sei, WebServiceFeature... features) {
      return getPort((QName) null, sei, features);
    }

    @Override
    public <T> T getPort(EndpointReference jaxwsEPR, Class<T> sei, WebServiceFeature... features) {
        /* TODO Check to see if WSDL Location is provided.
         * if not check WebService annotation's WSDLLocation
         * if both are not provided then throw exception.
         * (JLB): I'm not sure lack of WSDL should cause an exception
         */
        
        if (log.isTraceEnabled()) {
            log.trace("getPort: jaxwsEPR = " + jaxwsEPR);
        }

        if (!isValidWSDLLocation()) {
            //TODO: Should I throw Exception if no WSDL
            //throw ExceptionFactory.makeWebServiceException("WSLD Not found");
        }
        
        if (jaxwsEPR == null) {
            throw ExceptionFactory.
               makeWebServiceException(Messages.getMessage("dispatchNoEndpointReference2"));
        }
        
        if (sei == null) {
            throw ExceptionFactory.makeWebServiceException(
                    Messages.getMessage("getPortInvalidSEI", jaxwsEPR.toString(), "null"));
        }
        
        org.apache.axis2.addressing.EndpointReference axis2EPR =
            EndpointReferenceUtils.createAxis2EndpointReference("");
        String addressingNamespace = null;
        
        try {
            addressingNamespace = EndpointReferenceUtils.convertToAxis2(axis2EPR, jaxwsEPR);
        }
        catch (Exception e) {
            throw ExceptionFactory.
              makeWebServiceException(Messages.getMessage("invalidEndpointReference", 
                                                          e.toString()));
        }
        
        if (log.isTraceEnabled()) {
            log.trace("getPort: Converted jaxwsEPR to axis2EPR = " + axis2EPR);
        }

        return getPort(axis2EPR, addressingNamespace, sei, features);
    }

    @Override
    public <T> T getPort(QName portName, Class<T> sei, WebServiceFeature... features) {
        verifyServiceDescriptionActive();
        /* TODO Check to see if WSDL Location is provided.
         * if not check WebService annotation's WSDLLocation
         * if both are not provided then throw exception.
         * (JLB): I'm not sure lack of WSDL should cause an exception
         */


        if (!isValidWSDLLocation()) {
            //TODO: Should I throw Exception if no WSDL
            //throw ExceptionFactory.makeWebServiceException("WSLD Not found");
        }
        if (sei == null) {
            throw ExceptionFactory.makeWebServiceException(
                    Messages.getMessage("getPortInvalidSEI", portName.toString(), "null"));
        }

        DescriptionBuilderComposite sparseComposite = getPortMetadata();
        resetPortMetadata();
        EndpointDescription endpointDesc = null;
        if (sparseComposite != null) {
            endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, sei, portName,
                                                  DescriptionFactory.UpdateType.GET_PORT,
                                                  sparseComposite, this);
        }
        else {
            endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, sei, portName,
                                                  DescriptionFactory.UpdateType.GET_PORT,
                                                  null, this);
            
        }
        if (endpointDesc == null) {
            throw ExceptionFactory.makeWebServiceException(
            		Messages.getMessage("portErr",portName.toString()));
        }

        String[] interfacesNames = 
            new String [] {sei.getName(), org.apache.axis2.jaxws.spi.BindingProvider.class.getName()};
        
        // As required by java.lang.reflect.Proxy, ensure that the interfaces
        // for the proxy are loadable by the same class loader. 
        Class[] interfaces = null;
        // First, let's try loading the interfaces with the SEI classLoader
        ClassLoader classLoader = getClassLoader(sei);
        try {
            interfaces = loadClasses(classLoader, interfacesNames);
        } catch (ClassNotFoundException e1) {
            // Let's try with context classLoader now
            classLoader = getContextClassLoader();
            try {
                interfaces = loadClasses(classLoader, interfacesNames);
            } catch (ClassNotFoundException e2) {
                throw ExceptionFactory.makeWebServiceException(Messages.getMessage("portErr1"), e2);
            }
        }

        JAXWSProxyHandler proxyHandler = new JAXWSProxyHandler(this, interfaces[0], endpointDesc, features);
        Object proxyClass = Proxy.newProxyInstance(classLoader, interfaces, proxyHandler);
        return sei.cast(proxyClass);
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getExecutor()
    */
    public Executor getExecutor() {
        //FIXME: Use client provider executor too.
        if (executor == null) {
            executor = getDefaultExecutor();
        }
        return executor;
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getHandlerResolver()
    */
    public HandlerResolver getHandlerResolver() {
        verifyServiceDescriptionActive();
        if (handlerResolver == null) {
            handlerResolver = new HandlerResolverImpl(serviceDescription, this);
        }
        return handlerResolver;
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getPorts()
    */
    public Iterator<QName> getPorts() {
        verifyServiceDescriptionActive();
        return getServiceDescription().getPorts(this).iterator();
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getServiceName()
    */
    public QName getServiceName() {
        return serviceQname;
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#getWSDLDocumentLocation()
    */
    public URL getWSDLDocumentLocation() {
        verifyServiceDescriptionActive();
        try {
            String wsdlLocation = ((ServiceDescriptionWSDL) serviceDescription).getWSDLLocation();
            if(wsdlLocation == null) {
                return null;
            }
            return new URL(wsdlLocation);
        } catch (MalformedURLException e) {
            throw ExceptionFactory
                    .makeWebServiceException(e);
        }
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#setExecutor(java.util.concurrent.Executor)
    */
    public void setExecutor(Executor e) {
        if (e == null) {
            throw ExceptionFactory
                    .makeWebServiceException(Messages.getMessage("cannotSetExcutorToNull"));
        }

        executor = e;
    }

    /*
    * (non-Javadoc)
    * @see javax.xml.ws.spi.ServiceDelegate#setHandlerResolver(javax.xml.ws.handler.HandlerResolver)
    */
    public void setHandlerResolver(HandlerResolver handlerresolver) {
        this.handlerResolver = handlerresolver;
    }

    //================================================
    // Internal public APIs
    //================================================

    /** Get the ServiceDescription tree that this ServiceDelegate */
    public ServiceDescription getServiceDescription() {
        return serviceDescription;
    }

    //TODO Change when ServiceDescription has to return ServiceClient or OperationClient

    /**
     * 
     */
    public ServiceClient getServiceClient(QName portQName) throws WebServiceException {
        verifyServiceDescriptionActive();
        return serviceDescription.getServiceClient(portQName, this);
    }
    
    public <T> T getPort(org.apache.axis2.addressing.EndpointReference axis2EPR, String addressingNamespace, Class<T> sei, WebServiceFeature... features) {
        verifyServiceDescriptionActive();
        DescriptionBuilderComposite sparseComposite = getPortMetadata();
        resetPortMetadata();
        EndpointDescription endpointDesc = null;
        
        if (sparseComposite != null) {
            endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, sei, axis2EPR,
                                                  addressingNamespace,
                                                  DescriptionFactory.UpdateType.GET_PORT,
                                                  sparseComposite, this);

        }
        else {
            endpointDesc =
                DescriptionFactory.updateEndpoint(serviceDescription, sei, axis2EPR,
                                                  addressingNamespace,
                                                  DescriptionFactory.UpdateType.GET_PORT,
                                                  null, this);
        }
        
        if (endpointDesc == null) {
            throw ExceptionFactory.makeWebServiceException(
                    Messages.getMessage("serviceDelegateNoPort", axis2EPR.toString()));
                                                           
        }

        String[] interfacesNames = 
            new String [] {sei.getName(), org.apache.axis2.jaxws.spi.BindingProvider.class.getName()};
        
        // As required by java.lang.reflect.Proxy, ensure that the interfaces
        // for the proxy are loadable by the same class loader. 
        Class[] interfaces = null;
        // First, let's try loading the interfaces with the SEI classLoader
        ClassLoader classLoader = getClassLoader(sei);
        try {
            interfaces = loadClasses(classLoader, interfacesNames);
        } catch (ClassNotFoundException e1) {
            // Let's try with context classLoader now
            classLoader = getContextClassLoader();
            try {
                interfaces = loadClasses(classLoader, interfacesNames);
            } catch (ClassNotFoundException e2) {
                throw ExceptionFactory.makeWebServiceException(
                     Messages.getMessage("serviceDelegateProxyError", e2.getMessage()));
            }
        }

        JAXWSProxyHandler proxyHandler = new JAXWSProxyHandler(this, interfaces[0], endpointDesc, axis2EPR, addressingNamespace, features);
        Object proxyClass = Proxy.newProxyInstance(classLoader, interfaces, proxyHandler);
        return sei.cast(proxyClass);        
    }

    //================================================
    // Impl methods
    //================================================

    //TODO: Need to make the default number of threads configurable
    private Executor getDefaultExecutor() {
    	ExecutorFactory executorFactory = (ExecutorFactory) FactoryRegistry.getFactory(
    			ExecutorFactory.class);
        return executorFactory.getExecutorInstance(ExecutorFactory.CLIENT_EXECUTOR);
    }

    private boolean isValidServiceName() {
        return serviceQname != null && !"".equals(serviceQname.toString().trim());
    }

    private boolean isValidWSDLLocation() {
        URL wsdlLocation = getWSDLDocumentLocation();
        return wsdlLocation != null && !"".equals(wsdlLocation.toString().trim());
    }

    // TODO: Remove this method and put the WSDLWrapper methods on the ServiceDescriptor directly
    private WSDLWrapper getWSDLWrapper() {
        verifyServiceDescriptionActive();
        return ((ServiceDescriptionWSDL)serviceDescription).getWSDLWrapper();
    }

    private boolean isServiceDefined(QName serviceName) {
        return getWSDLWrapper().getService(serviceName) != null;
    }

    private boolean isValidDispatchType(Class clazz) {
        return clazz != null && (clazz == String.class ||
                clazz == Source.class ||
                clazz == DataSource.class ||
                clazz == SOAPMessage.class ||
                clazz == OMElement.class);
    }

    private boolean isValidDispatchTypeWithMode(Class clazz, Mode mode) {

    	if (clazz != null && !(clazz == SOAPMessage.class && mode.equals(Service.Mode.PAYLOAD))) {
    			return true;
    	} else {
    		return false;
    	}
  
    }

    /** @return ClassLoader */
    private static ClassLoader getClassLoader(final Class cls) {
        // NOTE: This method must remain private because it uses AccessController
        ClassLoader cl = null;
        try {
            cl = (ClassLoader)AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return cls.getClassLoader();
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e);
            }
            throw ExceptionFactory.makeWebServiceException(e.getException());
        }

        return cl;
    }
    
    /** @return ClassLoader */
    private static ClassLoader getContextClassLoader() {
        // NOTE: This method must remain private because it uses AccessController
        ClassLoader cl = null;
        try {
            cl = (ClassLoader)AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return Thread.currentThread().getContextClassLoader();
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e);
            }
            throw ExceptionFactory.makeWebServiceException(e.getException());
        }

        return cl;
    }
    
    /**
     * Return the class for this name
     *
     * @return Class
     */
    private static Class forName(final String className, final boolean initialize,
                                 final ClassLoader classLoader) throws ClassNotFoundException {
        // NOTE: This method must remain protected because it uses AccessController
        Class cl = null;
        try {
            cl = (Class)AccessController.doPrivileged(
                    new PrivilegedExceptionAction() {
                        public Object run() throws ClassNotFoundException {
                            return Class.forName(className, initialize, classLoader);
                        }
                    }
            );
        } catch (PrivilegedActionException e) {
            if (log.isDebugEnabled()) {
                log.debug("Exception thrown from AccessController: " + e);
            }
            throw (ClassNotFoundException)e.getException();
        }

        return cl;
    }
    
    private static Class[] loadClasses(ClassLoader classLoader, String[] classNames)
        throws ClassNotFoundException {
        Class[] classes = new Class[classNames.length];
        for (int i = 0; i < classNames.length; i++) {
            classes[i] = forName(classNames[i], true, classLoader);
        }
        return classes;
    }
    
    /**
     * PROPRIETARY METHOD TO RELEASE RESOUCES.  USE OF THIS METHOD IS NOT JAX-WS COMPLIANT 
     * AND IS NON-PORTABLE!  
     * 
     * This method can be called by client code to try to release
     * resources associated with the Service instance parameter.  These resources include
     * the JAX-WS metadata objects (e.g. ServiceDescription, EndpointDescription) and the
     * associated Axis2 objects (e.g. AxisService and realted objects).  Note that these 
     * resources can be shared across multiple service delegates, and so they will not actually 
     * be released until the last service delegate using them is closed.
     * 
     * Note that it is not necessary to call this method since the service delegate finalizer
     * will also release the resources as appropriate.  However, finalizers are not necessarily run
     * in a timely fashion and the timing varies across JVMs.  To predictibly release resources
     * to prevent large memory requirements and/or OutOfMemory errors, this proprietary release 
     * method can be called.
     * 
     * @param service Instance of the Service for which resources may be released.
     */
    public static void releaseService(Service service) {
        // Find the ServiceDelegate corresponding to the service to be closed
        // This is the back way around since there is no close on the Service
        ServiceDelegate serviceDelegate = getServiceDelegateForService(service);
        serviceDelegate.releaseServiceResources();
    }
    
    private static ServiceDelegate getServiceDelegateForService(Service service) {
        // Need to get to the private Service._delegate
        ServiceDelegate returnServiceDelegate = null;
        try {
            try {
                Field serviceDelgateField = service.getClass().getDeclaredField("delegate");
                serviceDelgateField.setAccessible(true);
                returnServiceDelegate = (ServiceDelegate) serviceDelgateField.get(service);
            } catch (NoSuchFieldException e) {
                // This may be a generated service subclass, so get the delegate from the superclass
                Field serviceDelegateField = service.getClass().getSuperclass().getDeclaredField("delegate");
                serviceDelegateField.setAccessible(true);
                returnServiceDelegate = (ServiceDelegate) serviceDelegateField.get(service);
            } 
        } catch (SecurityException e) {
            if (log.isDebugEnabled()) {
                log.debug("Attempt to get service delegate for service caught exception.", e);
            }
            throw ExceptionFactory.makeWebServiceException(e);
        } catch (IllegalAccessException e) {
            if (log.isDebugEnabled()) {
                log.debug("Attempt to get service delegate for service caught exception.", e);
            }
            throw ExceptionFactory.makeWebServiceException(e);
        } catch (NoSuchFieldException e) {
            if (log.isDebugEnabled()) {
                log.debug("Attempt to get service delegate for service caught exception.", e);
            }
            throw ExceptionFactory.makeWebServiceException(e);
        }
        return returnServiceDelegate;

    }
    
    /**
     * This can be called from the proprietary static release method (which can be called via 
     * client code), or it can be called by the finalizer.  This method tries to release resources 
     * associated with the ServiceDelegate.  Note that since other ServiceDelegates can share these 
     * resources (e.g. ServiceDescription, EndpointDescription, AxisService), the resources may 
     * not be releaseed until the last ServiceDelegate using them issues a close.
     */
    private void releaseServiceResources() {
        if (log.isDebugEnabled()) {
            log.debug("ServiceDelegate.releaseServiceResouces entry");
        }
        // This can be called indirectly by client code or by the finalizer.  If it hasn't been
        // called yet, have the endpointDescriptions release resources.
        if (serviceDescription != null) {
            serviceDescription.releaseResources(this);
            serviceDescription = null;
        }
    }

    /**
     * Verify that there is an associated serviceDescription for this delegate.  If not, a
     * webServiceException will be thrown.  A serviceDelegate may have a null serviceDescription
     * if the client code issues the proprietary method call relealseServiceResources. 
     */
    private void verifyServiceDescriptionActive() {
        if (serviceDescription == null) {
            // TODO: This should be NLS'd
            throw ExceptionFactory.makeWebServiceException("Attempt to use Service after it was released");
        }
    }
    
    protected void finalize() throws Throwable {
        if (log.isDebugEnabled()) {
            log.debug("ServiceDelegate.finalize entered for " + this);
        }
        try {
            releaseServiceResources();
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("ServiceDelgate Finalizer caught exception", e);
            }
        } finally {
            super.finalize();
        }
        if (log.isDebugEnabled()) {
            log.debug("ServiceDelegate.finalize exited");
        }
    }   
}
