/*
 * 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.client.dispatch;

import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.jaxws.BindingProvider;
import org.apache.axis2.jaxws.ExceptionFactory;
import org.apache.axis2.jaxws.client.async.AsyncResponse;
import org.apache.axis2.jaxws.core.InvocationContext;
import org.apache.axis2.jaxws.core.InvocationContextFactory;
import org.apache.axis2.jaxws.core.MessageContext;
import org.apache.axis2.jaxws.core.controller.InvocationController;
import org.apache.axis2.jaxws.core.controller.InvocationControllerFactory;
import org.apache.axis2.jaxws.description.EndpointDescription;
import org.apache.axis2.jaxws.description.EndpointInterfaceDescription;
import org.apache.axis2.jaxws.description.OperationDescription;
import org.apache.axis2.jaxws.i18n.Messages;
import org.apache.axis2.jaxws.marshaller.impl.alt.MethodMarshallerUtils;
import org.apache.axis2.jaxws.message.Message;
import org.apache.axis2.jaxws.registry.FactoryRegistry;
import org.apache.axis2.jaxws.spi.Binding;
import org.apache.axis2.jaxws.spi.Constants;
import org.apache.axis2.jaxws.spi.ServiceDelegate;
import org.apache.axis2.jaxws.spi.migrator.ApplicationContextMigratorUtil;
import org.apache.axis2.kernel.http.HTTPConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Node;

import javax.xml.namespace.QName;
import jakarta.xml.soap.SOAPBody;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPMessage;
import javax.xml.transform.dom.DOMSource;
import jakarta.xml.ws.AsyncHandler;
import jakarta.xml.ws.ProtocolException;
import jakarta.xml.ws.Response;
import jakarta.xml.ws.Service.Mode;
import jakarta.xml.ws.WebServiceException;
import jakarta.xml.ws.WebServiceFeature;
import jakarta.xml.ws.http.HTTPBinding;
import jakarta.xml.ws.soap.SOAPBinding;

import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;

public abstract class BaseDispatch<T> extends BindingProvider
        implements jakarta.xml.ws.Dispatch {

    private static Log log = LogFactory.getLog(BaseDispatch.class);

    protected InvocationController ic;

    protected ServiceClient serviceClient;

    protected Mode mode;

    protected BaseDispatch(ServiceDelegate svcDelgate,
                           EndpointDescription epDesc,
                           EndpointReference epr,
                           String addressingNamespace,
                           WebServiceFeature... features) {
        super(svcDelgate, epDesc, epr, addressingNamespace, features);

        InvocationControllerFactory icf = (InvocationControllerFactory) FactoryRegistry.getFactory(InvocationControllerFactory.class);
        ic = icf.getInvocationController();
        
        if (ic == null) {
            throw new WebServiceException(Messages.getMessage("missingInvocationController"));
        }
    }

    /**
     * Take the input object and turn it into an OMElement so that it can be sent.
     *
     * @param value
     * @return
     */
    protected abstract Message createMessageFromValue(Object value);

    /**
     * Given a message, return the business object based on the requestor's required format (PAYLOAD
     * vs. MESSAGE) and datatype.
     *
     * @param message
     * @return
     */
    protected abstract Object getValueFromMessage(Message message);

    /**
     * Creates an instance of the AsyncListener that is to be used for waiting for async responses.
     *
     * @return a configured AsyncListener instance
     */
    protected abstract AsyncResponse createAsyncResponseListener();

    
    /**
     * Note to developer: When making a change or fix to this method, please consider
     * all 5 Proxy/Dispatch "invoke" methods now available in JAX-WS. For Dispatch, 
     * these are:
     * 1) Synchronous invoke()
     * 2) invokeOneWay()
     * 3) invokeAsynch (Future)
     * 4) invokeAsynch (Callback)
     * 
     * For Proxy:
     * 5) invokeSEIMethod() 
     *
     */
    public Object invoke(Object obj) throws WebServiceException {

        // Catch all exceptions and rethrow an appropriate WebService Exception
        try {
            if (log.isDebugEnabled()) {
                log.debug("Entered synchronous invocation: BaseDispatch.invoke()");
            }

            // Create the InvocationContext instance for this request/response flow.
            InvocationContext invocationContext =
                    InvocationContextFactory.createInvocationContext(null);
            invocationContext.setServiceClient(serviceClient);

            // Create the MessageContext to hold the actual request message and its
            // associated properties
            MessageContext requestMsgCtx = new MessageContext();
            requestMsgCtx.getAxisMessageContext().setProperty(BINDING_PROVIDER, this);
            requestMsgCtx.setEndpointDescription(getEndpointDescription());
            invocationContext.setRequestMessageContext(requestMsgCtx);
            
            /*
             * TODO: review: make sure the handlers are set on the InvocationContext
             * This implementation of the JAXWS runtime does not use Endpoint, which
             * would normally be the place to initialize and store the handler list.
             * In lieu of that, we will have to intialize and store them on the 
             * InvocationContext.  also see the InvocationContextFactory.  On the client
             * side, the binding is not yet set when we call into that factory, so the
             * handler list doesn't get set on the InvocationContext object there.  Thus
             * we gotta do it here.
             */

            // be sure to use whatever handlerresolver is registered on the Service
            Binding binding = (Binding) getBinding();
            invocationContext.setHandlers(binding.getHandlerChain());

            initMessageContext(obj, requestMsgCtx);

            // call common init method for all invoke* paths
            preInvokeInit(invocationContext);
                        
            // Migrate the properties from the client request context bag to
            // the request MessageContext.
            ApplicationContextMigratorUtil.performMigrationToMessageContext(
                    Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID,
                    getRequestContext(), requestMsgCtx);

            // Perform the WebServiceFeature configuration requested by the user.
            binding.configure(requestMsgCtx, this);

            // Initializing the message context above will put the outbound message onto the messageContext
            // Determine the operation if possible from the outbound message.  If it can not be determined
            // it will be set to null.  In this case, an anonymous operation will be used.  Note that determining
            // the operation will mean deserializing the message.  That means that any WebServiceFeatures must have
            // been configured first so that any relevant configurations (such as MTOM) have been initialized prior to 
            // the message being deserialized.  This is particularly true for Dispatch<JAXB Element>.
            requestMsgCtx.setOperationDescription(getOperationDescriptionForDispatch(requestMsgCtx));

            // Send the request using the InvocationController
            ic.invoke(invocationContext);

            MessageContext responseMsgCtx = invocationContext.getResponseMessageContext();
            responseMsgCtx.setEndpointDescription(requestMsgCtx.getEndpointDescription());

            // Migrate the properties from the response MessageContext back
            // to the client response context bag.
            ApplicationContextMigratorUtil.performMigrationFromMessageContext(
                    Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID,
                    getResponseContext(), responseMsgCtx);
            
            if (hasFaultResponse(responseMsgCtx)) {
                WebServiceException wse = BaseDispatch.getFaultResponse(responseMsgCtx);
                throw wse;
            }

            // Get the return object
            Object returnObj = null;
            try {
                Message responseMsg = responseMsgCtx.getMessage();
                returnObj = getValueFromMessage(responseMsg);
            }
            finally {
                // Free the incoming input stream
                try {
                    responseMsgCtx.freeInputStream();
                }
                catch (Throwable t) {
                    throw ExceptionFactory.makeWebServiceException(t);
                }
            }
           
            //Check to see if we need to maintain session state
            checkMaintainSessionState(requestMsgCtx, invocationContext);

            if (log.isDebugEnabled()) {
                log.debug("Synchronous invocation completed: BaseDispatch.invoke()");
            }

            return returnObj;
        } catch (WebServiceException e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invoke(): Synchronous invocation failed, " 
                        + "caught a WebServiceException: ", e);
            }
            throw e;
        } catch (Exception e) {
            // All exceptions are caught and rethrown as a WebServiceException
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invoke(): Synchronous invocation failed, caught an Exception, " + 
                        "wrapping into a WebServiceException. Exception caught: ", e);
            }  
            throw ExceptionFactory.makeWebServiceException(e);
        }
    }

    /**
     * Given a JAXWS Message Context which contains an outbound service-requester Message for a Dispatch client, 
     * determine the OperationDescription for the operation contained in that Dispatch message.
     * 
     * Note that operation resolution can be disabled by a property setting.
     * @see org.apache.axis2.jaxws.Constants.DISPATCH_CLIENT_OUTBOUND_RESOLUTION
     * 
     * @param requestMessageCtx JAXWS Message Context containing the outbound Dispatch message
     * @return the OperationDescription corresponding to the operation contained in the Dispatch message, or null 
     * if it can not be determined or if dispatch operation resolution is disabled via a property.
     */
    private OperationDescription getOperationDescriptionForDispatch(MessageContext requestMessageCtx) {
        OperationDescription operationDesc = null;
        if (dispatchOperationResolutionEnabled()) {
            EndpointInterfaceDescription endpointInterfaceDesc = getEndpointDescription().getEndpointInterfaceDescription();
            // The SEI interface could be null (for example if there was no SEI and all the ports were dynamically added).
            // If there is an SEI, then try to determine the operation for the outbound dispatch message.
            if (endpointInterfaceDesc != null) {
                QName bodyElementQName = getBodyElementQNameFromDispatchMessage(requestMessageCtx);
                operationDesc = determineOperationDescFromBodyElementQName(endpointInterfaceDesc, bodyElementQName);
            }
        }
        return operationDesc;
    }
    
    /**
     * Returns the OperationDescription corresponding to the bodyElementQName passed in.  What that body element corresponds to
     * depends on the type of the message:
     * - For Doc/Lit/Wrapped, the body element is the operation name
     * - For Doc/Lit/Bare, the body element is the element name contained in the wsdl:message wsdl:part
     * - For RPC, the body element is effectively the operation name.
     * 
     * @param endpointInterfaceDesc The interface (i.e. SEI) on which to search for the operation
     * @param bodyElementQName the QName of the first body element for which to find the operation
     * 
     * @return The OperationDescription corresponding to the body element QName or null if one can not be found.
     */
    private OperationDescription determineOperationDescFromBodyElementQName(EndpointInterfaceDescription endpointInterfaceDesc,
                                                                            QName bodyElementQName) {
        OperationDescription operationDesc = null;
        
        // If there's no bodyElementQName for us to work with, there's nothing more we can do.
        if (bodyElementQName != null) {
            // This logic mimics the code in SOAPMessageBodyBasedOperationDispatcher.findOperation.  We will look for
            // the AxisOperation corresponding to the body element name.  Note that we are searching for the AxisOperation instead
            // of searching through the OperationDescriptions so that we can use the getOperationByMessageElementQName
            // for the Doc/Lit/Bare case.  Once we have the AxisOperation, we'll use that to find the Operation Description.
            AxisService axisService = endpointInterfaceDesc.getEndpointDescription().getAxisService();
            AxisOperation axisOperation = null;
    
            // Doc/Lit/Wrapped and RPC, the operation name is the first body element qname
            axisOperation = axisService.getOperation(new QName(bodyElementQName.getLocalPart()));
            
            if (axisOperation == null) {
                // Doc/Lit/Bare, the first body element qname is the element name contained in the wsdl:message part
                axisOperation = axisService.getOperationByMessageElementQName(bodyElementQName);
            }
            
            if (axisOperation == null) {
                // Not sure why we wouldn't have found the operation above using just the localPart rather than the full QName used here,
                // but this is what SOAPMessageBodyBasedOperationDispatcher.findOperation does.
                axisOperation = axisService.getOperation(bodyElementQName);
            }
    
            // If we found an axis operation, then find the operation description that corresponds to it
            if (axisOperation != null) {
                OperationDescription allOpDescs[] = endpointInterfaceDesc.getDispatchableOperations();
                for (OperationDescription checkOpDesc : allOpDescs ) {
                    AxisOperation checkAxisOperation = checkOpDesc.getAxisOperation();
                    if (checkAxisOperation == axisOperation) {
                        operationDesc = checkOpDesc;
                        break;
                    }
                }
            }
        }
        return operationDesc;
    }

    /**
     * Answer if operation resolution on outbound messages for dispatch clients should be done.  The default value 
     * is TRUE, enabling operation resolution.  Resolution can be disabled via a property on the AxisConfiguration
     * or on the RequestContext.  
     * 
     * Operation resolution is also disabled if a non-null value is specified on the request context for the Action
     * 
     * @see org.apache.axis2.jaxws.Constants.DISPATCH_CLIENT_OUTBOUND_RESOLUTION
     * @see jakarta.xml.ws.BindingProvider.SOAPACTION_USE_PROPERTY
     * @see jakarta.xml.ws.BindingProvider.SOAPACTION_URI_PROPERTY
     * 
     * @return true if operation resolution should be performed on outbound 
     */
    private boolean dispatchOperationResolutionEnabled() {
        boolean resolutionEnabled = true;
        
        // See if any properties disabled operation resolution 
        // Check for System property setting
        String flagValue = getProperty(org.apache.axis2.jaxws.Constants.DISPATCH_CLIENT_OUTBOUND_RESOLUTION);

        // If no System property was set, see if one was set on this request context.
        if (flagValue == null) {
            flagValue =  (String) getRequestContext().get(org.apache.axis2.jaxws.Constants.DISPATCH_CLIENT_OUTBOUND_RESOLUTION);
        }
        
        // If any property was set, check the value.
        if (flagValue != null) {
            if ("false".equalsIgnoreCase(flagValue)) {
                resolutionEnabled = false;
            } else if ("true".equalsIgnoreCase(flagValue)) {
                resolutionEnabled = true;
            }
        }

        // If a property didn't disable resolution, then see if a URI value was specified.
        // If so, we'll use that later and there's no need to do operation resolution.         
        if (resolutionEnabled) {
            Boolean useSoapAction = (Boolean) getRequestContext().get(SOAPACTION_USE_PROPERTY);
            if (useSoapAction != null && useSoapAction.booleanValue()) {
                String soapAction = (String) getRequestContext().get(SOAPACTION_URI_PROPERTY);
                if (soapAction != null) {
                    resolutionEnabled = false;
                }
            }
        }
        return resolutionEnabled;
    }

    /**
     * Retrieve the specified property from the AxisConfiguration.
     * 
     * @param key The property to retrieve from the AxisConfiguration
     * @return the value associated with the property or null if the property did not exist on the configuration.
     */
    private String getProperty(String key) {
        String propertyValue = null;
        AxisConfiguration axisConfig = serviceDelegate.getServiceDescription().getAxisConfigContext().getAxisConfiguration();
        Parameter parameter = axisConfig.getParameter(key);
        if (parameter != null) {
            propertyValue = (String) parameter.getValue();
        }
        return propertyValue;
    }


    /**
     * Given a JAXWS Message Context which contains an outbound service-requester Message for a Dispatch client,
     * determine the QName of the first body element contained in that message.
     * 
     * @param requestMessageCtx requestMessageCtx JAXWS Message Context containing the outbound Dispatch message
     * @return the QName of the first body element contained in the outbound Dispatch message, or null if it 
     * can not be determined.
     */
    QName getBodyElementQNameFromDispatchMessage(MessageContext requestMessageCtx) {
        QName bodyElementQName = null;
        Message dispatchMessage = requestMessageCtx.getMessage();
        SOAPMessage soapMessage = dispatchMessage.getAsSOAPMessage();
        try {
            SOAPBody soapBody = soapMessage.getSOAPBody();
            Node firstElement = soapBody.getFirstChild();
            // A Doc/Lit/Bare message may not have a firsElement.  The soap:Body element may be empty if there 
            // are no arguments to the operation.
            if (firstElement != null) {
                String ns = firstElement.getNamespaceURI();
                String lp= firstElement.getLocalName();
                // A Doc/Lit/Bare message may not have a localPart on the element.  That can happen if the first element
                // is the argument value and there is no wrapper element surrounding it.
                if (lp != null) {
                    bodyElementQName = new QName(ns, lp);
                }
            }
        } catch (SOAPException e) {
            if (log.isDebugEnabled()) {
                log.debug("Unabled to get the first body element from the outbound dispatch message", e);
            }
        }
        return bodyElementQName;
    }

    protected void initMessageContext(Object obj, MessageContext requestMsgCtx) {
        Message requestMsg = createRequestMessage(obj);
        setupMessageProperties(requestMsg);
        requestMsgCtx.setMessage(requestMsg);
        // handle HTTP_REQUEST_METHOD property
        String method = (String)requestContext.get(jakarta.xml.ws.handler.MessageContext.HTTP_REQUEST_METHOD);
        if (method != null) {
            requestMsgCtx.setProperty(org.apache.axis2.Constants.Configuration.HTTP_METHOD, method);
        }
    }

    /**
     * Note to developer: When making a change or fix to this method, please consider
     * all 5 Proxy/Dispatch "invoke" methods now available in JAX-WS. For Dispatch, 
     * these are:
     * 1) Synchronous invoke()
     * 2) invokeOneWay()
     * 3) invokeAsynch (Future)
     * 4) invokeAsynch (Callback)
     * 
     * For Proxy:
     * 5) invokeSEIMethod() 
     *
     */
    public void invokeOneWay(Object obj) throws WebServiceException {

        // All exceptions are caught and rethrown as a WebServiceException
        MessageContext requestMsgCtx = null;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Entered one-way invocation: BaseDispatch.invokeOneWay()");
            }

            // Create the InvocationContext instance for this request/response flow.
            InvocationContext invocationContext =
                    InvocationContextFactory.createInvocationContext(null);
            invocationContext.setServiceClient(serviceClient);

            // Create the MessageContext to hold the actual request message and its
            // associated properties
            requestMsgCtx = new MessageContext();
            requestMsgCtx.getAxisMessageContext().setProperty(BINDING_PROVIDER, this);
            requestMsgCtx.setEndpointDescription(getEndpointDescription());
            invocationContext.setRequestMessageContext(requestMsgCtx);
            
            /*
             * TODO: review: make sure the handlers are set on the InvocationContext
             * This implementation of the JAXWS runtime does not use Endpoint, which
             * would normally be the place to initialize and store the handler list.
             * In lieu of that, we will have to intialize and store them on the 
             * InvocationContext.  also see the InvocationContextFactory.  On the client
             * side, the binding is not yet set when we call into that factory, so the
             * handler list doesn't get set on the InvocationContext object there.  Thus
             * we gotta do it here.
             */

            // be sure to use whatever handlerresolver is registered on the Service
            Binding binding = (Binding) getBinding();
            invocationContext.setHandlers(binding.getHandlerChain());

            initMessageContext(obj, requestMsgCtx);

            /*
             * if SESSION_MAINTAIN_PROPERTY is true, and the client app has explicitly set a HEADER_COOKIE on the request context, assume the client
             * app is expecting the HEADER_COOKIE to be the session id.  If we were establishing a new session, no cookie would be sent, and the 
             * server would reply with a "Set-Cookie" header, which is copied as a "Cookie"-keyed property to the service context during response.
             * In this case, if we succeed in using an existing server session, no "Set-Cookie" header will be returned, and therefore no
             * "Cookie"-keyed property would be set on the service context.  So, let's copy our request context HEADER_COOKIE key to the service
             * context now to prevent the "no cookie" exception in BindingProvider.setupSessionContext.  It is possible the server does not support
             * sessions, in which case no error occurs, but the client app would assume it is participating in a session.
             */
            if ((requestContext.containsKey(BindingProvider.SESSION_MAINTAIN_PROPERTY)) && ((Boolean)requestContext.get(BindingProvider.SESSION_MAINTAIN_PROPERTY))) {
                if ((requestContext.containsKey(HTTPConstants.HEADER_COOKIE)) && (requestContext.get(HTTPConstants.HEADER_COOKIE) != null)) {
                    if (invocationContext.getServiceClient().getServiceContext().getProperty(HTTPConstants.HEADER_COOKIE) == null) {
                        invocationContext.getServiceClient().getServiceContext().setProperty(HTTPConstants.HEADER_COOKIE, requestContext.get(HTTPConstants.HEADER_COOKIE));
                        if (log.isDebugEnabled()) {
                            log.debug("Client-app defined Cookie property (assume to be session cookie) on request context copied to service context." +
                                "  Caution:  server may or may not support sessions, but client app will not be informed when not supported.");
                        }
                    }
                }
            }

            // call common init method for all invoke* paths
            preInvokeInit(invocationContext);
            
            // Migrate the properties from the client request context bag to
            // the request MessageContext.
            ApplicationContextMigratorUtil.performMigrationToMessageContext(
                    Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID,
                    getRequestContext(), requestMsgCtx);

            // Perform the WebServiceFeature configuration requested by the user.
            binding.configure(requestMsgCtx, this);

            // Initializing the message context above will put the outbound message onto the messageContext
            // Determine the operation if possible from the outbound message.  If it can not be determined
            // it will be set to null.  In this case, an anonymous operation will be used.  Note that determining
            // the operation will mean deserializing the message.  That means that any WebServiceFeatures must have
            // been configured first so that any relevant configurations (such as MTOM) have been initialized prior to 
            // the message being deserialized.  This is particularly true for Dispatch<JAXB Element>.
            requestMsgCtx.setOperationDescription(getOperationDescriptionForDispatch(requestMsgCtx));

            // Send the request using the InvocationController
            ic.invokeOneWay(invocationContext);

            //Check to see if we need to maintain session state
            checkMaintainSessionState(requestMsgCtx, invocationContext);

            if (log.isDebugEnabled()) {
                log.debug("One-way invocation completed: BaseDispatch.invokeOneWay()");
            }

            return;
        } catch (WebServiceException e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeOneWay(): One-way invocation failed, " + 
                        "caught a WebServiceException: ", e);
            }
            throw e;
        } catch (Exception e) {
            // All exceptions are caught and rethrown as a WebServiceException
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeOneWay(): One-way invocation failed, " + 
                        "caught an Exception, wrapping into a WebServicesException. " + 
                        " Exception caught: ", e);
            }
            throw ExceptionFactory.makeWebServiceException(e);
        } finally {
            // In all other cases we rely on freeInputStream to perform the clean up. Since we don't expect
            // a response in the invokeOneWay case, we need to perform call TransportSender#cleanup explicitly
            try {
                if (requestMsgCtx != null && requestMsgCtx.getAxisMessageContext() != null) {
                    org.apache.axis2.context.MessageContext axisMsgCtx = requestMsgCtx.getAxisMessageContext();
                    if (axisMsgCtx.getTransportOut() != null && axisMsgCtx.getTransportOut().getSender() != null) {
                        axisMsgCtx.getTransportOut().getSender().cleanup(axisMsgCtx);
                    }
                }
            } catch (Exception ignore) {
            }
        }
    }

    /**
     * Note to developer: When making a change or fix to this method, please consider
     * all 5 Proxy/Dispatch "invoke" methods now available in JAX-WS. For Dispatch, 
     * these are:
     * 1) Synchronous invoke()
     * 2) invokeOneWay()
     * 3) invokeAsynch (Future)
     * 4) invokeAsynch (Callback)
     * 
     * For Proxy:
     * 5) invokeSEIMethod() 
     *
     */
    public Future<?> invokeAsync(Object obj, AsyncHandler asynchandler) throws WebServiceException {

        // All exceptions are caught and rethrown as a WebServiceException
        try {
            if (log.isDebugEnabled()) {
                log.debug("Entered asynchronous (callback) invocation: BaseDispatch.invokeAsync()");
            }

            // Create the InvocationContext instance for this request/response flow.
            InvocationContext invocationContext =
                    InvocationContextFactory.createInvocationContext(null);
            invocationContext.setServiceClient(serviceClient);

            // Create the MessageContext to hold the actual request message and its
            // associated properties
            MessageContext requestMsgCtx = new MessageContext();
            requestMsgCtx.getAxisMessageContext().setProperty(BINDING_PROVIDER, this);
            requestMsgCtx.setEndpointDescription(getEndpointDescription());
            invocationContext.setRequestMessageContext(requestMsgCtx);
            
            /*
             * TODO: review: make sure the handlers are set on the InvocationContext
             * This implementation of the JAXWS runtime does not use Endpoint, which
             * would normally be the place to initialize and store the handler list.
             * In lieu of that, we will have to intialize and store them on the 
             * InvocationContext.  also see the InvocationContextFactory.  On the client
             * side, the binding is not yet set when we call into that factory, so the
             * handler list doesn't get set on the InvocationContext object there.  Thus
             * we gotta do it here.
             */

            // be sure to use whatever handlerresolver is registered on the Service
            Binding binding = (Binding) getBinding();
            invocationContext.setHandlers(binding.getHandlerChain());

            initMessageContext(obj, requestMsgCtx);
            /*
             * if SESSION_MAINTAIN_PROPERTY is true, and the client app has explicitly set a HEADER_COOKIE on the request context, assume the client
             * app is expecting the HEADER_COOKIE to be the session id.  If we were establishing a new session, no cookie would be sent, and the 
             * server would reply with a "Set-Cookie" header, which is copied as a "Cookie"-keyed property to the service context during response.
             * In this case, if we succeed in using an existing server session, no "Set-Cookie" header will be returned, and therefore no
             * "Cookie"-keyed property would be set on the service context.  So, let's copy our request context HEADER_COOKIE key to the service
             * context now to prevent the "no cookie" exception in BindingProvider.setupSessionContext.  It is possible the server does not support
             * sessions, in which case no error occurs, but the client app would assume it is participating in a session.
             */
            if ((requestContext.containsKey(BindingProvider.SESSION_MAINTAIN_PROPERTY)) && ((Boolean)requestContext.get(BindingProvider.SESSION_MAINTAIN_PROPERTY))) {
                if ((requestContext.containsKey(HTTPConstants.HEADER_COOKIE)) && (requestContext.get(HTTPConstants.HEADER_COOKIE) != null)) {
                    if (invocationContext.getServiceClient().getServiceContext().getProperty(HTTPConstants.HEADER_COOKIE) == null) {
                        invocationContext.getServiceClient().getServiceContext().setProperty(HTTPConstants.HEADER_COOKIE, requestContext.get(HTTPConstants.HEADER_COOKIE));
                        if (log.isDebugEnabled()) {
                            log.debug("Client-app defined Cookie property (assume to be session cookie) on request context copied to service context." +
                                "  Caution:  server may or may not support sessions, but client app will not be informed when not supported.");
                        }
                    }
                }
            }

            // call common init method for all invoke* paths
            preInvokeInit(invocationContext);
            
            // Migrate the properties from the client request context bag to
            // the request MessageContext.
            ApplicationContextMigratorUtil.performMigrationToMessageContext(
                    Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID,
                    getRequestContext(), requestMsgCtx);

            // Perform the WebServiceFeature configuration requested by the user.
            binding.configure(requestMsgCtx, this);

            // Initializing the message context above will put the outbound message onto the messageContext
            // Determine the operation if possible from the outbound message.  If it can not be determined
            // it will be set to null.  In this case, an anonymous operation will be used.  Note that determining
            // the operation will mean deserializing the message.  That means that any WebServiceFeatures must have
            // been configured first so that any relevant configurations (such as MTOM) have been initialized prior to 
            // the message being deserialized.  This is particularly true for Dispatch<JAXB Element>.
            requestMsgCtx.setOperationDescription(getOperationDescriptionForDispatch(requestMsgCtx));

            // Setup the Executor that will be used to drive async responses back to 
            // the client.
            // FIXME: We shouldn't be getting this from the ServiceDelegate, rather each 
            // Dispatch object should have it's own.
            Executor e = serviceDelegate.getExecutor();
            invocationContext.setExecutor(e);

            // Create the AsyncListener that is to be used by the InvocationController.
            AsyncResponse listener = createAsyncResponseListener();
            invocationContext.setAsyncResponseListener(listener);

            // Send the request using the InvocationController
            Future<?> asyncResponse = ic.invokeAsync(invocationContext, asynchandler);

            //Check to see if we need to maintain session state
            checkMaintainSessionState(requestMsgCtx, invocationContext);

            if (log.isDebugEnabled()) {
                log.debug("Asynchronous (callback) invocation sent: BaseDispatch.invokeAsync()");
            }

            return asyncResponse;
        } catch (WebServiceException e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeAsync() [Callback]: Asynchronous invocation failed, " +
                    "caught a WebServiceException: ", e);
            }
            throw e;
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeAsync() [Callback]: Asynchronous invocation failed, " +
                    "caught an Exception, wrapping into a WebServiceException. Exception caught: ", e);
            }
            // All exceptions are caught and rethrown as a WebServiceException
            throw ExceptionFactory.makeWebServiceException(e);
        }
    }

    /**
     * Note to developer: When making a change or fix to this method, please consider
     * all 5 Proxy/Dispatch "invoke" methods now available in JAX-WS. For Dispatch, 
     * these are:
     * 1) Synchronous invoke()
     * 2) invokeOneWay()
     * 3) invokeAsynch (Future)
     * 4) invokeAsynch (Callback)
     * 
     * For Proxy:
     * 5) invokeSEIMethod() 
     *
     */
    public Response invokeAsync(Object obj) throws WebServiceException {

        // All exceptions are caught and rethrown as a WebServiceException
        try {
            if (log.isDebugEnabled()) {
                log.debug("Entered asynchronous (polling) invocation: BaseDispatch.invokeAsync()");
            }

            // Create the InvocationContext instance for this request/response flow.
            InvocationContext invocationContext =
                    InvocationContextFactory.createInvocationContext(null);
            invocationContext.setServiceClient(serviceClient);

            // Create the MessageContext to hold the actual request message and its
            // associated properties
            MessageContext requestMsgCtx = new MessageContext();
            requestMsgCtx.getAxisMessageContext().setProperty(BINDING_PROVIDER, this);
            requestMsgCtx.setEndpointDescription(getEndpointDescription());
            invocationContext.setRequestMessageContext(requestMsgCtx);
            
            /*
             * TODO: review: make sure the handlers are set on the InvocationContext
             * This implementation of the JAXWS runtime does not use Endpoint, which
             * would normally be the place to initialize and store the handler list.
             * In lieu of that, we will have to intialize and store them on the 
             * InvocationContext.  also see the InvocationContextFactory.  On the client
             * side, the binding is not yet set when we call into that factory, so the
             * handler list doesn't get set on the InvocationContext object there.  Thus
             * we gotta do it here.
             */

            // be sure to use whatever handlerresolver is registered on the Service
            Binding binding = (Binding) getBinding();
            invocationContext.setHandlers(binding.getHandlerChain());

            initMessageContext(obj, requestMsgCtx);

            /*
             * if SESSION_MAINTAIN_PROPERTY is true, and the client app has explicitly set a HEADER_COOKIE on the request context, assume the client
             * app is expecting the HEADER_COOKIE to be the session id.  If we were establishing a new session, no cookie would be sent, and the 
             * server would reply with a "Set-Cookie" header, which is copied as a "Cookie"-keyed property to the service context during response.
             * In this case, if we succeed in using an existing server session, no "Set-Cookie" header will be returned, and therefore no
             * "Cookie"-keyed property would be set on the service context.  So, let's copy our request context HEADER_COOKIE key to the service
             * context now to prevent the "no cookie" exception in BindingProvider.setupSessionContext.  It is possible the server does not support
             * sessions, in which case no error occurs, but the client app would assume it is participating in a session.
             */
            if ((requestContext.containsKey(BindingProvider.SESSION_MAINTAIN_PROPERTY)) && ((Boolean)requestContext.get(BindingProvider.SESSION_MAINTAIN_PROPERTY))) {
                if ((requestContext.containsKey(HTTPConstants.HEADER_COOKIE)) && (requestContext.get(HTTPConstants.HEADER_COOKIE) != null)) {
                    if (invocationContext.getServiceClient().getServiceContext().getProperty(HTTPConstants.HEADER_COOKIE) == null) {
                        invocationContext.getServiceClient().getServiceContext().setProperty(HTTPConstants.HEADER_COOKIE, requestContext.get(HTTPConstants.HEADER_COOKIE));
                        if (log.isDebugEnabled()) {
                            log.debug("Client-app defined Cookie property (assume to be session cookie) on request context copied to service context." +
                                "  Caution:  server may or may not support sessions, but client app will not be informed when not supported.");
                        }
                    }
                }
            }

            // call common init method for all invoke* paths
            preInvokeInit(invocationContext);
            
            // Migrate the properties from the client request context bag to
            // the request MessageContext.
            ApplicationContextMigratorUtil.performMigrationToMessageContext(
                    Constants.APPLICATION_CONTEXT_MIGRATOR_LIST_ID,
                    getRequestContext(), requestMsgCtx);

            // Perform the WebServiceFeature configuration requested by the user.
            binding.configure(requestMsgCtx, this);

            // Initializing the message context above will put the outbound message onto the messageContext
            // Determine the operation if possible from the outbound message.  If it can not be determined
            // it will be set to null.  In this case, an anonymous operation will be used.  Note that determining
            // the operation will mean deserializing the message.  That means that any WebServiceFeatures must have
            // been configured first so that any relevant configurations (such as MTOM) have been initialized prior to 
            // the message being deserialized.  This is particularly true for Dispatch<JAXB Element>.
            requestMsgCtx.setOperationDescription(getOperationDescriptionForDispatch(requestMsgCtx));
            

            // Setup the Executor that will be used to drive async responses back to 
            // the client.
            // FIXME: We shouldn't be getting this from the ServiceDelegate, rather each 
            // Dispatch object should have it's own.
            Executor e = serviceDelegate.getExecutor();
            invocationContext.setExecutor(e);

            // Create the AsyncListener that is to be used by the InvocationController.
            AsyncResponse listener = createAsyncResponseListener();
            invocationContext.setAsyncResponseListener(listener);

            // Send the request using the InvocationController
            Response asyncResponse = ic.invokeAsync(invocationContext);

            //Check to see if we need to maintain session state
            checkMaintainSessionState(requestMsgCtx, invocationContext);

            if (log.isDebugEnabled()) {
                log.debug("Asynchronous (polling) invocation sent: BaseDispatch.invokeAsync()");
            }

            return asyncResponse;
        } catch (WebServiceException e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeAsync() [Polling]: Asynchronous invocation failed, " +
                    "caught a WebServiceException: ", e);
            }
            throw e;
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("BaseDispatch.invokeAsync() [Polling]: Asynchronous invocation failed, " +
                    "caught an Exception, wrapping into a WebServiceException. Exception caught: ",e);
            }
            // All exceptions are caught and rethrown as a WebServiceException
            throw ExceptionFactory.makeWebServiceException(e);
        }
    }

    public void setServiceClient(ServiceClient sc) {
        serviceClient = sc;
    }

    public Mode getMode() {
        return mode;
    }

    public void setMode(Mode m) {
        mode = m;
    }

    /**
     * Returns the fault that is contained within the MessageContext for an invocation. If no fault
     * exists, null will be returned.
     *
     * @param msgCtx
     * @return
     */
    public static WebServiceException getFaultResponse(MessageContext msgCtx) {
        try {
            Message msg = msgCtx.getMessage();
            if (msg != null && msg.isFault()) {
                //XMLFault fault = msg.getXMLFault();
                // 4.3.2 conformance bullet 1 requires a ProtocolException here
                ProtocolException pe =
                    MethodMarshallerUtils.createSystemException(msg.getXMLFault(), msg);
                if (msgCtx.getLocalException() != null) {
                    // If a local exception occured, set it as the initial cause of the 
                    // exception that will be returned
                    ExceptionFactory.setInitialCause(pe, msgCtx.getLocalException());
                }
                return pe;
            } else if (msgCtx.getLocalException() != null) {
                // use the factory, it'll throw the right thing:
                return ExceptionFactory.makeWebServiceException(msgCtx.getLocalException());
            }
        } finally {
            // Free the incoming input stream
            try {
                msgCtx.freeInputStream();
            } catch (IOException ioe) {
                return ExceptionFactory.makeWebServiceException(ioe);
            }
        }

        return null;
    }

    /**
     * Returns a boolean indicating whether or not the MessageContext contained a fault.
     *
     * @param msgCtx
     * @return
     */
    public boolean hasFaultResponse(MessageContext msgCtx) {
    	if(!msgCtx.getAxisMessageContext().getOptions().isExceptionToBeThrownOnSOAPFault()){
        	if(log.isDebugEnabled()){
        		log.debug("msgCtx.Options.isExceptionToBeThrownOnSOAPFault set to false; Exception will not be thrown on fault");
        	}
    		return false;
    	}
        if (msgCtx.getMessage() != null && msgCtx.getMessage().isFault())
            return true;
        else if (msgCtx.getLocalException() != null)
            return true;
        else
            return false;
    }

    /*
     * Configure any properties that will be needed on the Message
     */
    private void setupMessageProperties(Message msg) {
        // If the user has enabled MTOM on the SOAPBinding, we need
        // to make sure that gets pushed to the Message object.
        Binding binding = (Binding) getBinding();
        if (binding != null && binding instanceof SOAPBinding) {
            SOAPBinding soapBinding = (SOAPBinding)binding;
            if (soapBinding.isMTOMEnabled())
                msg.setMTOMEnabled(true);
        }
    }

    /*
    * Checks to see if the parameter for the invocation is valid
    * given the scenario that the client is operating in.  There are
    * some cases when nulls are allowed and others where it is
    * an error.
    */
    private boolean isValidInvocationParam(Object object) {
        String bindingId = endpointDesc.getClientBindingID();

        // If no bindingId was found, use the default.
        if (bindingId == null) {
            bindingId = SOAPBinding.SOAP11HTTP_BINDING;
        }

        // If it's not an HTTP_BINDING, then we can allow for null params,  
        // but only in PAYLOAD mode per JAX-WS Section 4.3.2.
        if (!bindingId.equals(HTTPBinding.HTTP_BINDING)) {
            if (mode.equals(Mode.MESSAGE) && object == null) {
                throw ExceptionFactory.makeWebServiceException(Messages.getMessage("dispatchNullParamMessageMode"));
            }
        } else {
            // In all cases (PAYLOAD and MESSAGE) we must throw a WebServiceException
            // if the parameter is null and request method is POST or PUT.
            if (object == null && isPOSTorPUTRequest()) {
                throw ExceptionFactory.makeWebServiceException(Messages.getMessage("dispatchNullParamHttpBinding"));
            }
        }

        if (object instanceof DOMSource) {
            DOMSource ds = (DOMSource)object;
            if (ds.getNode() == null && ds.getSystemId() == null) {
                throw ExceptionFactory.makeWebServiceException(Messages.getMessage("dispatchBadDOMSource"));
            }
        }

        // If we've gotten this far, then all is good.
        return true;
    }
    
    private boolean isPOSTorPUTRequest() {
        String method = (String)this.requestContext.get(jakarta.xml.ws.handler.MessageContext.HTTP_REQUEST_METHOD);
        // if HTTP_REQUEST_METHOD is not specified, assume it is a POST method
        return (method == null || 
                HTTPConstants.HEADER_POST.equalsIgnoreCase(method) || 
                HTTPConstants.HEADER_PUT.equalsIgnoreCase(method));
    }
    
    private Message createRequestMessage(Object obj) throws WebServiceException {
        
        // Check to see if the object is a valid invocation parameter.  
        // Then create the message from the object.
        // If an exception occurs, it is local to the client and therefore is a
        // WebServiceException (and not ProtocolExceptions).
        // This code complies with JAX-WS 2.0 sections 4.3.2, 4.3.3 and 4.3.4.
        if (!isValidInvocationParam(obj)) {
            throw ExceptionFactory.makeWebServiceException(Messages.getMessage("dispatchInvalidParam"));
        } 
        Message requestMsg = null;
        try {
             requestMsg = createMessageFromValue(obj);
        } catch (Throwable t) {
            // The webservice exception wraps the thrown exception.
            throw ExceptionFactory.makeWebServiceException(t);
        }
        return requestMsg;
    }
    
    private void preInvokeInit(InvocationContext requestIC) {
        /*
         * if SESSION_MAINTAIN_PROPERTY is true, and the client app has explicitly set a HEADER_COOKIE on the request context, assume the client
         * app is expecting the HEADER_COOKIE to be the session id.  If we were establishing a new session, no cookie would be sent, and the 
         * server would reply with a "Set-Cookie" header, which is copied as a "Cookie"-keyed property to the service context during response.
         * In this case, if we succeed in using an existing server session, no "Set-Cookie" header will be returned, and therefore no
         * "Cookie"-keyed property would be set on the service context.  So, let's copy our request context HEADER_COOKIE key to the service
         * context now to prevent the "no cookie" exception in BindingProvider.setupSessionContext.  It is possible the server does not support
         * sessions, in which case no error occurs, but the client app would assume it is participating in a session.
         */
        if ((requestContext.containsKey(BindingProvider.SESSION_MAINTAIN_PROPERTY)) && ((Boolean)requestContext.get(BindingProvider.SESSION_MAINTAIN_PROPERTY))) {
            if ((requestContext.containsKey(HTTPConstants.HEADER_COOKIE)) && (requestContext.get(HTTPConstants.HEADER_COOKIE) != null)) {
                if (requestIC.getServiceClient().getServiceContext().getProperty(HTTPConstants.HEADER_COOKIE) == null) {
                    requestIC.getServiceClient().getServiceContext().setProperty(HTTPConstants.HEADER_COOKIE, requestContext.get(HTTPConstants.HEADER_COOKIE));
                    if (log.isDebugEnabled()) {
                        log.debug("Client-app defined Cookie property (assume to be session cookie) on request context copied to service context." +
                                "  Caution:  server may or may not support sessions, but client app will not be informed when not supported.");
                    }
                }
            }
        }
    }
}
