/**
 * 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.cxf.endpoint;

import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.namespace.QName;
import jakarta.xml.ws.handler.MessageContext;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.binding.Binding;
import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.classloader.ClassLoaderUtils.ClassLoaderHolder;
import org.apache.cxf.common.i18n.UncheckedException;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.interceptor.AbstractBasicInterceptorProvider;
import org.apache.cxf.interceptor.ClientOutFaultObserver;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.interceptor.InterceptorProvider;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.ExchangeImpl;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.phase.PhaseChainCache;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.phase.PhaseManager;
import org.apache.cxf.service.Service;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.BindingMessageInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.MessageInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.MessageObserver;
import org.apache.cxf.workqueue.SynchronousExecutor;

public class ClientImpl
    extends AbstractBasicInterceptorProvider
    implements Client, Retryable, MessageObserver {

    public static final String THREAD_LOCAL_REQUEST_CONTEXT = "thread.local.request.context";
    /**
     * When a synchronous request/response invoke is done using an asynchronous transport mechanism,
     * this is the timeout used for waiting for the response.  Default is 60 seconds.
     */
    public static final String SYNC_TIMEOUT = "cxf.synchronous.timeout";

    public static final String FINISHED = "exchange.finished";

    private static final Logger LOG = LogUtils.getL7dLogger(ClientImpl.class);

    protected Bus bus;
    protected ConduitSelector conduitSelector;
    protected ClientOutFaultObserver outFaultObserver;
    protected int synchronousTimeout = 60000; // default 60 second timeout

    protected PhaseChainCache outboundChainCache = new PhaseChainCache();
    protected PhaseChainCache inboundChainCache = new PhaseChainCache();

    protected Map<String, Object> currentRequestContext = new ConcurrentHashMap<>(8, 0.75f, 4);
    protected Thread latestContextThread;
    protected Map<Thread, EchoContext> requestContext
        = Collections.synchronizedMap(new WeakHashMap<Thread, EchoContext>());

    protected Map<Thread, ResponseContext> responseContext
        = Collections.synchronizedMap(new WeakHashMap<Thread, ResponseContext>());

    protected Executor executor;

    public ClientImpl(Bus b, Endpoint e) {
        this(b, e, (ConduitSelector)null);
    }

    public ClientImpl(Bus b, Endpoint e, Conduit c) {
        this(b, e, new PreexistingConduitSelector(c));
    }

    public ClientImpl(Bus b, Endpoint e, ConduitSelector sc) {
        bus = b;
        outFaultObserver = new ClientOutFaultObserver(bus);
        getConduitSelector(sc).setEndpoint(e);
        notifyLifecycleManager();
    }

    /**
     * Create a Client that uses a specific EndpointImpl.
     * @param bus
     * @param svc
     * @param port
     * @param endpointImplFactory
     */
    public ClientImpl(Bus bus, Service svc, QName port,
                      EndpointImplFactory endpointImplFactory) {
        this.bus = bus;
        outFaultObserver = new ClientOutFaultObserver(bus);
        EndpointInfo epfo = findEndpoint(svc, port);

        try {
            if (endpointImplFactory != null) {
                getConduitSelector().setEndpoint(endpointImplFactory.newEndpointImpl(bus, svc, epfo));
            } else {
                getConduitSelector().setEndpoint(new EndpointImpl(bus, svc, epfo));
            }
        } catch (EndpointException epex) {
            throw new IllegalStateException("Unable to create endpoint: " + epex.getMessage(), epex);
        }
        notifyLifecycleManager();
    }

    public Bus getBus() {
        return bus;
    }

    public void destroy() {
        if (bus == null) {
            return;
        }
        if (getEndpoint() != null) {
            for (Closeable c : getEndpoint().getCleanupHooks()) {
                try {
                    c.close();
                } catch (IOException e) {
                    //ignore
                }
            }
        }
        ClientLifeCycleManager mgr = bus.getExtension(ClientLifeCycleManager.class);
        if (null != mgr) {
            mgr.clientDestroyed(this);
        }

        if (conduitSelector != null) {
            if (conduitSelector instanceof Closeable) {
                try {
                    ((Closeable)conduitSelector).close();
                } catch (IOException e) {
                    //ignore, we're destroying anyway
                }
            } else {
                getConduit().close();
            }
        }

        bus = null;
        conduitSelector = null;
        outFaultObserver = null;
        outboundChainCache = null;
        inboundChainCache = null;

        currentRequestContext = null;
        requestContext.clear();
        requestContext = null;
        responseContext.clear();
        responseContext = null;
        executor = null;
    }

    private void notifyLifecycleManager() {
        ClientLifeCycleManager mgr = bus.getExtension(ClientLifeCycleManager.class);
        if (null != mgr) {
            mgr.clientCreated(this);
        }
    }

    private EndpointInfo findEndpoint(Service svc, QName port) {
        if (port != null) {
            EndpointInfo epfo = svc.getEndpointInfo(port);
            if (epfo == null) {
                throw new IllegalArgumentException("The service " + svc.getName()
                                                   + " does not have an endpoint " + port + ".");
            }
            return epfo;
        }
        
        for (ServiceInfo svcfo : svc.getServiceInfos()) {
            for (EndpointInfo e : svcfo.getEndpoints()) {
                BindingInfo bfo = e.getBinding();
                String bid = bfo.getBindingId();
                if ("http://schemas.xmlsoap.org/wsdl/soap/".equals(bid)
                    || "http://schemas.xmlsoap.org/wsdl/soap12/".equals(bid)) {
                    for (Object o : bfo.getExtensors().get()) {
                        try {
                            String s = (String)o.getClass().getMethod("getTransportURI").invoke(o);
                            if (s != null && s.endsWith("http")) {
                                return e;
                            }
                        } catch (Throwable t) {
                            //ignore
                        }
                    }
                }
            }
        }
        throw new UnsupportedOperationException(
             "Only document-style SOAP 1.1 and 1.2 http are supported "
             + "for auto-selection of endpoint; none were found.");
    }

    public Endpoint getEndpoint() {
        return getConduitSelector().getEndpoint();
    }

    public void releaseThreadContexts() {
        final Thread t = Thread.currentThread();
        requestContext.remove(t);
        responseContext.remove(t);
    }

    @Override
    public Contexts getContexts() {
        return new Contexts() {
            @Override
            public void close() throws Exception {
                releaseThreadContexts();
            }
            @Override
            public Map<String, Object> getRequestContext() {
                return ClientImpl.this.getRequestContext();
            }
            @Override
            public Map<String, Object> getResponseContext() {
                return ClientImpl.this.getResponseContext();
            }
        };
    }

    public Map<String, Object> getRequestContext() {
        if (isThreadLocalRequestContext()) {
            final Thread t = Thread.currentThread();
            requestContext.computeIfAbsent(t, k -> new EchoContext(currentRequestContext));
            latestContextThread = t;
            return requestContext.get(t);
        }
        return currentRequestContext;
    }
    public Map<String, Object> getResponseContext() {
        if (!responseContext.containsKey(Thread.currentThread())) {
            final Thread t = Thread.currentThread();
            responseContext.put(t, new ResponseContext(responseContext));
        }
        return responseContext.get(Thread.currentThread());
    }
    protected Map<String, Object> setResponseContext(Map<String, Object> ctx) {
        if (ctx instanceof ResponseContext) {
            ResponseContext c = (ResponseContext)ctx;
            responseContext.put(Thread.currentThread(), c);
            return c;
        }
        ResponseContext c = new ResponseContext(ctx, responseContext);
        responseContext.put(Thread.currentThread(), c);
        return c;
    }
    public boolean isThreadLocalRequestContext() {
        Object o = currentRequestContext.get(THREAD_LOCAL_REQUEST_CONTEXT);
        if (o != null) {
            final boolean local;
            if (o instanceof Boolean) {
                local = ((Boolean)o).booleanValue();
            } else {
                local = Boolean.parseBoolean(o.toString());
            }
            return local;
        }
        return false;
    }
    public void setThreadLocalRequestContext(boolean b) {
        currentRequestContext.put(THREAD_LOCAL_REQUEST_CONTEXT, b);
    }


    public Object[] invoke(BindingOperationInfo oi, Object... params) throws Exception {
        return invoke(oi, params, null);
    }

    public Object[] invoke(String operationName, Object... params) throws Exception {
        QName q = new QName(getEndpoint().getService().getName().getNamespaceURI(), operationName);

        return invoke(q, params);
    }

    public Object[] invoke(QName operationName, Object... params) throws Exception {
        BindingOperationInfo op = getEndpoint().getEndpointInfo().getBinding().getOperation(operationName);
        if (op == null) {
            throw new UncheckedException(
                new org.apache.cxf.common.i18n.Message("NO_OPERATION", LOG, operationName));
        }

        if (op.isUnwrappedCapable()) {
            op = op.getUnwrappedOperation();
        }

        return invoke(op, params);
    }

    public Object[] invokeWrapped(String operationName, Object... params) throws Exception {
        QName q = new QName(getEndpoint().getService().getName().getNamespaceURI(), operationName);

        return invokeWrapped(q, params);
    }

    public Object[] invokeWrapped(QName operationName, Object... params) throws Exception {
        BindingOperationInfo op = getEndpoint().getEndpointInfo().getBinding().getOperation(operationName);
        if (op == null) {
            throw new UncheckedException(
                new org.apache.cxf.common.i18n.Message("NO_OPERATION", LOG, operationName));
        }
        return invoke(op, params);
    }

    public Object[] invoke(BindingOperationInfo oi,
                           Object[] params,
                           Exchange exchange) throws Exception {
        Map<String, Object> context = new HashMap<>();
        return invoke(oi, params, context, exchange);
    }
    public Object[] invoke(BindingOperationInfo oi,
                           Object[] params,
                           Map<String, Object> context) throws Exception {
        return invoke(oi, params, context, (Exchange)null);
    }

    public void invoke(ClientCallback callback,
                       String operationName,
                       Object... params) throws Exception {
        QName q = new QName(getEndpoint().getService().getName().getNamespaceURI(), operationName);
        invoke(callback, q, params);
    }

    public void invoke(ClientCallback callback,
                       QName operationName,
                       Object... params) throws Exception {
        BindingOperationInfo op = getEndpoint().getEndpointInfo().getBinding().getOperation(operationName);
        if (op == null) {
            throw new UncheckedException(
                new org.apache.cxf.common.i18n.Message("NO_OPERATION", LOG, operationName));
        }

        if (op.isUnwrappedCapable()) {
            op = op.getUnwrappedOperation();
        }

        invoke(callback, op, params);
    }


    public void invokeWrapped(ClientCallback callback,
                              String operationName,
                              Object... params)
        throws Exception {
        QName q = new QName(getEndpoint().getService().getName().getNamespaceURI(), operationName);
        invokeWrapped(callback, q, params);
    }

    public void invokeWrapped(ClientCallback callback,
                              QName operationName,
                              Object... params)
        throws Exception {
        BindingOperationInfo op = getEndpoint().getEndpointInfo().getBinding().getOperation(operationName);
        if (op == null) {
            throw new UncheckedException(
                new org.apache.cxf.common.i18n.Message("NO_OPERATION", LOG, operationName));
        }
        invoke(callback, op, params);
    }


    public void invoke(ClientCallback callback,
                       BindingOperationInfo oi,
                       Object... params) throws Exception {
        invoke(callback, oi, params, null, null);
    }

    public void invoke(ClientCallback callback,
                       BindingOperationInfo oi,
                       Object[] params,
                       Map<String, Object> context) throws Exception {
        invoke(callback, oi, params, context, null);
    }

    public void invoke(ClientCallback callback,
                       BindingOperationInfo oi,
                       Object[] params,
                       Exchange exchange) throws Exception {
        invoke(callback, oi, params, null, exchange);
    }

    public void invoke(ClientCallback callback,
                       BindingOperationInfo oi,
                       Object[] params,
                       Map<String, Object> context,
                       Exchange exchange) throws Exception {
        doInvoke(callback, oi, params, context, exchange);
    }

    public Object[] invoke(BindingOperationInfo oi,
                           Object[] params,
                           Map<String, Object> context,
                           Exchange exchange) throws Exception {
        return doInvoke(null, oi, params, context, exchange);
    }

    private Object[] doInvoke(final ClientCallback callback,
                              BindingOperationInfo oi,
                              Object[] params,
                              Map<String, Object> context,
                              Exchange exchange) throws Exception {
        Bus origBus = BusFactory.getAndSetThreadDefaultBus(bus);
        ClassLoaderHolder origLoader = null;
        Map<String, Object> resContext = null;
        try {
            ClassLoader loader = bus.getExtension(ClassLoader.class);
            if (loader != null) {
                origLoader = ClassLoaderUtils.setThreadContextClassloader(loader);
            }
            if (exchange == null) {
                exchange = new ExchangeImpl();
            }
            exchange.setSynchronous(callback == null);
            Endpoint endpoint = getEndpoint();
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Invoke, operation info: " + oi + ", params: " + Arrays.toString(params));
            }
            Message message = endpoint.getBinding().createMessage();

            // Make sure INVOCATION CONTEXT, REQUEST_CONTEXT and RESPONSE_CONTEXT are present
            // on message
            if (context == null) {
                context = new HashMap<>();
            }
            Map<String, Object> reqContext = CastUtils.cast((Map<?, ?>)context.get(REQUEST_CONTEXT));
            resContext = CastUtils.cast((Map<?, ?>)context.get(RESPONSE_CONTEXT));
            if (reqContext == null) {
                reqContext = new HashMap<>(getRequestContext());
                context.put(REQUEST_CONTEXT, reqContext);
            }
            if (resContext == null) {
                resContext = new ResponseContext(responseContext);
                context.put(RESPONSE_CONTEXT, resContext);
            }

            message.put(Message.INVOCATION_CONTEXT, context);
            setContext(reqContext, message);
            exchange.putAll(reqContext);

            setParameters(params, message);

            if (null != oi) {
                exchange.setOneWay(oi.getOutput() == null);
            }

            exchange.setOutMessage(message);
            exchange.put(ClientCallback.class, callback);

            setOutMessageProperties(message, oi);
            setExchangeProperties(exchange, endpoint, oi);

            PhaseInterceptorChain chain = setupInterceptorChain(endpoint);
            message.setInterceptorChain(chain);
            if (callback == null) {
                chain.setFaultObserver(outFaultObserver);
            } else {
                // We need to wrap the outFaultObserver if the callback is not null
                // calling the conduitSelector.complete to make sure the fail over feature works
                chain.setFaultObserver(new MessageObserver() {
                    public void onMessage(Message message) {
                        Exception ex = message.getContent(Exception.class);
                        if (ex != null) {
                            completeExchange(message.getExchange());
                            if (message.getContent(Exception.class) == null) {
                                // handle the right response
                                Message inMsg = message.getExchange().getInMessage();
                                Map<String, Object> ctx = responseContext.get(Thread.currentThread());
                                List<Object> resList = CastUtils.cast(inMsg.getContent(List.class));
                                Object[] result = resList == null ? null : resList.toArray();
                                callback.handleResponse(ctx, result);
                                return;
                            }
                        }
                        outFaultObserver.onMessage(message);
                    }
                });
            }
            prepareConduitSelector(message);

            // add additional interceptors and such
            modifyChain(chain, message, false);
            try {
                chain.doIntercept(message);
            } catch (Fault fault) {
                enrichFault(fault);
                throw fault;
            }

            if (callback != null) {
                return null;
            }
            return processResult(message, exchange, oi, resContext);
        } finally {
            //ensure ResponseContext has HTTP RESPONSE CODE
            if (null != exchange) {
                Integer responseCode = (Integer)exchange.get(Message.RESPONSE_CODE);
                resContext.put(MessageContext.HTTP_RESPONSE_CODE, responseCode);
                resContext.put(org.apache.cxf.message.Message.RESPONSE_CODE, responseCode);
                setResponseContext(resContext);
            }
            if (origLoader != null) {
                origLoader.reset();
            }
            if (origBus != bus) {
                BusFactory.setThreadDefaultBus(origBus);
            }
        }
    }

    private void completeExchange(Exchange exchange) {
        getConduitSelector().complete(exchange);
    }

    /**
     * TODO This is SOAP specific code and should not be in cxf core
     * @param fault
     */
    private void enrichFault(Fault fault) {
        if (fault.getCause().getCause() instanceof IOException
                || fault.getCause() instanceof IOException) {
            String soap11NS = "http://schemas.xmlsoap.org/soap/envelope/";
            String soap12NS = "http://www.w3.org/2003/05/soap-envelope";
            QName faultCode = fault.getFaultCode();
            //for SoapFault, if it's underlying cause is IOException,
            //it means something like server is down or can't create
            //connection, according to soap spec we should set fault as
            //Server Fault
            if (faultCode.getNamespaceURI().equals(
                    soap11NS)
                    && "Client".equals(faultCode.getLocalPart())) {
                faultCode = new QName(soap11NS, "Server");
                fault.setFaultCode(faultCode);
            }
            if (faultCode.getNamespaceURI().equals(
                    soap12NS)
                    && "Sender".equals(faultCode.getLocalPart())) {
                faultCode = new QName(soap12NS, "Receiver");
                fault.setFaultCode(faultCode);
            }
        }
    }

    protected Object[] processResult(Message message,
                                   Exchange exchange,
                                   BindingOperationInfo oi,
                                   Map<String, Object> resContext) throws Exception {
        Exception ex = null;
        // Check to see if there is a Fault from the outgoing chain if it's an out Message
        if (!message.get(Message.INBOUND_MESSAGE).equals(Boolean.TRUE)) {
            ex = message.getContent(Exception.class);
        }
        boolean mepCompleteCalled = false;
        if (ex != null) {
            completeExchange(exchange);
            mepCompleteCalled = true;
            if (message.getContent(Exception.class) != null) {
                throw ex;
            }
        }
        ex = message.getExchange().get(Exception.class);
        if (ex != null) {
            if (!mepCompleteCalled) {
                completeExchange(exchange);
            }
            throw ex;
        }

        //REVISIT
        // - use a protocol neutral no-content marker instead of 202?
        // - move the decoupled destination property name into api
        Integer responseCode = (Integer)exchange.get(Message.RESPONSE_CODE);
        if (null != responseCode && 202 == responseCode) {
            Endpoint ep = exchange.getEndpoint();
            if (null != ep && null != ep.getEndpointInfo() && null == ep.getEndpointInfo().
                getProperty("org.apache.cxf.ws.addressing.MAPAggregator.decoupledDestination")) {
                return null;
            }
        }

        // Wait for a response if we need to
        if (oi != null && !oi.getOperationInfo().isOneWay()) {
            waitResponse(exchange);
        }

        // leave the input stream open for the caller
        Boolean keepConduitAlive = (Boolean)exchange.get(Client.KEEP_CONDUIT_ALIVE);
        if (keepConduitAlive == null || !keepConduitAlive) {
            completeExchange(exchange);
        }

        // Grab the response objects if there are any
        List<Object> resList = null;
        Message inMsg = exchange.getInMessage();
        if (inMsg != null) {
            if (null != resContext) {
                resContext.putAll(inMsg);
                // remove the recursive reference if present
                resContext.remove(Message.INVOCATION_CONTEXT);
                setResponseContext(resContext);
            }
            resList = CastUtils.cast(inMsg.getContent(List.class));
        }

        // check for an incoming fault
        ex = getException(exchange);

        if (ex != null) {
            throw ex;
        }

        if (resList == null   
            && oi != null && !oi.getOperationInfo().isOneWay()) {
            
            BindingOperationInfo boi = oi;
            if (boi.isUnwrapped()) {
                boi = boi.getWrappedOperation();
            }
            if (!boi.getOutput().getMessageParts().isEmpty()) {
                //we were supposed to get some output, but didn't.
                throw new IllegalEmptyResponseException("Response message did not contain proper response data."
                    + " Expected: " + boi.getOutput().getMessageParts().get(0).getConcreteName());
            }
        }
        if (resList != null) {
            return resList.toArray();
        }

        return null;
    }
    protected Exception getException(Exchange exchange) {
        if (exchange.getInFaultMessage() != null) {
            return exchange.getInFaultMessage().getContent(Exception.class);
        } else if (exchange.getOutFaultMessage() != null) {
            return exchange.getOutFaultMessage().getContent(Exception.class);
        } else if (exchange.getInMessage() != null) {
            return exchange.getInMessage().getContent(Exception.class);
        }
        return null;
    }

    protected void setContext(Map<String, Object> ctx, Message message) {
        if (ctx != null) {
            message.putAll(ctx);
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("set requestContext to message be" + ctx);
            }
        }
    }

    protected void waitResponse(Exchange exchange) throws IOException {
        synchronized (exchange) {
            long remaining = synchronousTimeout;
            Long o = PropertyUtils.getLong(exchange.getOutMessage(), SYNC_TIMEOUT);
            if (o != null) {
                remaining = o;
            }
            while (!Boolean.TRUE.equals(exchange.get(FINISHED)) && remaining > 0) {
                long start = System.currentTimeMillis();
                try {
                    exchange.wait(remaining);
                } catch (InterruptedException ex) {
                    // ignore
                }
                long end = System.currentTimeMillis();
                remaining -= (int)(end - start);
            }
            if (!Boolean.TRUE.equals(exchange.get(FINISHED))) {
                LogUtils.log(LOG, Level.WARNING, "RESPONSE_TIMEOUT",
                    exchange.getBindingOperationInfo().getOperationInfo().getName().toString());
                String msg = new org.apache.cxf.common.i18n.Message("RESPONSE_TIMEOUT", LOG, exchange
                    .getBindingOperationInfo().getOperationInfo().getName().toString()).toString();
                throw new IOException(msg);
            }
        }
    }

    protected void setParameters(Object[] params, Message message) {
        MessageContentsList contents = new MessageContentsList(params);
        message.setContent(List.class, contents);
    }

    public void onMessage(Message message) {
        if (bus == null) {
            throw new IllegalStateException("Message received on a Client that has been closed or destroyed.");
        }
        Endpoint endpoint = message.getExchange().getEndpoint();
        if (endpoint == null) {
            // in this case correlation will occur outside the transport,
            // however there's a possibility that the endpoint may have been
            // rebased in the meantime, so that the response will be mediated
            // via a set of in interceptors provided by a *different* endpoint
            //
            endpoint = getConduitSelector().getEndpoint();
            message.getExchange().put(Endpoint.class, endpoint);
        }
        message = endpoint.getBinding().createMessage(message);
        message.getExchange().setInMessage(message);
        message.put(Message.REQUESTOR_ROLE, Boolean.TRUE);
        message.put(Message.INBOUND_MESSAGE, Boolean.TRUE);
        PhaseManager pm = bus.getExtension(PhaseManager.class);

        List<Interceptor<? extends Message>> i1 = bus.getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by bus: " + i1);
        }
        List<Interceptor<? extends Message>> i2 = getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by client: " + i2);
        }
        List<Interceptor<? extends Message>> i3 = endpoint.getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by endpoint: " + i3);
        }
        List<Interceptor<? extends Message>> i4 = endpoint.getBinding().getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by binding: " + i4);
        }

        PhaseInterceptorChain chain;
        if (endpoint.getService().getDataBinding() instanceof InterceptorProvider) {
            InterceptorProvider p = (InterceptorProvider)endpoint.getService().getDataBinding();
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Interceptors contributed by databinding: " + p.getInInterceptors());
            }
            chain = inboundChainCache.get(pm.getInPhases(), i1, i2, i3, i4,
                                          p.getInInterceptors());
        } else {
            chain = inboundChainCache.get(pm.getInPhases(), i1, i2, i3, i4);
        }
        message.setInterceptorChain(chain);

        chain.setFaultObserver(outFaultObserver);
        modifyChain(chain, message, true);
        modifyChain(chain, message.getExchange().getOutMessage(), true);

        Bus origBus = BusFactory.getAndSetThreadDefaultBus(bus);
        // execute chain
        ClientCallback callback = message.getExchange().get(ClientCallback.class);
        try {
            if (callback != null) {
                if (callback.isCancelled()) {
                    completeExchange(message.getExchange());
                    return;
                }
                callback.start(message);
            }

            String startingAfterInterceptorID = (String) message.get(
                InterceptorChain.STARTING_AFTER_INTERCEPTOR_ID);
            String startingInterceptorID = (String) message.get(
                InterceptorChain.STARTING_AT_INTERCEPTOR_ID);
            if (startingAfterInterceptorID != null) {
                chain.doInterceptStartingAfter(message, startingAfterInterceptorID);
            } else if (startingInterceptorID != null) {
                chain.doInterceptStartingAt(message, startingInterceptorID);
            } else if (message.getContent(Exception.class) != null) {
                outFaultObserver.onMessage(message);
            } else {
                callback = message.getExchange().get(ClientCallback.class);

                if (callback != null && !isPartialResponse(message)) {
                    try {
                        chain.doIntercept(message);
                    } catch (Throwable error) {
                        //so that asyn callback handler get chance to
                        //handle non-runtime exceptions
                        message.getExchange().setInMessage(message);
                        Map<String, Object> resCtx = CastUtils
                                .cast((Map<?, ?>) message.getExchange()
                                        .getOutMessage().get(
                                                Message.INVOCATION_CONTEXT));
                        resCtx = CastUtils.cast((Map<?, ?>) resCtx
                                .get(RESPONSE_CONTEXT));
                        if (resCtx != null) {
                            setResponseContext(resCtx);
                        }
                        // remove callback so that it won't be invoked twice
                        callback = message.getExchange().remove(ClientCallback.class);
                        if (callback != null) {
                            callback.handleException(resCtx, error);
                        }
                    }
                } else {
                    chain.doIntercept(message);
                }

            }

            callback = message.getExchange().get(ClientCallback.class);
            if (callback == null || isPartialResponse(message)) {
                return;
            }

            // remove callback so that it won't be invoked twice
            callback = message.getExchange().remove(ClientCallback.class);
            if (callback != null) {
                message.getExchange().setInMessage(message);
                Map<String, Object> resCtx = CastUtils.cast((Map<?, ?>)message
                                                                .getExchange()
                                                                .getOutMessage()
                                                                .get(Message.INVOCATION_CONTEXT));
                resCtx = CastUtils.cast((Map<?, ?>)resCtx.get(RESPONSE_CONTEXT));
                if (resCtx != null && responseContext != null) {
                    setResponseContext(resCtx);
                }
                try {
                    Object[] obj = processResult(message, message.getExchange(),
                                                 null, resCtx);

                    callback.handleResponse(resCtx, obj);
                } catch (Throwable ex) {
                    callback.handleException(resCtx, ex);
                }
            }
        } finally {
            if (origBus != bus) {
                BusFactory.setThreadDefaultBus(origBus);
            }
            synchronized (message.getExchange()) {
                if (!isPartialResponse(message)
                    || message.getContent(Exception.class) != null) {
                    message.getExchange().put(FINISHED, Boolean.TRUE);
                    message.getExchange().setInMessage(message);
                    message.getExchange().notifyAll();
                }
            }
        }
    }

    public Conduit getConduit() {
        Message message = new MessageImpl();
        Exchange exchange = new ExchangeImpl();
        message.setExchange(exchange);
        message.putAll(getRequestContext());
        setExchangeProperties(exchange, getEndpoint(), null);
        return getConduitSelector().selectConduit(message);
    }

    protected void prepareConduitSelector(Message message) {
        getConduitSelector().prepare(message);
        message.getExchange().put(ConduitSelector.class, getConduitSelector());
    }

    protected void setOutMessageProperties(Message message, BindingOperationInfo boi) {
        message.put(Message.REQUESTOR_ROLE, Boolean.TRUE);
        message.put(Message.INBOUND_MESSAGE, Boolean.FALSE);
        if (null != boi) {
            message.put(BindingMessageInfo.class, boi.getInput());
            message.put(MessageInfo.class, boi.getOperationInfo().getInput());
        }
    }

    protected void setExchangeProperties(Exchange exchange,
                                         Endpoint endpoint,
                                         BindingOperationInfo boi) {
        if (endpoint != null) {
            exchange.put(Endpoint.class, endpoint);
            exchange.put(Service.class, endpoint.getService());
            exchange.put(Binding.class, endpoint.getBinding());
        }
        if (boi != null) {
            exchange.put(BindingOperationInfo.class, boi);
        }

        if (exchange.isSynchronous() || executor == null) {
            exchange.put(MessageObserver.class, this);
        } else {
            exchange.put(Executor.class, executor);
            exchange.put(MessageObserver.class, new MessageObserver() {
                public void onMessage(final Message message) {
                    if (!message.getExchange()
                        .containsKey(Executor.class.getName() + ".USING_SPECIFIED")) {

                        executor.execute(new Runnable() {
                            public void run() {
                                ClientImpl.this.onMessage(message);
                            }
                        });
                    } else {
                        ClientImpl.this.onMessage(message);
                    }
                }
            });
        }
        exchange.put(Retryable.class, this);
        exchange.put(Client.class, this);
        exchange.put(Bus.class, bus);

        if (endpoint != null) {
            EndpointInfo endpointInfo = endpoint.getEndpointInfo();
            if (boi != null) {
                exchange.put(Message.WSDL_OPERATION, boi.getName());
            }

            QName serviceQName = endpointInfo.getService().getName();
            exchange.put(Message.WSDL_SERVICE, serviceQName);

            QName interfaceQName = endpointInfo.getService().getInterface().getName();
            exchange.put(Message.WSDL_INTERFACE, interfaceQName);

            QName portQName = endpointInfo.getName();
            exchange.put(Message.WSDL_PORT, portQName);
            URI wsdlDescription = endpointInfo.getProperty("URI", URI.class);
            if (wsdlDescription == null) {
                String address = endpointInfo.getAddress();
                try {
                    wsdlDescription = new URI(address + "?wsdl");
                } catch (URISyntaxException e) {
                    // do nothing
                }
                endpointInfo.setProperty("URI", wsdlDescription);
            }
            exchange.put(Message.WSDL_DESCRIPTION, wsdlDescription);
        }
    }

    protected PhaseInterceptorChain setupInterceptorChain(Endpoint endpoint) {

        PhaseManager pm = bus.getExtension(PhaseManager.class);

        List<Interceptor<? extends Message>> i1 = bus.getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by bus: " + i1);
        }
        List<Interceptor<? extends Message>> i2 = getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by client: " + i2);
        }
        List<Interceptor<? extends Message>> i3 = endpoint.getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by endpoint: " + i3);
        }
        List<Interceptor<? extends Message>> i4 = endpoint.getBinding().getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by binding: " + i4);
        }
        List<Interceptor<? extends Message>> i5 = null;
        if (endpoint.getService().getDataBinding() instanceof InterceptorProvider) {
            i5 = ((InterceptorProvider)endpoint.getService().getDataBinding()).getOutInterceptors();
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Interceptors contributed by databinding: " + i5);
            }
        }
        if (i5 != null) {
            return outboundChainCache.get(pm.getOutPhases(), i1, i2, i3, i4, i5);
        }
        return outboundChainCache.get(pm.getOutPhases(), i1, i2, i3, i4);
    }

    protected void modifyChain(InterceptorChain chain, Message ctx, boolean in) {
        if (ctx == null) {
            return;
        }
        Collection<InterceptorProvider> providers
            = CastUtils.cast((Collection<?>)ctx.get(Message.INTERCEPTOR_PROVIDERS));
        if (providers != null) {
            for (InterceptorProvider p : providers) {
                if (in) {
                    chain.add(p.getInInterceptors());
                } else {
                    chain.add(p.getOutInterceptors());
                }
            }
        }
        String key = in ? Message.IN_INTERCEPTORS : Message.OUT_INTERCEPTORS;
        Collection<Interceptor<? extends Message>> is
            = CastUtils.cast((Collection<?>)ctx.get(key));
        if (is != null) {
            chain.add(is);
        }
    }

    protected void setEndpoint(Endpoint e) {
        getConduitSelector().setEndpoint(e);
    }

    public int getSynchronousTimeout() {
        return synchronousTimeout;
    }

    public void setSynchronousTimeout(int synchronousTimeout) {
        this.synchronousTimeout = synchronousTimeout;
    }

    public final ConduitSelector getConduitSelector() {
        return getConduitSelector(null);
    }

    protected final ConduitSelector getConduitSelector(
        ConduitSelector override
    ) {
        if (null == conduitSelector) {
            setConduitSelector(override);
        }
        return conduitSelector;
    }

    public final synchronized void setConduitSelector(ConduitSelector selector) {
        conduitSelector = selector == null ? new UpfrontConduitSelector() : selector;
    }

    private boolean isPartialResponse(Message in) {
        return Boolean.TRUE.equals(in.get(Message.PARTIAL_RESPONSE_MESSAGE));
    }

    @Override
    public void close() throws Exception {
        destroy();
    }


    public class EchoContext extends ConcurrentHashMap<String, Object> {
        private static final long serialVersionUID = 1L;
        public EchoContext(Map<String, Object> sharedMap) {
            super(8, 0.75f, 4);
            putAll(sharedMap);
        }

        public void reload() {
            super.clear();
            super.putAll(requestContext.get(latestContextThread));
        }
        
        @Override
        public void clear() {
            super.clear();
            try {
                for (Map.Entry<Thread, EchoContext> ent : requestContext.entrySet()) {
                    if (ent.getValue() == this) {
                        requestContext.remove(ent.getKey());
                        return;
                    }
                }
            } catch (Throwable t) {
                //ignore
            }
        }
    }

    /**
     * Class to handle the response contexts.   The clear is overloaded to remove
     * this context from the threadLocal caches in the ClientImpl
     */
    static class ResponseContext implements Map<String, Object>, Serializable {
        private static final long serialVersionUID = 2L;
        final Map<String, Object> wrapped;
        final Map<Thread, ResponseContext> responseContext;
        
        ResponseContext(Map<String, Object> origMap, Map<Thread, ResponseContext> rc) {
            wrapped = origMap;
            responseContext = rc;
        }

        ResponseContext(Map<Thread, ResponseContext> rc) {
            wrapped = new HashMap<>();
            responseContext = rc;
        }

        @Override
        public void clear() {
            wrapped.clear();
            try {
                for (Map.Entry<Thread, ResponseContext> ent : responseContext.entrySet()) {
                    if (ent.getValue() == this) {
                        responseContext.remove(ent.getKey());
                        return;
                    }
                }
            } catch (Throwable t) {
                //ignore
            }
        }

        @Override
        public int size() {
            return wrapped.size();
        }
        @Override
        public boolean isEmpty() {
            return wrapped.isEmpty();
        }
        @Override
        public boolean containsKey(Object key) {
            return wrapped.containsKey(key);
        }
        @Override
        public boolean containsValue(Object value) {
            return wrapped.containsKey(value);
        }
        @Override
        public Object get(Object key) {
            return wrapped.get(key);
        }
        @Override
        public Object put(String key, Object value) {
            return wrapped.put(key, value);
        }
        @Override
        public Object remove(Object key) {
            return wrapped.remove(key);
        }
        @Override
        public void putAll(Map<? extends String, ? extends Object> m) {
            wrapped.putAll(m);
        }
        @Override
        public Set<String> keySet() {
            return wrapped.keySet();
        }
        @Override
        public Collection<Object> values() {
            return wrapped.values();
        }
        @Override
        public Set<Entry<String, Object>> entrySet() {
            return wrapped.entrySet();
        }
    }

    public void setExecutor(Executor executor) {
        if (!SynchronousExecutor.isA(executor)) {
            this.executor = executor;
        }
    }

    
    public class IllegalEmptyResponseException extends IllegalStateException {
        private static final long serialVersionUID = 1L;

        public IllegalEmptyResponseException() {
            super();
        }

        public IllegalEmptyResponseException(String message) {
            super(message);
        }

        public IllegalEmptyResponseException(String message, Throwable cause) {
            super(message, cause);
        }

        public IllegalEmptyResponseException(Throwable cause) {
            super(cause);
        }
    }

}
