fi.laverca.util.CommonsHTTPSender.java Source code

Java tutorial

Introduction

Here is the source code for fi.laverca.util.CommonsHTTPSender.java

Source

/*
 * Copyright 2001-2004 The Apache Software Foundation.
 * 
 * Licensed 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.
 */
/* ==========================================
 * Laverca Project
 * https://sourceforge.net/projects/laverca/
 * ==========================================
 * Copyright 2015 Laverca Project
 * 
 * Licensed 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.
 * 
 * Modified by Eemeli Miettinen to use Commons HTTPComponents in Laverca
 *
 * Again modified by Matti Aarnio to fit in Kiuru MSSP core services.
 */

package fi.laverca.util;

import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.xml.soap.MimeHeader;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPException;

import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.handlers.BasicHandler;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.axis.utils.JavaUtils;
import org.apache.axis.utils.Messages;
import org.apache.axis.utils.NetworkUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

/**
 * A replacement of the default Axis Commons HTTP sender that makes it
 * possible to share a connection manager among RoamingClient instances.
 */
// TODO: Clean up deprecated code & update Commons HTTP Client lib
@SuppressWarnings({ "serial", "deprecation" })
public class CommonsHTTPSender extends BasicHandler {

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

    private static int connectionTimeout = 30000; // At most 30 seconds to wait for a socket connection to form

    private static final ThreadLocal<HttpClient> settings = new ThreadLocal<HttpClient>();

    /**
     * Initializes the thread local HTTP client
     * @param hc HttpClient
     */
    public static void initThreadLocals(final HttpClient hc) {
        log.debug("initThreadLocals()");
        log.debug("  sslSocketFactory = " + hc);

        settings.set(hc);
    }

    boolean httpChunkStream = true; // Use HTTP chunking or not.

    public CommonsHTTPSender() {
        // Empty constructor
    }

    /**
     * invoke creates a socket connection, sends the request SOAP message and then
     * reads the response SOAP message back from the SOAP server
     *
     * @param msgContext the messsage context
     *
     * @throws AxisFault if there was an error sending the SOAP message
     */
    @Override
    public void invoke(final MessageContext msgContext) throws AxisFault {

        HttpPost post = null;
        HttpResponse response = null;
        if (log.isDebugEnabled()) {
            log.debug(Messages.getMessage("enter00", "CommonsHTTPSender::invoke"));
        }
        try {

            HttpClient httpClient = settings.get();

            URL targetURL = new URL(msgContext.getStrProp(MessageContext.TRANS_URL));

            Message reqMessage = msgContext.getRequestMessage();
            post = new HttpPost(targetURL.toString());

            // set false as default, addContentInfo can overwrite
            HttpParams params = post.getParams();
            HttpProtocolParams.setUseExpectContinue(params, false);

            addContextInfo(post, httpClient, msgContext, targetURL);

            MessageRequestEntity requestEntity = null;
            if (msgContext.isPropertyTrue(HTTPConstants.MC_GZIP_REQUEST)) {
                requestEntity = new GzipMessageRequestEntity(post, reqMessage, httpChunkStream);
            } else {
                requestEntity = new MessageRequestEntity(post, reqMessage, httpChunkStream);
            }
            post.setEntity(requestEntity);

            String httpVersion = msgContext.getStrProp(MessageContext.HTTP_TRANSPORT_VERSION);
            if (httpVersion != null) {
                if (httpVersion.equals(HTTPConstants.HEADER_PROTOCOL_V10)) {
                    params.setParameter(httpVersion, HttpVersion.HTTP_1_0);
                }
            }

            HttpContext localContext = new BasicHttpContext();

            if (httpClient == null) {
                // We might end up here if initThreadLocals() was not properly called
                log.fatal("Initialization failed: No HTTPClient");
                throw new AxisFault("Initialization failed: No HTTPClient");
            }

            response = httpClient.execute(post, localContext);
            int returnCode = response.getStatusLine().getStatusCode();

            String contentType = getHeader(post, HTTPConstants.HEADER_CONTENT_TYPE);
            String contentLocation = getHeader(post, HTTPConstants.HEADER_CONTENT_LOCATION);
            String contentLength = getHeader(post, HTTPConstants.HEADER_CONTENT_LENGTH);

            if ((returnCode > 199) && (returnCode < 300)) {

                // SOAP return is OK - so fall through
            } else if (msgContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS) {
                // For now, if we're SOAP 1.2, fall through, since the range of
                // valid result codes is much greater
            } else if ((contentType != null) && !contentType.equals("text/html")
                    && ((returnCode > 499) && (returnCode < 600))) {
                // SOAP Fault should be in here - so fall through

            } else {
                String statusMessage = response.getStatusLine().getReasonPhrase();
                AxisFault fault = new AxisFault("HTTP", "(" + returnCode + ")" + statusMessage, null, null);

                try {
                    String body = getResponseBodyAsString(response);
                    fault.setFaultDetailString(Messages.getMessage("return01", "" + returnCode, body));
                    fault.addFaultDetail(Constants.QNAME_FAULTDETAIL_HTTPERRORCODE, Integer.toString(returnCode));
                    throw fault;
                } finally {
                    HttpClientUtils.closeQuietly(response);
                    post.releaseConnection();
                }
            }

            // After this phase, the response and post are NOT to be closed/released
            // in this code path! See comments further below at "AXIS closure processing rules"

            // Wrap the response body stream so that close() also releases
            // the connection back to the pool.
            InputStream releaseConnectionOnCloseStream = createConnectionReleasingInputStream(post, response);

            Header contentEncoding = response.getFirstHeader(HTTPConstants.HEADER_CONTENT_ENCODING);
            if (contentEncoding != null) {
                if (contentEncoding.getValue().equalsIgnoreCase(HTTPConstants.COMPRESSION_GZIP)) {
                    releaseConnectionOnCloseStream = new GZIPInputStream(releaseConnectionOnCloseStream);
                } else {
                    try {
                        releaseConnectionOnCloseStream.close();
                    } catch (Throwable t) {
                        // ignore
                    }
                    throw new AxisFault("HTTP",
                            "unsupported content-encoding of '" + contentEncoding.getValue() + "' found", null,
                            null);
                }
            }
            Message outMsg = new Message(releaseConnectionOnCloseStream, false, contentType, contentLocation);
            // Transfer HTTP headers of HTTP message to MIME headers of SOAP message
            Header[] responseHeaders = post.getAllHeaders();
            MimeHeaders responseMimeHeaders = outMsg.getMimeHeaders();
            for (int i = 0; i < responseHeaders.length; i++) {
                Header responseHeader = responseHeaders[i];
                responseMimeHeaders.addHeader(responseHeader.getName(), responseHeader.getValue());
            }
            outMsg.setMessageType(Message.RESPONSE);
            msgContext.setResponseMessage(outMsg);
            if (log.isDebugEnabled()) {
                if (null == contentLength) {
                    log.debug("\n" + Messages.getMessage("no00", "Content-Length"));
                }
                log.debug("\n" + Messages.getMessage("xmlRecd00"));
                log.debug("-----------------------------------------------");
                log.debug(outMsg.getSOAPPartAsString());
            }

        } catch (Exception e) {
            log.debug(e);
            throw AxisFault.makeFault(e);

        } finally {

            // AXIS closure processing rules..
            //
            // 1: Always release the connection back to the pool
            //    IF it was ONE WAY invocation

            if (msgContext.isPropertyTrue("axis.one.way")) {
                HttpClientUtils.closeQuietly(response);
                if (post != null) {
                    post.releaseConnection();
                }
            } else {
                log.debug("A HTTP POST which did NOT plan to release the HTTP connection back to the pool");
            }

            // 2: Otherwise the Axis machinery will process call
            //    close() on the releaseConnectionOnCloseStream.
        }

        if (log.isDebugEnabled()) {
            log.debug(Messages.getMessage("exit00", "CommonsHTTPSender::invoke"));
        }
    }

    /**
     * Extracts info from message context.
     *
     * @param method Post method
     * @param httpClient The client used for posting
     * @param msgContext the message context
     * @param tmpURL the url to post to.
     *
     * @throws Exception if any error occurred
     */
    private void addContextInfo(final HttpPost method, final HttpClient httpClient, final MessageContext msgContext,
            final URL tmpURL) throws Exception {
        HttpParams params = method.getParams();

        if (msgContext.getTimeout() != 0) {
            // optionally set a timeout for response waits
            HttpConnectionParams.setSoTimeout(params, msgContext.getTimeout());
        }

        // Always set the 30 second timeout on establishing the connection
        HttpConnectionParams.setConnectionTimeout(params, connectionTimeout);

        Message msg = msgContext.getRequestMessage();
        if (msg != null) {
            method.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, msg.getContentType(msgContext.getSOAPConstants()));
        }

        if (msgContext.useSOAPAction()) {
            // define SOAPAction header
            String action = msgContext.getSOAPActionURI();
            if (action != null && !"".equals(action))
                method.setHeader(HTTPConstants.HEADER_SOAP_ACTION, "\"" + action + "\"");
        }

        String userID = msgContext.getUsername();
        String passwd = msgContext.getPassword();

        // if UserID is not part of the context, but is in the URL, use
        // the one in the URL.
        if ((userID == null) && (tmpURL.getUserInfo() != null)) {
            String info = tmpURL.getUserInfo();
            int sep = info.indexOf(':');

            if ((sep >= 0) && (sep + 1 < info.length())) {
                userID = info.substring(0, sep);
                passwd = info.substring(sep + 1);
            } else {
                userID = info;
            }
        }
        if (userID != null) {
            Credentials proxyCred = new UsernamePasswordCredentials(userID, passwd);
            // if the username is in the form "user\domain"
            // then use NTCredentials instead.
            int domainIndex = userID.indexOf("\\");
            if (domainIndex > 0) {
                String domain = userID.substring(0, domainIndex);
                if (userID.length() > domainIndex + 1) {
                    String user = userID.substring(domainIndex + 1);
                    proxyCred = new NTCredentials(user, passwd, NetworkUtils.getLocalHostname(), domain);
                }
            }
            ((DefaultHttpClient) httpClient).getCredentialsProvider().setCredentials(AuthScope.ANY, proxyCred);
        }

        // add compression headers if needed
        if (msgContext.isPropertyTrue(HTTPConstants.MC_ACCEPT_GZIP)) {
            method.addHeader(HTTPConstants.HEADER_ACCEPT_ENCODING, HTTPConstants.COMPRESSION_GZIP);
        }
        if (msgContext.isPropertyTrue(HTTPConstants.MC_GZIP_REQUEST)) {
            method.addHeader(HTTPConstants.HEADER_CONTENT_ENCODING, HTTPConstants.COMPRESSION_GZIP);
        }

        // Transfer MIME headers of SOAPMessage to HTTP headers.
        MimeHeaders mimeHeaders = msg.getMimeHeaders();
        if (mimeHeaders != null) {
            for (Iterator<?> i = mimeHeaders.getAllHeaders(); i.hasNext();) {
                MimeHeader mimeHeader = (MimeHeader) i.next();
                //HEADER_CONTENT_TYPE and HEADER_SOAP_ACTION are already set.
                //Let's not duplicate them.
                String headerName = mimeHeader.getName();
                if (headerName.equals(HTTPConstants.HEADER_CONTENT_TYPE)
                        || headerName.equals(HTTPConstants.HEADER_SOAP_ACTION)) {
                    continue;
                }
                method.addHeader(mimeHeader.getName(), mimeHeader.getValue());
            }
        }

        // process user defined headers for information.
        Hashtable<?, ?> userHeaderTable = (Hashtable<?, ?>) msgContext.getProperty(HTTPConstants.REQUEST_HEADERS);

        if (userHeaderTable != null) {
            for (Iterator<?> e = userHeaderTable.entrySet().iterator(); e.hasNext();) {
                Map.Entry<?, ?> me = (Map.Entry<?, ?>) e.next();
                Object keyObj = me.getKey();

                if (null == keyObj) {
                    continue;
                }
                String key = keyObj.toString().trim();
                String value = me.getValue().toString().trim();

                if (key.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT)
                        && value.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT_100_Continue)) {
                    HttpProtocolParams.setUseExpectContinue(params, true);
                } else if (key.equalsIgnoreCase(HTTPConstants.HEADER_TRANSFER_ENCODING_CHUNKED)) {
                    String val = me.getValue().toString();
                    if (null != val) {
                        httpChunkStream = JavaUtils.isTrue(val);
                    }
                } else {
                    method.addHeader(key, value);
                }
            }
        }
    }

    private static String getResponseBodyAsString(final HttpResponse resp) throws IOException {
        HttpEntity ent = resp.getEntity();
        if (ent == null)
            throw new IOException();
        try {
            return EntityUtils.toString(ent, "UTF-8");
        } catch (IOException e) {
            throw e;
        } catch (Exception e) { // org.apache.http.ParseException
            throw new IOException(e);
        }
    }

    private static String getHeader(HttpPost method, String headerName) {
        Header header = method.getFirstHeader(headerName);
        return (header == null) ? null : header.getValue().trim();
    }

    private InputStream createConnectionReleasingInputStream(final HttpPost post, final HttpResponse response)
            throws IOException {
        return new FilterInputStream(response.getEntity().getContent()) {
            @Override
            public void close() throws IOException {
                if (log.isDebugEnabled())
                    log.debug("Close http response, and release post method connection");
                try {
                    super.close();
                } finally {
                    HttpClientUtils.closeQuietly(response);
                    post.releaseConnection();
                }
            }
        };
    }

    private static class MessageRequestEntity implements HttpEntity {

        private HttpPost method;
        private Message message;
        boolean httpChunkStream = true; //Use HTTP chunking or not.

        public MessageRequestEntity(HttpPost method, Message message) {
            this.message = message;
            this.method = method;
        }

        public MessageRequestEntity(HttpPost method, Message message, boolean httpChunkStream) {
            this.message = message;
            this.method = method;
            this.httpChunkStream = httpChunkStream;
        }

        @Override
        public boolean isRepeatable() {
            return true;
        }

        protected boolean isContentLengthNeeded() {
            return this.method.getProtocolVersion() == HttpVersion.HTTP_1_0 || !httpChunkStream;
        }

        @Override
        public long getContentLength() {
            if (isContentLengthNeeded()) {
                try {
                    return message.getContentLength();
                } catch (Exception e) {
                }
            }
            return -1; /* -1 for chunked */
        }

        @Override
        public Header getContentType() {
            return null; // a separate header is added
        }

        @Override
        public void consumeContent() throws IOException {
            EntityUtils.consume(method.getEntity());
        }

        @Override
        public InputStream getContent() throws IOException, IllegalStateException {
            return null;
        }

        @Override
        public Header getContentEncoding() {
            return null;
        }

        @Override
        public boolean isChunked() {
            return true;
        }

        @Override
        public boolean isStreaming() {
            return false;
        }

        @Override
        public void writeTo(OutputStream out) throws IOException {
            try {
                this.message.writeTo(out);
            } catch (SOAPException e) {
                throw new IOException(e.getMessage());
            }
        }

    }

    private static class GzipMessageRequestEntity extends MessageRequestEntity {

        public GzipMessageRequestEntity(HttpPost method, Message message) {
            super(method, message);
        }

        public GzipMessageRequestEntity(HttpPost method, Message message, boolean httpChunkStream) {
            super(method, message, httpChunkStream);
        }

        public void writeRequest(OutputStream out) throws IOException {
            if (cachedStream != null) {
                cachedStream.writeTo(out);
            } else {
                GZIPOutputStream gzStream = new GZIPOutputStream(out);
                super.writeTo(gzStream);
                gzStream.finish();
            }
        }

        @Override
        public long getContentLength() {
            if (isContentLengthNeeded()) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                try {
                    writeRequest(baos);
                    cachedStream = baos;
                    return baos.size();
                } catch (IOException e) {
                    // fall through to doing chunked.
                }
            }
            return -1; // do chunked
        }

        private ByteArrayOutputStream cachedStream;
    }
}