com.microsoft.tfs.core.ws.runtime.client.SOAPService.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.core.ws.runtime.client.SOAPService.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.core.ws.runtime.client;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.URI;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.microsoft.tfs.core.httpclient.Credentials;
import com.microsoft.tfs.core.httpclient.Header;
import com.microsoft.tfs.core.httpclient.HttpClient;
import com.microsoft.tfs.core.httpclient.HttpMethod;
import com.microsoft.tfs.core.httpclient.HttpStatus;
import com.microsoft.tfs.core.httpclient.MultiThreadedHttpConnectionManager;
import com.microsoft.tfs.core.httpclient.StatusLine;
import com.microsoft.tfs.core.httpclient.URIException;
import com.microsoft.tfs.core.httpclient.auth.AuthScope;
import com.microsoft.tfs.core.httpclient.methods.PostMethod;
import com.microsoft.tfs.core.httpclient.params.HttpClientParams;
import com.microsoft.tfs.core.httpclient.util.EncodingUtil;
import com.microsoft.tfs.core.ws.runtime.Messages;
import com.microsoft.tfs.core.ws.runtime.client.TransportRequestHandler.Status;
import com.microsoft.tfs.core.ws.runtime.exceptions.EndpointNotFoundException;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthException;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthFailedException;
import com.microsoft.tfs.core.ws.runtime.exceptions.InvalidServerResponseException;
import com.microsoft.tfs.core.ws.runtime.exceptions.ProxyUnauthorizedException;
import com.microsoft.tfs.core.ws.runtime.exceptions.SOAPFault;
import com.microsoft.tfs.core.ws.runtime.exceptions.ServiceErrorException;
import com.microsoft.tfs.core.ws.runtime.exceptions.TransportException;
import com.microsoft.tfs.core.ws.runtime.exceptions.TransportRequestHandlerCanceledException;
import com.microsoft.tfs.core.ws.runtime.exceptions.UnauthorizedException;
import com.microsoft.tfs.core.ws.runtime.stax.StaxFactoryProvider;
import com.microsoft.tfs.core.ws.runtime.xml.XMLStreamReaderHelper;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.GUID;
import com.microsoft.tfs.util.LocaleUtil;
import com.microsoft.tfs.util.tasks.CanceledException;
import com.microsoft.tfs.util.tasks.TaskMonitorService;
import com.microsoft.tfs.util.xml.DOMCreateUtils;
import com.microsoft.tfs.util.xml.DOMSerializeUtils;

/**
 * Base class for SOAP service implementations. One of {@link SOAP11Service} or
 * {@link SOAP12Service} is extended to provide a stub implementation.
 */
public abstract class SOAPService {
    private final static Log log = LogFactory.getLog(SOAPService.class);
    private final static Log base64log = LogFactory.getLog("base64." + SOAPService.class.getName()); //$NON-NLS-1$

    private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$

    protected static final int RESPONSE_MAX_SIZE_FOR_DEBUG_LOGGING = 1024 * 1024;

    /**
     * If the system property "teamexplorer.soap.disable-gzip=true" then we do
     * not tell the server that we support gzip compression in our SOAP
     * requests, therefore the responses will come back uncompressed. The
     * default is to say that we support compression which means that the server
     * has the option of compressing our responses before transmitting over the
     * wire - however some responses will not be compressed - it is dependant on
     * the server.
     */
    protected static final boolean ALLOW_RESPONSE_COMPRESSION = !Boolean
            .getBoolean("teamexplorer.soap.disable-gzip"); //$NON-NLS-1$

    /**
     * The HTTP client used by this stub for all network operations. Must be
     * configured with a {@link MultiThreadedHttpConnectionManager}.
     */
    private final HttpClient client;

    /**
     * The complete URI to the SOAP endpoint this stub will use.
     */
    private final URI endpoint;

    /**
     * The qualified name of the SOAP port this stub will use.
     */
    private final QName port;

    /**
     * The SOAPHeader provider, if any. May be <code>null</code>.
     */
    private volatile SOAPHeaderProvider soapHeaderProvider;

    /**
     * Optional exception handler for transport configuration and authentication
     * exceptions. May be <code>null</code>.
     */
    private final List<TransportRequestHandler> transportRequestHandlers = new ArrayList<TransportRequestHandler>();

    /**
     * The Accept-Langauge HTTP request header value, if any. May be
     * <code>null</code>.
     */
    private volatile String acceptLanguageHeaderValue;

    private volatile boolean promptForCredentials = true;

    /**
     * Turn on coalescing so text processing is easier. This must remain enabled
     * or the generated web service classes must be updated.
     */
    private final static XMLInputFactory xmlInputFactory = StaxFactoryProvider.getXMLInputFactory(true);

    /**
     * Create a stub that will use the given HttpClient instance. The client's
     * connection manager <b>must</b> be an instance of
     * {@link MultiThreadedHttpConnectionManager}. The client <b>must</b> also
     * have its client param "http.protocol.expect-continue" set to false.
     *
     * @param client
     *        an HttpClient instance to use (not null).
     * @param endpoint
     *        the complete URI to the SOAP endpoint to use (not null).
     * @param port
     *        the qualified name fo the SOAP port to use (not null).
     */
    public SOAPService(final HttpClient client, final URI endpoint, final QName port) {
        Check.notNull(client, "client"); //$NON-NLS-1$
        Check.notNull(endpoint, "endpoint"); //$NON-NLS-1$
        Check.notNull(port, "port"); //$NON-NLS-1$

        this.client = client;
        this.endpoint = endpoint;
        this.port = port;
    }

    /**
     * Create a stub that will allocate its own HttpClient (with its own
     * HttpConnectionManager).
     *
     * @param endpoint
     *        the complete URI to the SOAP endpoint to use (not null).
     * @param port
     *        the qualified name fo the SOAP port to use (not null).
     */
    public SOAPService(final URI endpoint, final QName port) {
        Check.notNull(endpoint, "endpoint"); //$NON-NLS-1$
        Check.notNull(port, "port"); //$NON-NLS-1$

        this.endpoint = endpoint;
        this.port = port;

        client = new HttpClient(new MultiThreadedHttpConnectionManager());
        final HttpClientParams params = new HttpClientParams();
        params.setBooleanParameter("http.protocol.expect-continue", false); //$NON-NLS-1$
        client.setParams(params);
    }

    /**
     * Sets the {@link SOAPHeaderProvider} for this stub.
     *
     * @param soapHeaderProvider
     *        the header provider (may be <code>null</code>)
     */
    public void setSOAPHeaderProvider(final SOAPHeaderProvider soapHeaderProvider) {
        this.soapHeaderProvider = soapHeaderProvider;
    }

    /**
     * Sets the {@link Locale} used to set the Accept-Language HTTP header for
     * every HTTP request. The {@link Locale} is tranformed into the header
     * value using {@link LocaleUtil#localeToRFC5646LanguageTag(Locale)}.
     *
     * @param locale
     *        the {@link Locale} to use to set the header. Pass
     *        <code>null</code> to disable sending this header.
     */
    public void setAcceptLanguage(final Locale locale) {
        if (locale != null) {
            try {
                acceptLanguageHeaderValue = LocaleUtil.localeToRFC5646LanguageTag(locale);
            } catch (final IllegalArgumentException e) {
                log.error("Couldn't turn Locale into Accept-Language header", e); //$NON-NLS-1$
                acceptLanguageHeaderValue = null;
            }
        } else {
            acceptLanguageHeaderValue = null;
        }

        log.debug(MessageFormat.format("Accept-Language header set to: {0}", acceptLanguageHeaderValue)); //$NON-NLS-1$
    }

    /**
     * @return <code>true</code> if the application may prompt the user for
     *         credentials if there was an authentication failure using this
     *         service, <code>false</code> if the application should not prompt
     *         the user
     */
    public boolean isPromptForCredentials() {
        return promptForCredentials;
    }

    /**
     * Sets whether the application should prompt for credentials after an
     * authentication failure using this service.
     *
     * @param promptForCredentials
     *        <code>true</code> if the application may prompt,
     *        <code>false</code> if it should not
     */
    public void setPromptForCredentials(final boolean promptForCredentials) {
        this.promptForCredentials = promptForCredentials;
    }

    /**
     * Sets this as a {@link TransportRequestHandler} for this stub. Users may
     * configure multiple transport request handlers, they will be called in the
     * order they are added.
     *
     * @param transportAuthHandler
     *        the request handler (not <code>null</code>)
     */
    public void addTransportRequestHandler(final TransportRequestHandler transportRequestHandler) {
        Check.notNull(transportRequestHandler, "transportRequestHandler"); //$NON-NLS-1$

        synchronized (transportRequestHandlers) {
            transportRequestHandlers.add(transportRequestHandler);
        }
    }

    /**
     * Removes this as a {@link TransportRequestHandler} for this stub. Users
     * may configure multiple transport request handlers, they will be called in
     * the order they are added.
     *
     * @param transportAuthHandler
     *        the request handler (not <code>null</code>)
     */
    public void removeTransportRequestHandler(final TransportRequestHandler transportRequestHandler) {
        Check.notNull(transportRequestHandler, "transportRequestHandler"); //$NON-NLS-1$

        synchronized (transportRequestHandlers) {
            transportRequestHandlers.remove(transportRequestHandler);
        }
    }

    /**
     * Sets any required HTTP request headers on the given method, which is
     * already initialized.
     *
     * @param method
     *        the HTTP method on which to set any request headers required by
     *        this stub.
     * @param invokedMethod
     *        the SOAP method being invoked.
     */
    protected void setRequestHeaders(final HttpMethod method, final String invokedMethod) {
        if (ALLOW_RESPONSE_COMPRESSION) {
            // If debugging then do not want the response compressed - makes it
            // easier to see what the
            // server is doing.
            method.addRequestHeader("Accept-Encoding", "gzip"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        if (acceptLanguageHeaderValue != null) {
            /*
             * TFS uses this header to return error messages and other web
             * content in the correct locale.
             */
            method.addRequestHeader("Accept-Language", acceptLanguageHeaderValue); //$NON-NLS-1$
        }
    }

    /**
     * Builds the SOAPRequestEntity used to perform a SOAP request.
     *
     * @return a new SOAPRequestEntity.
     */
    protected abstract SOAPRequestEntity buildRequestEntity(String invokedMethodName,
            SOAPMethodRequestWriter requestWriter);

    /**
     * @return the default SOAP namespace used for this stub.
     */
    protected abstract String getDefaultSOAPNamespace();

    /**
     * Create a SOAP request for the given method name. The writing of the
     * request body is delegated to the given request writer.
     *
     * @param methodName
     *        the method name to invoke (not null or emtpy).
     * @param requestWriter
     *        the request writer that will do the work of writing the request
     *        body (except the SOAP envelope). If null, no request stream writer
     *        is invoked (an empty request is sent).
     * @return the SOAP request that was created.
     */
    protected SOAPRequest createSOAPRequest(final String methodName, final SOAPMethodRequestWriter requestWriter) {
        final PostMethod method = new PostMethod(endpoint.toString());
        method.setContentChunked(false);

        // Let the extending classes insert their headers, if required (SOAP 1.1
        // needs the SOAPAction header).
        setRequestHeaders(method, methodName);

        /*
         * The content type doesn't need to be set here, because the request
         * entity declares it and HttpClient will set the header from the
         * declared value.
         */

        // The SOAP request entity does all the work of composing the message.
        final SOAPRequestEntity requestEntity = buildRequestEntity(methodName, requestWriter);

        requestEntity.setSOAPHeaderProvider(soapHeaderProvider);

        /*
         * We must wrap the request entity in a buffered version so its content
         * length can be known (the request body is buffered in memory before it
         * is sent). This also makes the NTLM authentication process more
         * efficient since repeated calls to the request entity to construct its
         * body can be done from the buffer.
         */
        final BufferedSOAPRequestEntity bufferedEntity = new BufferedSOAPRequestEntity(requestEntity);

        // Use the buffered wrapper.
        method.setRequestEntity(bufferedEntity);

        return new SOAPRequest(method, requestEntity);
    }

    /**
     * Execute a SOAP request that was built via
     * {@link #createSOAPRequest(String, SOAPMethodRequestWriter)}
     *
     * @param request
     *        the request to execute (not null).
     * @param responseName
     *        the name of the SOAP response message for this request (not null)
     * @param responseReader
     *        the response reader that will do the work of reading the response
     *        (except the SOAP envelope). If null, no response stream reader is
     *        invoked (no response data is read except for the SOAP envelope and
     *        body elements).
     * @throws SOAPFault
     *         if a SOAP fault was returned by the server.
     * @throws UnauthorizedException
     *         if the client could not contact the server because of an
     *         authorization error (HTTP 401).
     * @throws ProxyUnauthorizedException
     *         if the client could not authenticate to the HTTP proxy
     * @throws FederatedAuthException
     *         if the client could not contact the server because it lacks the
     *         proper federated authentication (ACS) cookies and the federated
     *         authentication handler (set by
     *         {@link #setTransportAuthHandler(TransportAuthHandler)} ) did not
     *         handle the exception. The caller is expected to obtain the
     *         cookies and resubmit.
     * @throws InvalidServerResponseException
     *         if the server returned data that could not be parsed as XML or
     *         SOAP.
     * @throws EndpointNotFoundException
     *         if the server returned HTTP 404 when the request was executed.
     * @throws TransportException
     *         if some other an IO error occurred.
     * @throws TransportRequestHandlerCanceledException
     *         if the user cancelled the prompt for credentials
     */
    protected void executeSOAPRequest(final SOAPRequest request, final String responseName,
            final SOAPMethodResponseReader responseReader) throws SOAPFault, UnauthorizedException,
            ProxyUnauthorizedException, FederatedAuthException, InvalidServerResponseException,
            EndpointNotFoundException, TransportException, TransportRequestHandlerCanceledException {
        /*
         * Duplicate the transport request handler map so we needn't keep a lock
         * and so that we have a consistent set throughout execution.
         */
        final List<TransportRequestHandler> requestHandlers = new ArrayList<TransportRequestHandler>();

        synchronized (transportRequestHandlers) {
            requestHandlers.addAll(transportRequestHandlers);
        }

        /*
         * Allow the transport authentication handler to process initial
         * credentials. This can happen if we're lazily authenticating and we do
         * not yet have a full set of credentials.
         */
        final AtomicBoolean cancel = new AtomicBoolean(false);

        for (final TransportRequestHandler requestHandler : requestHandlers) {
            // cancel doesn't stop us from invoking handlers
            if (requestHandler.prepareRequest(this, request, cancel) == Status.COMPLETE) {
                break;
            }
        }

        if (cancel.get()) {
            throw new TransportRequestHandlerCanceledException();
        }

        /*
         * Execute this method in a retry loop. On exceptions, we can delegate
         * to a user configured exception handler, which may modify the method
         * and allow us to resubmit.
         *
         * The typical use case for this is ACS authentication - it can expire
         * in the middle of a call and we want to prompt the user to
         * reauthenticate.
         */

        RuntimeException failure = null;
        do {
            try {
                executeSOAPRequestInternal(request, responseName, responseReader);
                break;
            } catch (final RuntimeException e) {
                // Give the handlers a chance to handle/correct/cancel this
                // exception

                boolean exceptionHandled = false;
                cancel.set(false);

                for (final TransportRequestHandler requestHandler : requestHandlers) {
                    // cancel doesn't stop us from invoking handlers
                    if (requestHandler.handleException(this, request, e, cancel) == Status.COMPLETE) {
                        /*
                         * This handler handled the exception - defer all others
                         * from attempting to handle it and reset the auth
                         * state.
                         */
                        request.getPostMethod().getHostAuthState().invalidate();

                        failure = null;
                        exceptionHandled = true;
                        break;
                    }

                    // Status was CONTINUE, continue with next handler
                }

                // Wasn't handled, prepare to throw it
                if (!exceptionHandled) {
                    // The user wants to cancel, convert to a cancel
                    if (cancel.get()) {
                        failure = new TransportRequestHandlerCanceledException();
                    } else {
                        failure = e;
                    }
                    break;
                }

                // Exception handled, loop to retry
            }
        } while (true);

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

        for (final TransportRequestHandler requestHandler : requestHandlers) {
            requestHandler.handleSuccess(this, request);
        }
    }

    private void executeSOAPRequestInternal(final SOAPRequest request, final String responseName,
            final SOAPMethodResponseReader responseReader)
            throws SOAPFault, UnauthorizedException, ProxyUnauthorizedException, InvalidServerResponseException,
            EndpointNotFoundException, TransportException, CanceledException {
        final PostMethod method = request.getPostMethod();

        final long start = System.currentTimeMillis();
        long serverExecute = -1;
        long contentLength = -1;
        int response = -1;
        boolean isCompressed = false;

        IOException ioException = null;
        byte[] responseBytes = null;
        TraceInputStream responseStream = null;

        try {
            /*
             * Our implementation aims to be tolerant of connection resets
             * caused by half-open sockets. It detects them and retries the
             * operation once.
             *
             * Here's the problem: sometimes IIS's ASP.NET worker process is
             * recycled (this can happen because of an application pool time
             * threshold, number of requests served threshold, memory usage
             * threshold, etc.). When the process is recycled, most sockets that
             * were connected to it (including client sockets) will continue to
             * work fine once IIS builds a new worker. Sometimes, however, bad
             * things happen: those connected sockets will be reset by the
             * server the first time they're used.
             *
             * Since the default TFS configuration (as of RC) is to recycle the
             * application pool's worker every 29 hours, a user is likely to
             * have a half-open TCP socket if he leaves his client running
             * through the night. The client may work correctly, but in the case
             * that it receives a reset, it is pretty safe to retry the
             * operation once.
             *
             * Some JREs use the string "Connection reset by peer", others use
             * "Connection reset". We will match both.
             */
            final long serverStart = System.currentTimeMillis();
            try {
                response = client.executeMethod(method);
            } catch (final SocketException e) {
                /*
                 * If the user cancelled the current task, we might get a
                 * "socket closed" exception if the HTTPConnectionCanceller
                 * closed the socket after timing out waiting for voluntary
                 * cancel.
                 */
                if (TaskMonitorService.getTaskMonitor().isCanceled() && (e.getMessage().startsWith("Socket closed") //$NON-NLS-1$
                        || e.getMessage().startsWith("Stream closed"))) //$NON-NLS-1$
                {
                    throw new CanceledException();
                }

                /*
                 * If this fault was not a TCP connection reset, rethrow it.
                 */
                if (e.getMessage().startsWith("Connection reset") == false) //$NON-NLS-1$
                {
                    throw e;
                }

                log.warn("Retrying invoke after a connection reset", e); //$NON-NLS-1$

                /*
                 * Give it one more try on the user's behalf.
                 */
                response = client.executeMethod(method);
            }
            serverExecute = System.currentTimeMillis() - serverStart;

            responseStream = getResponseStream(method);
            isCompressed = responseStream.isCompressed();

            switch (response) {
            case HttpStatus.SC_OK:
                XMLStreamReader reader = null;

                try {

                    reader = SOAPService.xmlInputFactory.createXMLStreamReader(responseStream,
                            SOAPRequestEntity.SOAP_ENCODING);

                    /*
                     * Read as far as the SOAP body from the stream.
                     */
                    final QName envelopeQName = new QName(getDefaultSOAPNamespace(), "Envelope", "soap"); //$NON-NLS-1$ //$NON-NLS-2$
                    final QName headerQName = new QName(getDefaultSOAPNamespace(), "Header", "soap"); //$NON-NLS-1$ //$NON-NLS-2$
                    final QName bodyQName = new QName(getDefaultSOAPNamespace(), "Body", "soap"); //$NON-NLS-1$ //$NON-NLS-2$

                    // Read the envelope.
                    if (reader.nextTag() == XMLStreamConstants.START_ELEMENT
                            && reader.getName().equals(envelopeQName)) {
                        while (reader.nextTag() == XMLStreamConstants.START_ELEMENT) {
                            if (reader.getName().equals(headerQName)) {
                                // Ignore headers for now.
                                XMLStreamReaderHelper.readUntilElementEnd(reader);
                            } else if (reader.getName().equals(bodyQName)) {
                                /*
                                 * The first element in the body should be
                                 * the desired response element, which we
                                 * must find (and read into) before we
                                 * delegate to the reader (if there is one).
                                 */
                                if (reader.nextTag() == XMLStreamConstants.START_ELEMENT
                                        && reader.getName().getLocalPart().equals(responseName)) {
                                    try {
                                        if (responseReader != null) {
                                            responseReader.readSOAPResponse(reader, responseStream);
                                        }
                                    } catch (final XMLStreamException e) {
                                        throw new InvalidServerResponseException(e);
                                    }

                                    return;
                                }
                            }
                        }
                    }

                    /*
                     * If we got here, some error happened (we couldn't find
                     * our envelope and body tags).
                     */
                    throw new InvalidServerResponseException(
                            "The server's response does not seem to be a SOAP message."); //$NON-NLS-1$
                } catch (final XMLStreamException e) {
                    final String messageFormat = "The server's response could not be parsed as XML: {0}"; //$NON-NLS-1$
                    final String message = MessageFormat.format(messageFormat, e.getMessage());
                    throw new InvalidServerResponseException(message);
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (final XMLStreamException e) {
                        }
                    }
                }
            case HttpStatus.SC_UNAUTHORIZED:
            case HttpStatus.SC_MOVED_TEMPORARILY:
                /*
                 * This may be an ACS or on-premises authentication failure,
                 * examine the headers.
                 */
                examineHeadersForFederatedAuthURL(method);
            case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
                throw new ProxyUnauthorizedException(client.getHostConfiguration().getProxyHost(),
                        client.getHostConfiguration().getProxyPort(),
                        client.getState().getProxyCredentials(AuthScope.ANY));
            case HttpStatus.SC_SERVICE_UNAVAILABLE:
                /*
                 * An error message may be inside the response, in the
                 * headers.
                 */
                examineHeadersForErrorMessage(method);
            case HttpStatus.SC_INTERNAL_SERVER_ERROR:
                /*
                 * A SOAP fault may be inside the response.
                 */
                examineBodyForFault(method);
            default:
                final String messageFormat = "The SOAP endpoint {0} could not be contacted.  HTTP status: {1}"; //$NON-NLS-1$
                final String message = MessageFormat.format(messageFormat, method.getURI().toString(),
                        Integer.toString(response));
                throw new EndpointNotFoundException(message, response);
            }
        } catch (final IOException e) {
            ioException = e;
            throw new TransportException(e.getMessage(), e);
        } finally {
            final long total = System.currentTimeMillis() - start;

            if (responseStream != null) {
                try {
                    responseStream.close();
                } catch (final IOException e) {
                    ioException = e;
                }
                responseBytes = responseStream.getBytes();
                contentLength = responseStream.getTotalBytes();
            }
            /*
             * perform logging
             */
            try {
                if (log.isDebugEnabled()) {
                    logExtended(method, serverExecute, total, contentLength, isCompressed, responseBytes,
                            ioException);
                } else {
                    log.info(makeNormalLogEntry(method, serverExecute, total, contentLength, isCompressed));
                }
            } catch (final Throwable t) {
                /*
                 * don't propogate any errors raised while logging
                 */
                log.warn("Error logging SOAP call", t); //$NON-NLS-1$
            }

            method.releaseConnection();
        }
    }

    protected TraceInputStream getResponseStream(final PostMethod method) throws IOException {
        boolean isCompressed = false;
        InputStream responseStream;
        final Header encoding = method.getResponseHeader("Content-Encoding"); //$NON-NLS-1$
        if (encoding != null && encoding.getValue().equalsIgnoreCase("gzip")) //$NON-NLS-1$
        {
            responseStream = new GZIPInputStream(method.getResponseBodyAsStream());
            isCompressed = true;
        } else {
            responseStream = method.getResponseBodyAsStream();
        }

        // Calculate if we want to store read bytes (useful for debugging)
        int storeBytes = -1;
        if (log.isTraceEnabled() || (log.isDebugEnabled()
                && method.getResponseContentLength() <= RESPONSE_MAX_SIZE_FOR_DEBUG_LOGGING)) {
            storeBytes = RESPONSE_MAX_SIZE_FOR_DEBUG_LOGGING;
        }

        return new TraceInputStream(responseStream, storeBytes, isCompressed);
    }

    private void logExtended(final PostMethod method, final long serverExecuteMs, final long totalMs,
            final long contentLength, final boolean isCompressed, final byte[] responseBytes, final Throwable t)
            throws IOException {
        final StringBuffer sb = new StringBuffer();
        final String newline = System.getProperty("line.separator"); //$NON-NLS-1$

        /*
         * the first line of the trace log message is the same as a normal log
         * message
         */
        sb.append(makeNormalLogEntry(method, serverExecuteMs, totalMs, contentLength, isCompressed))
                .append(newline);

        /*
         * append request name, path, and headers
         */
        sb.append(method.getName() + " " + method.getPath()).append(newline); //$NON-NLS-1$
        sb.append(newline);

        final Header[] requestHeaders = method.getRequestHeaders();
        for (int i = 0; i < requestHeaders.length; i++) {
            sb.append(requestHeaders[i].getName() + ": " + requestHeaders[i].getValue()).append(newline); //$NON-NLS-1$
        }
        sb.append(newline);

        /*
         * append request body
         */
        final ByteArrayOutputStream requestBodyByteStream = new ByteArrayOutputStream();

        /*
         * this line makes the assumption that we're using
         * BufferedSoapRequestEntity
         */
        method.getRequestEntity().writeRequest(requestBodyByteStream);

        final String requestBodyString = requestBodyByteStream.toString(SOAPRequestEntity.SOAP_ENCODING);
        sb.append(requestBodyString).append(newline);

        /*
         * separate request and response portions of the trace log entry
         */
        sb.append(newline);

        /*
         * In the case of multiple failed attempts at method execution, the
         * PostMethod may come back to us with a null status line, which will
         * cause NullPointerExceptions when invoking some of its methods (like
         * getStatusCode()). We can prevent the exceptions by checking for this
         * internal state.
         */
        final StatusLine statusLine = method.getStatusLine();

        /*
         * append response code and headers
         */
        sb.append(((statusLine != null) ? method.getStatusCode() : -1) + " " //$NON-NLS-1$
                + ((statusLine != null) ? method.getStatusText() : "<no status line>")).append(newline); //$NON-NLS-1$
        sb.append(newline);

        final Header[] responseHeaders = method.getResponseHeaders();
        for (int i = 0; i < responseHeaders.length; i++) {
            sb.append(responseHeaders[i].getName() + ": " + responseHeaders[i].getValue()).append(newline); //$NON-NLS-1$
        }
        sb.append(newline);

        String responseBodyAsString = null;
        if (responseBytes != null) {
            responseBodyAsString = EncodingUtil.getString(responseBytes, method.getResponseCharSet());
        }

        StringBuffer base64buffer = null;

        /*
         * If we have a response body, and the base64log is enabled or we
         * couldn't convert the response body to a string, we then log a base64
         * representation of the response body to the base64log.
         */
        if (responseBytes != null && (base64log.isDebugEnabled() || responseBodyAsString == null)) {
            final String base64guid = GUID.newGUIDString();

            final String base64EncodedResponse = getFormattedBase64Encoding(responseBytes);

            base64buffer = new StringBuffer();
            base64buffer.append("-- " //$NON-NLS-1$
                    + base64guid + " base64 encoded response: " //$NON-NLS-1$
                    + responseBytes.length + " byte(s) --"); //$NON-NLS-1$
            base64buffer.append(newline);
            base64buffer.append(base64EncodedResponse).append(newline);
            base64buffer.append("-- end base64 encoded response --").append(newline); //$NON-NLS-1$

            sb.append("-- base64 response key: " + base64guid).append(newline); //$NON-NLS-1$
            sb.append(newline);
        }

        sb.append(responseBodyAsString != null ? responseBodyAsString : "-- RESPONSE UNAVAILABLE --"); //$NON-NLS-1$
        sb.append(newline);

        /*
         * if an error occurred during the low-level HTTP call, log it
         */
        if (t != null) {
            sb.append(newline);
            final StringWriter sw = new StringWriter();
            final PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            pw.flush();
            sb.append("ERROR: " + t.getMessage()).append(newline); //$NON-NLS-1$
            sb.append(sw.toString());
        }

        /*
         * finish off the log entry with a separator
         */
        sb.append("--------------------------------------------------------------------------------"); //$NON-NLS-1$
        // String end = sb.toString().substring(sb.length() > 200 ? sb.length()
        // - 200 : 0);
        log.debug(sb.toString());

        /*
         * write the base64 log last - the ensures that if both logs are being
         * appended to the same file, the regular log entry appears first
         */
        if (base64buffer != null) {
            base64log.debug(base64buffer.toString());
        }
    }

    protected String getFormattedBase64Encoding(final byte[] responseBytes) {
        String unformatted;
        try {
            unformatted = new String(Base64.encodeBase64(responseBytes), "US-ASCII"); //$NON-NLS-1$
        } catch (final UnsupportedEncodingException e) {
            /*
             * should never happen: every Java implementation must support
             * US-ASCII encoding:
             * http://java.sun.com/j2se/1.4.2/docs/api/java/nio
             * /charset/Charset.html
             */
            throw new RuntimeException(e);
        }
        final String newline = System.getProperty("line.separator"); //$NON-NLS-1$

        final StringBuffer sb = new StringBuffer();

        int ix = 0;
        while (ix < unformatted.length()) {
            final int newIx = Math.min(ix + 80, unformatted.length());
            sb.append(unformatted.substring(ix, newIx));
            if (newIx < unformatted.length()) {
                sb.append(newline);
            }
            ix = newIx;
        }

        return sb.toString();
    }

    private String makeNormalLogEntry(final PostMethod method, final long serverExecuteMs, final long totalMs,
            final long contentLength, final boolean isCompressed) {
        final String methodName = ((BufferedSOAPRequestEntity) method.getRequestEntity()).getMethodName();

        /*
         * In the case of multiple failed attempts at method execution, the
         * PostMethod may come back to us with a null status line, which will
         * cause NullPointerExceptions when invoking some of its methods (like
         * getStatusCode()). We can prevent the exceptions by checking for this
         * internal state.
         */
        final StatusLine statusLine = method.getStatusLine();

        return "SOAP method=\'" //$NON-NLS-1$
                + methodName + "\', status=" //$NON-NLS-1$
                + ((statusLine != null) ? method.getStatusCode() : -1) + ", content-length=" //$NON-NLS-1$
                + contentLength + ", server-wait=" //$NON-NLS-1$
                + serverExecuteMs + " ms, parse=" //$NON-NLS-1$
                + (totalMs - serverExecuteMs) + " ms, total=" //$NON-NLS-1$
                + totalMs + " ms, throughput=" //$NON-NLS-1$
                + Math.round(contentLength / Math.max(totalMs, 1F) * 1000F) + " B/s" //$NON-NLS-1$
                + (isCompressed ? ", gzip" : ", uncompressed"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    protected void finishSOAPRequest(final SOAPRequest request) {
        request.getPostMethod().releaseConnection();
    }

    /**
     * Look at the message body for an XML document that might contain a SOAP
     * fault. If one is found, {@link SOAPFault} is thrown; else
     * {@link InvalidServerResponseException} is thrown.
     *
     * @param method
     *        the method containing the (as of yet) unread response body.
     * @throws SOAPFault
     *         if a SOAP fault is found in the body.
     * @throws InvalidServerResponseException
     *         if no fault was found.
     */
    public void examineBodyForFault(final PostMethod method) throws SOAPFault, InvalidServerResponseException {
        Document doc;

        byte[] responseBody = null;
        InputStream responseBodyStream = null;
        if (method.getResponseContentLength() != -1
                && method.getResponseContentLength() <= RESPONSE_MAX_SIZE_FOR_DEBUG_LOGGING) {
            try {
                responseBody = method.getResponseBody();
                responseBodyStream = new ByteArrayInputStream(responseBody);
            } catch (final IOException ex) {
                // ignore
            }
        }

        if (responseBodyStream == null) {
            try {
                responseBodyStream = method.getResponseBodyAsStream();
            } catch (final IOException e) {
                throw new InvalidServerResponseException("Unable to parse obtain the server's response.", e); //$NON-NLS-1$
            }
        }

        try {
            doc = DOMCreateUtils.parseStream(method.getResponseBodyAsStream(), null);
        } catch (final Exception e) {
            if (responseBody != null) {
                final String base64Body = getFormattedBase64Encoding(responseBody);
                final String messageFormat = "unable to parse server's response body:{0}{1}"; //$NON-NLS-1$
                final String message = MessageFormat.format(messageFormat, NEWLINE, base64Body);
                log.warn(message);
            }

            throw new InvalidServerResponseException(
                    "The server's error could not be parsed as XML.  No SOAP fault found.", //$NON-NLS-1$
                    e);
        }

        /*
         * this subclass method will throw a SOAPFault if the correct soap fault
         * elements are present in the DOM
         */
        examineResponseDOMForFault(doc);

        /*
         * Couldn't find SOAP fault elements in the body.
         */

        final String messageFormat = "no soap fault found in server response from 500: {0}{1}"; //$NON-NLS-1$
        final String message = MessageFormat.format(messageFormat, NEWLINE, DOMSerializeUtils.toString(doc));
        log.warn(message);

        throw new InvalidServerResponseException(
                "The server's error could not be parsed as XML.  No SOAP fault found."); //$NON-NLS-1$
    }

    private void examineHeadersForFederatedAuthURL(final PostMethod method)
            throws URIException, FederatedAuthException {
        final Header locationHeader = method.getResponseHeader("Location"); //$NON-NLS-1$
        final Header fedAuthRedirectHeader = method.getResponseHeader("X-TFS-FedAuthRedirect"); //$NON-NLS-1$

        final Header fedAuthIssuerHeader = method.getResponseHeader("X-TFS-FedAuthIssuer"); //$NON-NLS-1$
        final Header fedAuthRealmHeader = method.getResponseHeader("X-TFS-FedAuthRealm"); //$NON-NLS-1$
        final Header fedServerErrorHeader = method.getResponseHeader("X-TFS-ServiceError"); //$NON-NLS-1$
        final Header[] authenticateHeaders = method.getResponseHeaders("WWW-Authenticate"); //$NON-NLS-1$

        final Credentials credentials = client.getState().getCredentials(AuthScope.ANY);
        final String uri = method.getURI().toString();
        final int statusCode = method.getStatusCode();

        final Header authenticationUrlHeader = fedAuthRedirectHeader != null ? fedAuthRedirectHeader
                : locationHeader;

        if (authenticationUrlHeader != null && fedAuthIssuerHeader != null && fedAuthRealmHeader != null) {
            String authenticationUrl = null;
            String fedAuthIssuer = null;
            String fedAuthRealm = null;
            String fedServerError = null;
            String[] mechanisms = new String[0];

            try {
                authenticationUrl = URLDecoder.decode(authenticationUrlHeader.getValue(), "UTF-8"); //$NON-NLS-1$
                fedAuthIssuer = URLDecoder.decode(fedAuthIssuerHeader.getValue(), "UTF-8"); //$NON-NLS-1$
                fedAuthRealm = URLDecoder.decode(fedAuthRealmHeader.getValue(), "UTF-8"); //$NON-NLS-1$
                fedServerError = URLDecoder.decode(fedServerErrorHeader.getValue(), "UTF-8"); //$NON-NLS-1$

                if (authenticateHeaders != null) {
                    mechanisms = new String[authenticateHeaders.length];

                    for (int i = 0; i < authenticateHeaders.length; i++) {
                        mechanisms[i] = URLDecoder.decode(authenticateHeaders[i].getValue(), "UTF-8"); //$NON-NLS-1$
                    }
                }
            } catch (final Exception e) {
                log.warn("Could not decode federated authentication URL as UTF-8", e); //$NON-NLS-1$
            }

            if (authenticationUrl != null && fedAuthRealm != null) {
                if (statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    /* Throw a FederatedAuthException so that issuers know. */
                    throw new FederatedAuthException(uri, authenticationUrl, fedAuthIssuer, fedAuthRealm,
                            mechanisms, credentials, fedServerError);
                } else {
                    throw new FederatedAuthFailedException(fedServerError, fedAuthRealm);
                }
            }
        }

        /*
         * No ACS URL or Realm found, it could be a 401 response from
         * on-premises server or another error.
         */
        if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
            throw new UnauthorizedException(uri, credentials);
        } else {
            final String messageFormat = Messages.getString("SOAPService.SoapEndpointCouldNotBeContactedFormat"); //$NON-NLS-1$
            final String message = MessageFormat.format(messageFormat, uri, Integer.toString(statusCode));

            throw new EndpointNotFoundException(message, statusCode);
        }
    }

    /**
     * Look at the message headers for an appropriate service error. If one is
     * found, {@link ServiceErrorException} is thrown; else
     * {@link EndpointNotFoundException} is thrown.
     *
     * @param method
     *        the method containing the (as of yet) unread response body.
     * @throws URIException
     * @throws ServiceErrorException
     *         if a SOAP fault is found in the body.
     * @throws EndpointNotFoundException
     *         if no fault was found.
     * @throws URIException
     *         if no fault was found and the uri was invalid
     */
    public void examineHeadersForErrorMessage(final PostMethod method)
            throws ServiceErrorException, EndpointNotFoundException, URIException {
        final String errorMessage = getServerErrorMessage(method);

        if (errorMessage != null) {
            throw new ServiceErrorException(errorMessage);
        }

        final String messageFormat = Messages.getString("SOAPService.SoapEndpointCouldNotBeContactedFormat"); //$NON-NLS-1$
        final String message = MessageFormat.format(messageFormat, method.getURI().toString(),
                Integer.toString(method.getStatusCode()));
        throw new EndpointNotFoundException(message, method.getStatusCode());
    }

    private String getServerErrorMessage(final PostMethod method) {
        final Header serviceErrorHeader = method.getResponseHeader("X-TFS-ServiceError"); //$NON-NLS-1$

        if (serviceErrorHeader != null) {
            try {
                return URLDecoder.decode(serviceErrorHeader.getValue(), "UTF-8"); //$NON-NLS-1$
            } catch (final Exception e) {
                log.warn("Could not decode service error message as UTF-8", e); //$NON-NLS-1$
            }

        }

        return null;
    }

    /**
     * Examines the given response body DOM for elements representing a SOAP
     * fault. If such elements are found, throw a new SOAPFault. If such
     * elements are not found, do nothing.
     *
     * @param responseDOM
     *        the parsed reponse body as a DOM
     */
    protected abstract void examineResponseDOMForFault(Document responseDOM);

    /**
     * Given a Node, gets its child node by name. If no matching child is found,
     * returns null.
     *
     * @param node
     *        the node to search. If null, null is returned.
     * @param childName
     *        the child name to search for (not null).
     * @return the child node that matched the given childName, null if none
     *         found or given node is null.
     */
    protected Node getChildByName(final Node node, final String childName) {
        Check.notNull(childName, "childName"); //$NON-NLS-1$

        if (node == null) {
            return null;
        }

        final NodeList children = node.getChildNodes();

        for (int i = 0; i < children.getLength(); i++) {
            final Node child = children.item(i);

            if (child != null && child.getNodeName().equalsIgnoreCase(childName)) {
                return child;
            }
        }

        return null;
    }

    /**
     * Gets the HttpClient in use by this stub so that it may be configured or
     * examined.
     *
     * @return the HttpClient used by this stub.
     */
    public final HttpClient getHTTPClient() {
        return client;
    }

    /**
     * Gets the HttpClient this stub is using for its operations. Will not be
     * null.
     *
     * @return the HttpClient this stub is using (not null).
     */
    public HttpClient getClient() {
        return client;
    }

    /**
     * @return the SOAP endpoint in use by this stub.
     */
    public URI getEndpoint() {
        return endpoint;
    }

    /**
     * @return the SOAP port in use by this stub.
     */
    public QName getPort() {
        return port;
    }
}