com.mirth.connect.connectors.http.HttpReceiver.java Source code

Java tutorial

Introduction

Here is the source code for com.mirth.connect.connectors.http.HttpReceiver.java

Source

/*
 * Copyright (c) Mirth Corporation. All rights reserved.
 * 
 * http://www.mirthcorp.com
 * 
 * The software in this package is published under the terms of the MPL license a copy of which has
 * been included with this distribution in the LICENSE.txt file.
 */

package com.mirth.connect.connectors.http;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import javax.security.auth.Subject;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.ListUtils;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.protocol.HTTP;
import org.apache.log4j.Logger;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.DefaultUserIdentity;
import org.eclipse.jetty.security.MappedLoginService.KnownUser;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.security.Constraint;

import com.mirth.connect.connectors.http.HttpStaticResource.ResourceType;
import com.mirth.connect.donkey.model.channel.ConnectorPluginProperties;
import com.mirth.connect.donkey.model.event.ConnectionStatusEventType;
import com.mirth.connect.donkey.model.event.ErrorEventType;
import com.mirth.connect.donkey.model.message.BatchRawMessage;
import com.mirth.connect.donkey.model.message.ConnectorMessage;
import com.mirth.connect.donkey.model.message.RawMessage;
import com.mirth.connect.donkey.model.message.Response;
import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.ConnectorTaskException;
import com.mirth.connect.donkey.server.channel.ChannelException;
import com.mirth.connect.donkey.server.channel.DispatchResult;
import com.mirth.connect.donkey.server.channel.SourceConnector;
import com.mirth.connect.donkey.server.event.ConnectionStatusEvent;
import com.mirth.connect.donkey.server.event.ErrorEvent;
import com.mirth.connect.donkey.server.message.batch.BatchMessageException;
import com.mirth.connect.donkey.server.message.batch.BatchMessageReader;
import com.mirth.connect.donkey.server.message.batch.ResponseHandler;
import com.mirth.connect.donkey.server.message.batch.SimpleResponseHandler;
import com.mirth.connect.donkey.util.Base64Util;
import com.mirth.connect.donkey.util.DonkeyElement.DonkeyElementException;
import com.mirth.connect.plugins.httpauth.AuthenticationResult;
import com.mirth.connect.plugins.httpauth.Authenticator;
import com.mirth.connect.plugins.httpauth.AuthenticatorProvider;
import com.mirth.connect.plugins.httpauth.AuthenticatorProviderFactory;
import com.mirth.connect.plugins.httpauth.HttpAuthConnectorPluginProperties;
import com.mirth.connect.plugins.httpauth.HttpAuthConnectorPluginProperties.AuthType;
import com.mirth.connect.plugins.httpauth.RequestInfo;
import com.mirth.connect.plugins.httpauth.RequestInfo.EntityProvider;
import com.mirth.connect.server.controllers.ChannelController;
import com.mirth.connect.server.controllers.ConfigurationController;
import com.mirth.connect.server.controllers.ControllerFactory;
import com.mirth.connect.server.controllers.EventController;
import com.mirth.connect.server.util.TemplateValueReplacer;
import com.mirth.connect.userutil.MessageHeaders;
import com.mirth.connect.userutil.MessageParameters;
import com.mirth.connect.util.CharsetUtils;

public class HttpReceiver extends SourceConnector implements BinaryContentTypeResolver {
    private Logger logger = Logger.getLogger(this.getClass());
    private HttpReceiverProperties connectorProperties;
    private ConfigurationController configurationController = ControllerFactory.getFactory()
            .createConfigurationController();
    private EventController eventController = ControllerFactory.getFactory().createEventController();
    private final TemplateValueReplacer replacer = new TemplateValueReplacer();
    private HttpConfiguration configuration = null;
    private Server server;
    private String host;
    private int port;
    private int timeout;
    private String[] binaryMimeTypesArray;
    private Pattern binaryMimeTypesRegex;
    private HttpAuthConnectorPluginProperties authProps;
    private AuthenticatorProvider authenticatorProvider;

    @Override
    public void onDeploy() throws ConnectorTaskException {
        this.connectorProperties = (HttpReceiverProperties) getConnectorProperties();

        if (connectorProperties.isXmlBody() && isProcessBatch()) {
            throw new ConnectorTaskException("Batch processing is not supported for Xml Body.");
        }

        // load the default configuration
        String configurationClass = configurationController.getProperty(connectorProperties.getProtocol(),
                "httpConfigurationClass");

        try {
            configuration = (HttpConfiguration) Class.forName(configurationClass).newInstance();
        } catch (Exception e) {
            logger.trace("could not find custom configuration class, using default");
            configuration = new DefaultHttpConfiguration();
        }

        try {
            configuration.configureConnectorDeploy(this);
        } catch (Exception e) {
            throw new ConnectorTaskException(e);
        }

        String replacedBinaryMimeTypes = replacer.replaceValues(connectorProperties.getBinaryMimeTypes(),
                getChannelId(), getChannel().getName());
        if (connectorProperties.isBinaryMimeTypesRegex()) {
            try {
                binaryMimeTypesRegex = Pattern.compile(replacedBinaryMimeTypes);
            } catch (PatternSyntaxException e) {
                throw new ConnectorTaskException(
                        "Invalid binary MIME types regular expression: " + replacedBinaryMimeTypes, e);
            }
        } else {
            binaryMimeTypesArray = StringUtils.split(replacedBinaryMimeTypes.replaceAll("\\s*,\\s*", ",").trim(),
                    ',');
        }

        if (connectorProperties.getPluginProperties() != null) {
            for (ConnectorPluginProperties pluginProperties : connectorProperties.getPluginProperties()) {
                if (pluginProperties instanceof HttpAuthConnectorPluginProperties) {
                    authProps = (HttpAuthConnectorPluginProperties) pluginProperties;
                }
            }
        }

        if (authProps != null && authProps.getAuthType() != AuthType.NONE) {
            try {
                authenticatorProvider = AuthenticatorProviderFactory.getAuthenticatorProvider(this, authProps);
            } catch (Exception e) {
                throw new ConnectorTaskException("Error creating authenticator provider.", e);
            }
        }
    }

    @Override
    public void onUndeploy() throws ConnectorTaskException {
        configuration.configureConnectorUndeploy(this);
    }

    @Override
    public void onStart() throws ConnectorTaskException {
        String channelId = getChannelId();
        String channelName = getChannel().getName();
        host = replacer.replaceValues(connectorProperties.getListenerConnectorProperties().getHost(), channelId,
                channelName);
        port = NumberUtils.toInt(replacer.replaceValues(
                connectorProperties.getListenerConnectorProperties().getPort(), channelId, channelName));
        timeout = NumberUtils
                .toInt(replacer.replaceValues(connectorProperties.getTimeout(), channelId, channelName), 0);

        // Initialize contextPath to "" or its value after replacements
        String contextPath = (connectorProperties.getContextPath() == null ? ""
                : replacer.replaceValues(connectorProperties.getContextPath(), channelId, channelName)).trim();

        /*
         * Empty string and "/" are both valid and equal functionally. However if there is a
         * resource defined, we need to make sure that the context path starts with a slash and
         * doesn't end with one.
         */
        if (!contextPath.startsWith("/")) {
            contextPath = "/" + contextPath;
        }
        if (contextPath.endsWith("/")) {
            contextPath = contextPath.substring(0, contextPath.length() - 1);
        }

        try {
            server = new Server();
            configuration.configureReceiver(this);

            HandlerCollection handlers = new HandlerCollection();
            Handler serverHandler = handlers;

            // Add handlers for each static resource
            if (connectorProperties.getStaticResources() != null) {
                NavigableMap<String, List<HttpStaticResource>> staticResourcesMap = new TreeMap<String, List<HttpStaticResource>>();

                // Add each static resource to a map first to allow sorting and deduplication
                for (HttpStaticResource staticResource : connectorProperties.getStaticResources()) {
                    String resourceContextPath = replacer.replaceValues(staticResource.getContextPath(), channelId,
                            channelName);
                    Map<String, List<String>> queryParameters = new HashMap<String, List<String>>();

                    // If query parameters were specified, extract them here
                    int queryIndex = resourceContextPath.indexOf('?');
                    if (queryIndex >= 0) {
                        String query = resourceContextPath.substring(queryIndex + 1);
                        resourceContextPath = resourceContextPath.substring(0, queryIndex);

                        for (NameValuePair param : URLEncodedUtils.parse(query, Charset.defaultCharset())) {
                            List<String> currentValue = queryParameters.get(param.getName());
                            String value = StringUtils.defaultString(param.getValue());

                            if (currentValue == null) {
                                currentValue = new ArrayList<String>();
                                queryParameters.put(param.getName(), currentValue);
                            }
                            currentValue.add(value);
                        }
                    }

                    // We always want to append resources starting with "/" to the base context path
                    if (resourceContextPath.endsWith("/")) {
                        resourceContextPath = resourceContextPath.substring(0, resourceContextPath.length() - 1);
                    }
                    if (!resourceContextPath.startsWith("/")) {
                        resourceContextPath = "/" + resourceContextPath;
                    }
                    resourceContextPath = contextPath + resourceContextPath;

                    List<HttpStaticResource> staticResourcesList = staticResourcesMap.get(resourceContextPath);
                    if (staticResourcesList == null) {
                        staticResourcesList = new ArrayList<HttpStaticResource>();
                        staticResourcesMap.put(resourceContextPath, staticResourcesList);
                    }
                    staticResourcesList
                            .add(new HttpStaticResource(resourceContextPath, staticResource.getResourceType(),
                                    staticResource.getValue(), staticResource.getContentType(), queryParameters));
                }

                // Iterate through each context path in reverse so that more specific contexts take precedence
                for (List<HttpStaticResource> staticResourcesList : staticResourcesMap.descendingMap().values()) {
                    for (HttpStaticResource staticResource : staticResourcesList) {
                        logger.debug("Adding static resource handler for context path: "
                                + staticResource.getContextPath());
                        ContextHandler resourceContextHandler = new ContextHandler();
                        resourceContextHandler.setContextPath(staticResource.getContextPath());
                        // This allows resources to be requested without a relative context path (e.g. "/")
                        resourceContextHandler.setAllowNullPathInfo(true);
                        resourceContextHandler.setHandler(new StaticResourceHandler(staticResource));
                        handlers.addHandler(resourceContextHandler);
                    }
                }
            }

            // Add the main request handler
            ContextHandler contextHandler = new ContextHandler();
            contextHandler.setContextPath(contextPath);
            contextHandler.setHandler(new RequestHandler());
            handlers.addHandler(contextHandler);

            // Wrap the handler collection in a security handler if needed
            if (authenticatorProvider != null) {
                serverHandler = createSecurityHandler(handlers);
            }
            server.setHandler(serverHandler);

            logger.debug("starting HTTP server with address: " + host + ":" + port);
            server.start();
            eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(),
                    getSourceName(), ConnectionStatusEventType.IDLE));
        } catch (Exception e) {
            eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(),
                    getSourceName(), ConnectionStatusEventType.FAILURE));
            throw new ConnectorTaskException("Failed to start HTTP Listener", e);
        }
    }

    @Override
    public void onStop() throws ConnectorTaskException {
        ConnectorTaskException firstCause = null;

        if (server != null) {
            try {
                logger.debug("stopping HTTP server");
                server.stop();
            } catch (Exception e) {
                firstCause = new ConnectorTaskException("Failed to stop HTTP Listener", e.getCause());
            }
        }

        if (authenticatorProvider != null) {
            authenticatorProvider.shutdown();
        }

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

    @Override
    public void onHalt() throws ConnectorTaskException {
        onStop();
    }

    private class RequestHandler extends AbstractHandler {
        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest servletRequest,
                HttpServletResponse servletResponse) throws IOException, ServletException {
            logger.debug("received HTTP request");
            eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(),
                    getSourceName(), ConnectionStatusEventType.CONNECTED));
            DispatchResult dispatchResult = null;
            String originalThreadName = Thread.currentThread().getName();

            try {
                Thread.currentThread().setName("HTTP Receiver Thread on " + getChannel().getName() + " ("
                        + getChannelId() + ") < " + originalThreadName);
                Map<String, Object> sourceMap = new HashMap<String, Object>();
                Object messageContent = null;

                try {
                    messageContent = getMessage(baseRequest, sourceMap);
                } catch (Throwable t) {
                    sendErrorResponse(servletResponse, dispatchResult, t);
                }

                if (messageContent != null) {
                    if (isProcessBatch()) {
                        if (messageContent instanceof byte[]) {
                            BatchMessageException e = new BatchMessageException(
                                    "Batch processing is not supported for binary data.");
                            logger.error(e.getMessage() + " (channel: " + ChannelController.getInstance()
                                    .getDeployedChannelById(getChannelId()).getName() + ")", e);
                            eventController.dispatchEvent(new ErrorEvent(getChannelId(), getMetaDataId(), null,
                                    ErrorEventType.SOURCE_CONNECTOR, getSourceName(), connectorProperties.getName(),
                                    null, e));
                        } else {
                            try {
                                BatchRawMessage batchRawMessage = new BatchRawMessage(
                                        new BatchMessageReader((String) messageContent), sourceMap);
                                ResponseHandler responseHandler = new SimpleResponseHandler();

                                dispatchBatchMessage(batchRawMessage, responseHandler);

                                dispatchResult = responseHandler.getResultForResponse();
                                sendResponse(baseRequest, servletResponse, dispatchResult);
                            } catch (Throwable t) {
                                sendErrorResponse(servletResponse, dispatchResult, t);
                            }
                        }
                    } else {
                        try {
                            RawMessage rawMessage = null;
                            if (messageContent instanceof byte[]) {
                                rawMessage = new RawMessage((byte[]) messageContent, null, sourceMap);
                            } else {
                                rawMessage = new RawMessage((String) messageContent, null, sourceMap);
                            }

                            dispatchResult = dispatchRawMessage(rawMessage);

                            sendResponse(baseRequest, servletResponse, dispatchResult);
                        } catch (Throwable t) {
                            sendErrorResponse(servletResponse, dispatchResult, t);
                        } finally {
                            finishDispatch(dispatchResult);
                        }
                    }
                }
            } finally {
                eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(),
                        getSourceName(), ConnectionStatusEventType.IDLE));
                Thread.currentThread().setName(originalThreadName);
            }
            baseRequest.setHandled(true);
        }
    }

    private void sendResponse(Request baseRequest, HttpServletResponse servletResponse,
            DispatchResult dispatchResult) throws Exception {
        ContentType contentType = ContentType
                .parse(replaceValues(connectorProperties.getResponseContentType(), dispatchResult));
        if (!connectorProperties.isResponseDataTypeBinary() && contentType.getCharset() == null) {
            /*
             * If text mode is used and a specific charset isn't already defined, use the one from
             * the connector properties. We can't use ContentType.withCharset here because it
             * doesn't preserve other parameters, like boundary definitions
             */
            contentType = ContentType.parse(contentType.toString() + "; charset="
                    + CharsetUtils.getEncoding(connectorProperties.getCharset()));
        }
        servletResponse.setContentType(contentType.toString());

        // set the response headers
        for (Entry<String, List<String>> entry : connectorProperties.getResponseHeaders().entrySet()) {
            for (String headerValue : entry.getValue()) {
                servletResponse.addHeader(entry.getKey(), replaceValues(headerValue, dispatchResult));
            }
        }

        // set the status code
        int statusCode = NumberUtils
                .toInt(replaceValues(connectorProperties.getResponseStatusCode(), dispatchResult), -1);

        /*
         * set the response body and status code (if we choose a response from the drop-down)
         */
        if (dispatchResult != null && dispatchResult.getSelectedResponse() != null) {
            dispatchResult.setAttemptedResponse(true);

            Response selectedResponse = dispatchResult.getSelectedResponse();
            Status newMessageStatus = selectedResponse.getStatus();

            /*
             * If the status code is custom, use the entered/replaced string If is is not a
             * variable, use the status of the destination's response (success = 200, failure = 500)
             * Otherwise, return 200
             */
            if (statusCode != -1) {
                servletResponse.setStatus(statusCode);
            } else if (newMessageStatus != null && newMessageStatus.equals(Status.ERROR)) {
                servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
            } else {
                servletResponse.setStatus(HttpStatus.SC_OK);
            }

            String message = selectedResponse.getMessage();

            if (message != null) {
                OutputStream responseOutputStream = servletResponse.getOutputStream();
                byte[] responseBytes;
                if (connectorProperties.isResponseDataTypeBinary()) {
                    responseBytes = Base64Util.decodeBase64(message.getBytes("US-ASCII"));
                } else {
                    responseBytes = message.getBytes(CharsetUtils.getEncoding(connectorProperties.getCharset()));
                }

                // If the client accepts GZIP compression, compress the content
                boolean gzipResponse = false;
                for (Enumeration<String> en = baseRequest.getHeaders("Accept-Encoding"); en.hasMoreElements();) {
                    String acceptEncoding = en.nextElement();

                    if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
                        gzipResponse = true;
                        break;
                    }
                }

                if (gzipResponse) {
                    servletResponse.setHeader(HTTP.CONTENT_ENCODING, "gzip");
                    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(responseOutputStream);
                    gzipOutputStream.write(responseBytes);
                    gzipOutputStream.finish();
                } else {
                    responseOutputStream.write(responseBytes);
                }

                // TODO include full HTTP payload in sentResponse
            }
        } else {
            /*
             * If the status code is custom, use the entered/replaced string Otherwise, return 200
             */
            if (statusCode != -1) {
                servletResponse.setStatus(statusCode);
            } else {
                servletResponse.setStatus(HttpStatus.SC_OK);
            }
        }
    }

    private void sendErrorResponse(HttpServletResponse servletResponse, DispatchResult dispatchResult, Throwable t)
            throws IOException {
        String responseError = ExceptionUtils.getStackTrace(t);
        logger.error("Error receiving message (" + connectorProperties.getName() + " \"Source\" on channel "
                + getChannelId() + ").", t);
        eventController.dispatchEvent(new ErrorEvent(getChannelId(), getMetaDataId(), dispatchResult.getMessageId(),
                ErrorEventType.SOURCE_CONNECTOR, getSourceName(), connectorProperties.getName(),
                "Error receiving message", t));

        if (dispatchResult != null) {
            // TODO decide if we still want to send back the exception content or something else?
            dispatchResult.setAttemptedResponse(true);
            dispatchResult.setResponseError(responseError);
            // TODO get full HTTP payload with error message
            if (dispatchResult.getSelectedResponse() != null) {
                dispatchResult.getSelectedResponse().setMessage(responseError);
            }
        }

        servletResponse.setContentType("text/plain");
        servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
        servletResponse.getOutputStream().write(responseError.getBytes());
    }

    private Object getMessage(Request request, Map<String, Object> sourceMap) throws IOException, ChannelException,
            MessagingException, DonkeyElementException, ParserConfigurationException {
        HttpRequestMessage requestMessage = createRequestMessage(request, false);

        /*
         * Now that we have the request body, we need to create the actual RawMessage message data.
         * Depending on the connector settings this could be our custom serialized XML, a Base64
         * string encoded from the raw request payload, or a string encoded from the payload with
         * the request charset.
         */
        Object rawMessageContent;

        if (connectorProperties.isXmlBody()) {
            rawMessageContent = HttpMessageConverter.httpRequestToXml(requestMessage,
                    connectorProperties.isParseMultipart(), connectorProperties.isIncludeMetadata(), this);
        } else {
            rawMessageContent = requestMessage.getContent();
        }

        eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(), getSourceName(),
                ConnectionStatusEventType.RECEIVING));

        populateSourceMap(request, requestMessage, sourceMap);

        return rawMessageContent;
    }

    private HttpRequestMessage createRequestMessage(Request request, boolean ignorePayload)
            throws IOException, MessagingException {
        HttpRequestMessage requestMessage = new HttpRequestMessage();
        requestMessage.setMethod(request.getMethod());
        requestMessage.setHeaders(HttpMessageConverter.convertFieldEnumerationToMap(request));
        requestMessage.setParameters(extractParameters(request));

        ContentType contentType;
        try {
            contentType = ContentType.parse(request.getContentType());
        } catch (RuntimeException e) {
            contentType = ContentType.TEXT_PLAIN;
        }
        requestMessage.setContentType(contentType);

        requestMessage.setRemoteAddress(StringUtils.trimToEmpty(request.getRemoteAddr()));
        requestMessage.setQueryString(StringUtils.trimToEmpty(request.getQueryString()));
        requestMessage.setRequestUrl(StringUtils.trimToEmpty(getRequestURL(request)));
        requestMessage.setContextPath(StringUtils.trimToEmpty(new URL(requestMessage.getRequestUrl()).getPath()));

        if (!ignorePayload) {
            InputStream requestInputStream = request.getInputStream();
            // If a security handler already consumed the entity, get it from the request attribute instead
            try {
                byte[] entity = (byte[]) request.getAttribute(EntityProvider.ATTRIBUTE_NAME);
                if (entity != null) {
                    requestInputStream = new ByteArrayInputStream(entity);
                }
            } catch (Exception e) {
            }

            // If the request is GZIP encoded, uncompress the content
            List<String> contentEncodingList = requestMessage.getCaseInsensitiveHeaders()
                    .get(HTTP.CONTENT_ENCODING);
            if (CollectionUtils.isNotEmpty(contentEncodingList)) {
                for (String contentEncoding : contentEncodingList) {
                    if (contentEncoding != null && (contentEncoding.equalsIgnoreCase("gzip")
                            || contentEncoding.equalsIgnoreCase("x-gzip"))) {
                        requestInputStream = new GZIPInputStream(requestInputStream);
                        break;
                    }
                }
            }

            /*
             * First parse out the body of the HTTP request. Depending on the connector settings,
             * this could end up being a string encoded with the request charset, a byte array
             * representing the raw request payload, or a MimeMultipart object.
             */

            // Only parse multipart if XML Body is selected and Parse Multipart is enabled
            if (connectorProperties.isXmlBody() && connectorProperties.isParseMultipart()
                    && ServletFileUpload.isMultipartContent(request)) {
                requestMessage.setContent(
                        new MimeMultipart(new ByteArrayDataSource(requestInputStream, contentType.toString())));
            } else if (isBinaryContentType(contentType)) {
                requestMessage.setContent(IOUtils.toByteArray(requestInputStream));
            } else {
                requestMessage.setContent(IOUtils.toString(requestInputStream,
                        HttpMessageConverter.getDefaultHttpCharset(request.getCharacterEncoding())));
            }
        }

        return requestMessage;
    }

    private void populateSourceMap(Request request, HttpRequestMessage requestMessage,
            Map<String, Object> sourceMap) {
        sourceMap.put("remoteAddress", requestMessage.getRemoteAddress());
        sourceMap.put("remotePort", request.getRemotePort());
        sourceMap.put("localAddress", StringUtils.trimToEmpty(request.getLocalAddr()));
        sourceMap.put("localPort", request.getLocalPort());
        sourceMap.put("method", requestMessage.getMethod());
        sourceMap.put("url", requestMessage.getRequestUrl());
        sourceMap.put("uri", StringUtils.trimToEmpty(request.getUri().toString()));
        sourceMap.put("protocol", StringUtils.trimToEmpty(request.getProtocol()));
        sourceMap.put("query", requestMessage.getQueryString());
        sourceMap.put("contextPath", requestMessage.getContextPath());
        sourceMap.put("headers", new MessageHeaders(requestMessage.getCaseInsensitiveHeaders()));
        sourceMap.put("parameters", new MessageParameters(requestMessage.getParameters()));

        // Add custom source map variables from the configuration interface
        sourceMap.putAll(configuration.getRequestInformation(request));
    }

    private class StaticResourceHandler extends AbstractHandler {

        private HttpStaticResource staticResource;

        public StaticResourceHandler(HttpStaticResource staticResource) {
            this.staticResource = staticResource;
        }

        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest servletRequest,
                HttpServletResponse servletResponse) throws IOException, ServletException {
            // Only allow GET requests, otherwise pass to the next request handler
            if (!baseRequest.getMethod().equalsIgnoreCase(HttpMethod.GET.asString())) {
                return;
            }

            String originalThreadName = Thread.currentThread().getName();

            try {
                Thread.currentThread().setName("HTTP Receiver Thread on " + getChannel().getName() + " ("
                        + getChannelId() + ") < " + originalThreadName);
                HttpRequestMessage requestMessage = createRequestMessage(baseRequest, true);

                String contextPath = URLDecoder.decode(requestMessage.getContextPath(), "US-ASCII");
                if (contextPath.endsWith("/")) {
                    contextPath = contextPath.substring(0, contextPath.length() - 1);
                }
                logger.debug("Received static resource request at: " + contextPath);

                Map<String, Object> sourceMap = new HashMap<String, Object>();
                populateSourceMap(baseRequest, requestMessage, sourceMap);

                String value = replacer.replaceValues(staticResource.getValue(), getChannelId(), sourceMap);
                String contentTypeString = replacer.replaceValues(staticResource.getContentType(), getChannelId(),
                        sourceMap);

                // If we're not reading from a directory and the context path doesn't match, pass to the next request handler
                if (staticResource.getResourceType() != ResourceType.DIRECTORY
                        && !staticResource.getContextPath().equalsIgnoreCase(contextPath)) {
                    return;
                }

                // If the query parameters do not match, pass to the next request handler
                if (!parametersEqual(staticResource.getQueryParameters(), requestMessage.getParameters())) {
                    return;
                }

                ContentType contentType;
                try {
                    contentType = ContentType.parse(contentTypeString);
                } catch (Exception e) {
                    contentType = ContentType.create(ContentType.TEXT_PLAIN.getMimeType(),
                            CharsetUtils.getEncoding(connectorProperties.getCharset()));
                }

                Charset charset = contentType.getCharset();
                if (charset == null) {
                    charset = Charset.forName(CharsetUtils.getEncoding(connectorProperties.getCharset()));
                }

                servletResponse.setContentType(contentType.toString());
                servletResponse.setStatus(HttpStatus.SC_OK);

                OutputStream responseOutputStream = servletResponse.getOutputStream();

                // If the client accepts GZIP compression, compress the content
                List<String> acceptEncodingList = requestMessage.getCaseInsensitiveHeaders().get("Accept-Encoding");
                if (CollectionUtils.isNotEmpty(acceptEncodingList)) {
                    for (String acceptEncoding : acceptEncodingList) {
                        if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
                            servletResponse.setHeader(HTTP.CONTENT_ENCODING, "gzip");
                            responseOutputStream = new GZIPOutputStream(responseOutputStream);
                            break;
                        }
                    }
                }

                if (staticResource.getResourceType() == ResourceType.FILE) {
                    // Just stream the file itself back to the client
                    IOUtils.copy(new FileInputStream(value), responseOutputStream);
                } else if (staticResource.getResourceType() == ResourceType.DIRECTORY) {
                    File file = new File(value);

                    if (file.isDirectory()) {
                        // Use the trailing path as the child path for the actual resource directory
                        String childPath = StringUtils.removeStartIgnoreCase(contextPath,
                                staticResource.getContextPath());
                        if (childPath.startsWith("/")) {
                            childPath = childPath.substring(1);
                        }

                        if (!childPath.contains("/")) {
                            file = new File(file, childPath);
                        } else {
                            // If a subdirectory is specified, pass to the next request handler
                            servletResponse.reset();
                            return;
                        }
                    } else {
                        throw new Exception(
                                "File \"" + file.toString() + "\" does not exist or is not a directory.");
                    }

                    if (file.exists()) {
                        if (file.isDirectory()) {
                            // The directory itself was requested, instead of a specific file
                            servletResponse.reset();
                            return;
                        }

                        // A valid file was found; stream it back to the client
                        IOUtils.copy(new FileInputStream(file), responseOutputStream);
                    } else {
                        // File does not exist, pass to the next request handler
                        servletResponse.reset();
                        return;
                    }
                } else {
                    // Stream the value string back to the client
                    IOUtils.write(value, responseOutputStream, charset);
                }

                // If we gzipped, we need to finish the stream now
                if (responseOutputStream instanceof GZIPOutputStream) {
                    ((GZIPOutputStream) responseOutputStream).finish();
                }
            } catch (Throwable t) {
                logger.error("Error handling static HTTP resource request (" + connectorProperties.getName()
                        + " \"Source\" on channel " + getChannelId() + ").", t);
                eventController.dispatchEvent(new ErrorEvent(getChannelId(), getMetaDataId(), null,
                        ErrorEventType.SOURCE_CONNECTOR, getSourceName(), connectorProperties.getName(),
                        "Error handling static HTTP resource request", t));

                servletResponse.reset();
                servletResponse.setContentType(ContentType.TEXT_PLAIN.toString());
                servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
                servletResponse.getOutputStream().write(ExceptionUtils.getStackTrace(t).getBytes());
            } finally {
                Thread.currentThread().setName(originalThreadName);
            }

            baseRequest.setHandled(true);
        }
    }

    private String replaceValues(String template, DispatchResult dispatchResult) {
        ConnectorMessage mergedConnectorMessage = null;

        if (dispatchResult != null && dispatchResult.getProcessedMessage() != null) {
            mergedConnectorMessage = dispatchResult.getProcessedMessage().getMergedConnectorMessage();
        }

        return (mergedConnectorMessage == null
                ? replacer.replaceValues(template, getChannelId(), getChannel().getName())
                : replacer.replaceValues(template, mergedConnectorMessage));
    }

    @Override
    public void handleRecoveredResponse(DispatchResult dispatchResult) {
        finishDispatch(dispatchResult);
    }

    public Server getServer() {
        return server;
    }

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }

    public int getTimeout() {
        return timeout;
    }

    private Map<String, List<String>> extractParameters(Request request) {
        /*
         * XXX: extractParameters must be called before the parameters are accessed, otherwise the
         * map will be null.
         */
        request.extractParameters();
        Map<String, List<String>> parameterMap = new HashMap<String, List<String>>();

        for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            List<String> list = parameterMap.get(entry.getKey());

            if (list == null) {
                list = new ArrayList<String>();
                parameterMap.put(entry.getKey(), list);
            }

            list.addAll(Arrays.asList(entry.getValue()));
        }

        return parameterMap;
    }

    private boolean parametersEqual(Map<String, List<String>> params1, Map<String, List<String>> params2) {
        if (!params1.keySet().equals(params2.keySet())) {
            return false;
        }

        for (Entry<String, List<String>> entry : params1.entrySet()) {
            if (!ListUtils.isEqualList(entry.getValue(), params2.get(entry.getKey()))) {
                return false;
            }
        }

        return true;
    }

    private String getRequestURL(Request request) {
        String requestURL = request.getRequestURL().toString();

        try {
            // Verify whether the URL is valid
            new URL(requestURL);
        } catch (MalformedURLException e) {
            // The request URL returned by Jetty is invalid, so build it up without the URI instead
            StringBuilder builder = new StringBuilder();
            String scheme = request.getScheme();
            int port = request.getServerPort();

            builder.append(scheme);
            builder.append("://");
            builder.append(request.getServerName());

            // Don't include port 80 if HTTP, or port 443 if HTTPS
            if ((scheme.equalsIgnoreCase(URIUtil.HTTP) && port != 80)
                    || (scheme.equalsIgnoreCase(URIUtil.HTTPS) && port != 443)) {
                builder.append(':');
                builder.append(port);
            }

            requestURL = builder.toString();
        }

        return requestURL;
    }

    private ConstraintSecurityHandler createSecurityHandler(Handler handler) throws Exception {
        final Authenticator authenticator = authenticatorProvider.getAuthenticator();

        final String authMethod;
        switch (authProps.getAuthType()) {
        case BASIC:
            authMethod = Constraint.__BASIC_AUTH;
            break;
        case DIGEST:
            authMethod = Constraint.__DIGEST_AUTH;
            break;
        default:
            authMethod = "customauth";
        }

        Constraint constraint = new Constraint();
        constraint.setName(authMethod);
        constraint.setRoles(new String[] { "user" });
        constraint.setAuthenticate(true);

        ConstraintMapping constraintMapping = new ConstraintMapping();
        constraintMapping.setConstraint(constraint);
        constraintMapping.setPathSpec("/*");

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        securityHandler.setAuthenticator(new org.eclipse.jetty.security.Authenticator() {
            @Override
            public void setConfiguration(AuthConfiguration configuration) {
            }

            @Override
            public String getAuthMethod() {
                return authMethod;
            }

            @Override
            public void prepareRequest(ServletRequest request) {
            }

            @Override
            public Authentication validateRequest(final ServletRequest req, ServletResponse res, boolean mandatory)
                    throws ServerAuthException {
                HttpServletRequest request = (HttpServletRequest) req;
                HttpServletResponse response = (HttpServletResponse) res;

                String remoteAddress = StringUtils.trimToEmpty(request.getRemoteAddr());
                int remotePort = request.getRemotePort();
                String localAddress = StringUtils.trimToEmpty(request.getLocalAddr());
                int localPort = request.getLocalPort();
                String protocol = StringUtils.trimToEmpty(request.getProtocol());
                String method = StringUtils.trimToEmpty(request.getMethod());
                String requestURI = StringUtils.trimToEmpty(request.getRequestURI());
                Map<String, List<String>> headers = HttpMessageConverter.convertFieldEnumerationToMap(request);

                Map<String, List<String>> queryParameters = new LinkedHashMap<String, List<String>>();
                for (Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
                    queryParameters.put(entry.getKey(), Arrays.asList(entry.getValue()));
                }

                EntityProvider entityProvider = new EntityProvider() {
                    @Override
                    public byte[] getEntity() throws IOException {
                        byte[] entity = (byte[]) req.getAttribute(ATTRIBUTE_NAME);
                        if (entity == null) {
                            entity = IOUtils.toByteArray(req.getInputStream());
                            req.setAttribute(ATTRIBUTE_NAME, entity);
                        }
                        return entity;
                    }
                };

                RequestInfo requestInfo = new RequestInfo(remoteAddress, remotePort, localAddress, localPort,
                        protocol, method, requestURI, headers, queryParameters, entityProvider,
                        configuration.getRequestInformation(request));

                try {
                    AuthenticationResult result = authenticator.authenticate(requestInfo);

                    for (Entry<String, List<String>> entry : result.getResponseHeaders().entrySet()) {
                        if (StringUtils.isNotBlank(entry.getKey()) && entry.getValue() != null) {
                            for (int i = 0; i < entry.getValue().size(); i++) {
                                if (i == 0) {
                                    response.setHeader(entry.getKey(), entry.getValue().get(i));
                                } else {
                                    response.addHeader(entry.getKey(), entry.getValue().get(i));
                                }
                            }
                        }
                    }

                    switch (result.getStatus()) {
                    case CHALLENGED:
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                        return org.eclipse.jetty.server.Authentication.SEND_CONTINUE;
                    case SUCCESS:
                        Principal userPrincipal = new KnownUser(StringUtils.trimToEmpty(result.getUsername()),
                                null);
                        Subject subject = new Subject();
                        subject.getPrincipals().add(userPrincipal);
                        return new UserAuthentication(getAuthMethod(),
                                new DefaultUserIdentity(subject, userPrincipal, new String[] { "user" }));
                    case FAILURE:
                    default:
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                        return org.eclipse.jetty.server.Authentication.SEND_FAILURE;
                    }
                } catch (Throwable t) {
                    logger.error("Error in HTTP authentication for " + connectorProperties.getName() + " ("
                            + connectorProperties.getName() + " \"Source\" on channel " + getChannelId() + ").", t);
                    eventController.dispatchEvent(new ErrorEvent(getChannelId(), getMetaDataId(), null,
                            ErrorEventType.DESTINATION_CONNECTOR, "Source", connectorProperties.getName(),
                            "Error in HTTP authentication for " + connectorProperties.getName(), t));
                    throw new ServerAuthException(t);
                }
            }

            @Override
            public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory,
                    User validatedUser) throws ServerAuthException {
                return true;
            }
        });
        securityHandler.addConstraintMapping(constraintMapping);

        securityHandler.setHandler(handler);
        return securityHandler;
    }

    @Override
    public boolean isBinaryContentType(ContentType contentType) {
        String mimeType = contentType.getMimeType();

        if (connectorProperties.isBinaryMimeTypesRegex()) {
            return binaryMimeTypesRegex.matcher(mimeType).matches();
        } else {
            return StringUtils.startsWithAny(mimeType, binaryMimeTypesArray);
        }
    }
}