org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package org.apache.jmeter.protocol.http.sampler;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.security.auth.Subject;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpConnection;
import org.apache.http.HttpConnectionMetrics;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.protocol.ResponseContentEncoding;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.FormBodyPartBuilder;
import org.apache.http.entity.mime.MIME;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultClientConnectionReuseStrategy;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.DefaultedHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.jmeter.protocol.http.control.AuthManager;
import org.apache.jmeter.protocol.http.control.CacheManager;
import org.apache.jmeter.protocol.http.control.CookieManager;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.protocol.http.util.EncoderCache;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.apache.jmeter.protocol.http.util.HTTPFileArg;
import org.apache.jmeter.protocol.http.util.SlowHC4SocketFactory;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.JsseSSLManager;
import org.apache.jmeter.util.SSLManager;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 * HTTP Sampler using Apache HttpClient 4.x.
 *
 */
public class HTTPHC4Impl extends HTTPHCAbstractImpl {

    private static final Logger log = LoggingManager.getLoggerForClass();

    /** retry count to be used (default 0); 0 = disable retries */
    private static final int RETRY_COUNT = JMeterUtils.getPropDefault("httpclient4.retrycount", 0);

    /** Idle timeout to be applied to connections if no Keep-Alive header is sent by the server (default 0 = disable) */
    private static final int IDLE_TIMEOUT = JMeterUtils.getPropDefault("httpclient4.idletimeout", 0);

    private static final int VALIDITY_AFTER_INACTIVITY_TIMEOUT = JMeterUtils
            .getPropDefault("httpclient4.validate_after_inactivity", 2000);

    private static final int TIME_TO_LIVE = JMeterUtils.getPropDefault("httpclient4.time_to_live", 2000);

    private static final String CONTEXT_METRICS = "jmeter_metrics"; // TODO hack for metrics related to HTTPCLIENT-1081, to be removed later

    private static final ConnectionKeepAliveStrategy IDLE_STRATEGY = new DefaultConnectionKeepAliveStrategy() {
        @Override
        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            long duration = super.getKeepAliveDuration(response, context);
            if (duration <= 0 && IDLE_TIMEOUT > 0) {// none found by the superclass
                if (log.isDebugEnabled()) {
                    log.debug("Setting keepalive to " + IDLE_TIMEOUT);
                }
                return IDLE_TIMEOUT;
            }
            return duration; // return the super-class value
        }

    };

    /**
     * Special interceptor made to keep metrics when connection is released for some method like HEAD
     * Otherwise calling directly ((HttpConnection) localContext.getAttribute(HttpCoreContext.HTTP_CONNECTION)).getMetrics();
     * would throw org.apache.http.impl.conn.ConnectionShutdownException
     * See <a href="https://bz.apache.org/jira/browse/HTTPCLIENT-1081">HTTPCLIENT-1081</a>
     */
    private static final HttpResponseInterceptor METRICS_SAVER = new HttpResponseInterceptor() {
        @Override
        public void process(HttpResponse response, HttpContext context) throws HttpException, IOException {
            HttpConnectionMetrics metrics = ((HttpConnection) context.getAttribute(HttpCoreContext.HTTP_CONNECTION))
                    .getMetrics();
            context.setAttribute(CONTEXT_METRICS, metrics);
        }
    };
    private static final HttpRequestInterceptor METRICS_RESETTER = new HttpRequestInterceptor() {
        @Override
        public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
            HttpConnectionMetrics metrics = ((HttpConnection) context.getAttribute(HttpCoreContext.HTTP_CONNECTION))
                    .getMetrics();
            metrics.reset();
        }
    };

    /**
     * Headers to save
     */
    private static final String[] HEADERS_TO_SAVE = new String[] { "content-length", "content-encoding",
            "content-md5" };

    /**
     * Custom implementation that backups headers related to Compressed responses 
     * that HC core {@link ResponseContentEncoding} removes after uncompressing
     * See Bug 59401
     */
    private static final HttpResponseInterceptor RESPONSE_CONTENT_ENCODING = new ResponseContentEncoding() {
        @Override
        public void process(HttpResponse response, HttpContext context) throws HttpException, IOException {
            ArrayList<Header[]> headersToSave = null;

            final HttpEntity entity = response.getEntity();
            final HttpClientContext clientContext = HttpClientContext.adapt(context);
            final RequestConfig requestConfig = clientContext.getRequestConfig();
            // store the headers if necessary
            if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
                final Header ceheader = entity.getContentEncoding();
                if (ceheader != null) {
                    headersToSave = new ArrayList<>(3);
                    for (String name : HEADERS_TO_SAVE) {
                        Header[] hdr = response.getHeaders(name); // empty if none
                        headersToSave.add(hdr);
                    }
                }
            }

            // Now invoke original parent code
            super.process(response, clientContext);
            // Should this be in a finally ? 
            if (headersToSave != null) {
                for (Header[] headers : headersToSave) {
                    for (Header headerToRestore : headers) {
                        if (response.containsHeader(headerToRestore.getName())) {
                            break;
                        }
                        response.addHeader(headerToRestore);
                    }
                }
            }
        }
    };

    /**
     * 1 HttpClient instance per combination of (HttpClient,HttpClientKey)
     */
    private static final ThreadLocal<Map<HttpClientKey, HttpClient>> HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY = new InheritableThreadLocal<Map<HttpClientKey, HttpClient>>() {
        @Override
        protected Map<HttpClientKey, HttpClient> initialValue() {
            return new HashMap<>();
        }
    };

    // Scheme used for slow HTTP sockets. Cannot be set as a default, because must be set on an HttpClient instance.
    private static final Scheme SLOW_HTTP;

    /*
     * Create a set of default parameters from the ones initially created.
     * This allows the defaults to be overridden if necessary from the properties file.
     */
    private static final HttpParams DEFAULT_HTTP_PARAMS;

    private static final String USER_TOKEN = "__jmeter.USER_TOKEN__"; //$NON-NLS-1$

    static final String SAMPLER_RESULT_TOKEN = "__jmeter.SAMPLER_RESULT__"; //$NON-NLS-1$

    private static final String HTTPCLIENT_TOKEN = "__jmeter.HTTPCLIENT_TOKEN__";

    static {
        log.info("HTTP request retry count = " + RETRY_COUNT);

        DEFAULT_HTTP_PARAMS = new SyncBasicHttpParams(); // Could we drop the Sync here?
        DEFAULT_HTTP_PARAMS.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false);
        DEFAULT_HTTP_PARAMS.setIntParameter(ClientPNames.MAX_REDIRECTS, HTTPSamplerBase.MAX_REDIRECTS);
        DefaultHttpClient.setDefaultHttpParams(DEFAULT_HTTP_PARAMS);

        // Process Apache HttpClient parameters file
        String file = JMeterUtils.getProperty("hc.parameters.file"); // $NON-NLS-1$
        if (file != null) {
            HttpClientDefaultParameters.load(file, DEFAULT_HTTP_PARAMS);
        }

        // Set up HTTP scheme override if necessary
        if (CPS_HTTP > 0) {
            log.info("Setting up HTTP SlowProtocol, cps=" + CPS_HTTP);
            SLOW_HTTP = new Scheme(HTTPConstants.PROTOCOL_HTTP, HTTPConstants.DEFAULT_HTTP_PORT,
                    new SlowHC4SocketFactory(CPS_HTTP));
        } else {
            SLOW_HTTP = null;
        }

        if (localAddress != null) {
            DEFAULT_HTTP_PARAMS.setParameter(ConnRoutePNames.LOCAL_ADDRESS, localAddress);
        }

    }

    private volatile HttpUriRequest currentRequest; // Accessed from multiple threads

    private volatile boolean resetSSLContext;

    protected HTTPHC4Impl(HTTPSamplerBase testElement) {
        super(testElement);
    }

    public static final class HttpDelete extends HttpEntityEnclosingRequestBase {

        public HttpDelete(final URI uri) {
            super();
            setURI(uri);
        }

        @Override
        public String getMethod() {
            return HTTPConstants.DELETE;
        }
    }

    @Override
    protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRedirect, int frameDepth) {

        if (log.isDebugEnabled()) {
            log.debug("Start : sample " + url.toString());
            log.debug("method " + method + " followingRedirect " + areFollowingRedirect + " depth " + frameDepth);
        }

        HTTPSampleResult res = createSampleResult(url, method);

        HttpClient httpClient = setupClient(url, res);

        HttpRequestBase httpRequest = null;
        try {
            URI uri = url.toURI();
            if (method.equals(HTTPConstants.POST)) {
                httpRequest = new HttpPost(uri);
            } else if (method.equals(HTTPConstants.GET)) {
                httpRequest = new HttpGet(uri);
            } else if (method.equals(HTTPConstants.PUT)) {
                httpRequest = new HttpPut(uri);
            } else if (method.equals(HTTPConstants.HEAD)) {
                httpRequest = new HttpHead(uri);
            } else if (method.equals(HTTPConstants.TRACE)) {
                httpRequest = new HttpTrace(uri);
            } else if (method.equals(HTTPConstants.OPTIONS)) {
                httpRequest = new HttpOptions(uri);
            } else if (method.equals(HTTPConstants.DELETE)) {
                httpRequest = new HttpDelete(uri);
            } else if (method.equals(HTTPConstants.PATCH)) {
                httpRequest = new HttpPatch(uri);
            } else if (HttpWebdav.isWebdavMethod(method)) {
                httpRequest = new HttpWebdav(method, uri);
            } else {
                throw new IllegalArgumentException("Unexpected method: '" + method + "'");
            }
            setupRequest(url, httpRequest, res); // can throw IOException
        } catch (Exception e) {
            res.sampleStart();
            res.sampleEnd();
            errorResult(e, res);
            return res;
        }

        HttpContext localContext = new BasicHttpContext();
        setupClientContextBeforeSample(localContext);

        res.sampleStart();

        final CacheManager cacheManager = getCacheManager();
        if (cacheManager != null && HTTPConstants.GET.equalsIgnoreCase(method)) {
            if (cacheManager.inCache(url)) {
                return updateSampleResultForResourceInCache(res);
            }
        }

        try {
            currentRequest = httpRequest;
            handleMethod(method, res, httpRequest, localContext);
            // store the SampleResult in LocalContext to compute connect time
            localContext.setAttribute(SAMPLER_RESULT_TOKEN, res);
            // perform the sample
            HttpResponse httpResponse = executeRequest(httpClient, httpRequest, localContext, url);

            // Needs to be done after execute to pick up all the headers
            final HttpRequest request = (HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
            extractClientContextAfterSample(localContext);
            // We've finished with the request, so we can add the LocalAddress to it for display
            final InetAddress localAddr = (InetAddress) httpRequest.getParams()
                    .getParameter(ConnRoutePNames.LOCAL_ADDRESS);
            if (localAddr != null) {
                request.addHeader(HEADER_LOCAL_ADDRESS, localAddr.toString());
            }
            res.setRequestHeaders(getConnectionHeaders(request));

            Header contentType = httpResponse.getLastHeader(HTTPConstants.HEADER_CONTENT_TYPE);
            if (contentType != null) {
                String ct = contentType.getValue();
                res.setContentType(ct);
                res.setEncodingAndType(ct);
            }
            HttpEntity entity = httpResponse.getEntity();
            if (entity != null) {
                res.setResponseData(readResponse(res, entity.getContent(), (int) entity.getContentLength()));
            }

            res.sampleEnd(); // Done with the sampling proper.
            currentRequest = null;

            // Now collect the results into the HTTPSampleResult:
            StatusLine statusLine = httpResponse.getStatusLine();
            int statusCode = statusLine.getStatusCode();
            res.setResponseCode(Integer.toString(statusCode));
            res.setResponseMessage(statusLine.getReasonPhrase());
            res.setSuccessful(isSuccessCode(statusCode));

            res.setResponseHeaders(getResponseHeaders(httpResponse, localContext));
            if (res.isRedirect()) {
                final Header headerLocation = httpResponse.getLastHeader(HTTPConstants.HEADER_LOCATION);
                if (headerLocation == null) { // HTTP protocol violation, but avoids NPE
                    throw new IllegalArgumentException(
                            "Missing location header in redirect for " + httpRequest.getRequestLine());
                }
                String redirectLocation = headerLocation.getValue();
                res.setRedirectLocation(redirectLocation);
            }

            // record some sizes to allow HTTPSampleResult.getBytes() with different options
            HttpConnectionMetrics metrics = (HttpConnectionMetrics) localContext.getAttribute(CONTEXT_METRICS);
            long headerBytes = res.getResponseHeaders().length() // condensed length (without \r)
                    + httpResponse.getAllHeaders().length // Add \r for each header
                    + 1 // Add \r for initial header
                    + 2; // final \r\n before data
            long totalBytes = metrics.getReceivedBytesCount();
            res.setHeadersSize((int) headerBytes);
            res.setBodySize((int) (totalBytes - headerBytes));
            if (log.isDebugEnabled()) {
                log.debug("ResponseHeadersSize=" + res.getHeadersSize() + " Content-Length=" + res.getBodySize()
                        + " Total=" + (res.getHeadersSize() + res.getBodySize()));
            }

            // If we redirected automatically, the URL may have changed
            if (getAutoRedirects()) {
                HttpUriRequest req = (HttpUriRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
                HttpHost target = (HttpHost) localContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
                URI redirectURI = req.getURI();
                if (redirectURI.isAbsolute()) {
                    res.setURL(redirectURI.toURL());
                } else {
                    res.setURL(new URL(new URL(target.toURI()), redirectURI.toString()));
                }
            }

            // Store any cookies received in the cookie manager:
            saveConnectionCookies(httpResponse, res.getURL(), getCookieManager());

            // Save cache information
            if (cacheManager != null) {
                cacheManager.saveDetails(httpResponse, res);
            }

            // Follow redirects and download page resources if appropriate:
            res = resultProcessing(areFollowingRedirect, frameDepth, res);

        } catch (IOException e) {
            log.debug("IOException", e);
            if (res.getEndTime() == 0) {
                res.sampleEnd();
            }
            // pick up headers if failed to execute the request
            if (res.getRequestHeaders() != null) {
                log.debug("Overwriting request old headers: " + res.getRequestHeaders());
            }
            res.setRequestHeaders(
                    getConnectionHeaders((HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST)));
            errorResult(e, res);
            return res;
        } catch (RuntimeException e) {
            log.debug("RuntimeException", e);
            if (res.getEndTime() == 0) {
                res.sampleEnd();
            }
            errorResult(e, res);
            return res;
        } finally {
            currentRequest = null;
            JMeterContextService.getContext().getSamplerContext().remove(HTTPCLIENT_TOKEN);
        }
        return res;
    }

    /**
     * Store in JMeter Variables the UserToken so that the SSL context is reused
     * See <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=57804">Bug 57804</a>
     * @param localContext {@link HttpContext}
     */
    private void extractClientContextAfterSample(HttpContext localContext) {
        Object userToken = localContext.getAttribute(HttpClientContext.USER_TOKEN);
        if (userToken != null) {
            if (log.isDebugEnabled()) {
                log.debug("Extracted from HttpContext user token:" + userToken + ", storing it as JMeter variable:"
                        + USER_TOKEN);
            }
            // During recording JMeterContextService.getContext().getVariables() is null
            JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
            if (jMeterVariables != null) {
                jMeterVariables.putObject(USER_TOKEN, userToken);
            }
        }
    }

    /**
     * Configure the UserToken so that the SSL context is reused
     * See <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=57804">Bug 57804</a>
     * @param localContext {@link HttpContext}
     */
    private void setupClientContextBeforeSample(HttpContext localContext) {
        Object userToken = null;
        // During recording JMeterContextService.getContext().getVariables() is null
        JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
        if (jMeterVariables != null) {
            userToken = jMeterVariables.getObject(USER_TOKEN);
        }
        if (userToken != null) {
            if (log.isDebugEnabled()) {
                log.debug("Found user token:" + userToken + " as JMeter variable:" + USER_TOKEN
                        + ", storing it in HttpContext");
            }
            localContext.setAttribute(HttpClientContext.USER_TOKEN, userToken);
        } else {
            // It would be better to create a ClientSessionManager that would compute this value
            // for now it can be Thread.currentThread().getName() but must be changed when we would change 
            // the Thread per User model
            String userId = Thread.currentThread().getName();
            if (log.isDebugEnabled()) {
                log.debug("Storing in HttpContext the user token:" + userId);
            }
            localContext.setAttribute(HttpClientContext.USER_TOKEN, userId);
        }
    }

    /**
     * Calls {@link #sendPostData(HttpPost)} if method is <code>POST</code> and
     * {@link #sendEntityData(HttpEntityEnclosingRequestBase)} if method is
     * <code>PUT</code> or <code>PATCH</code>
     * <p>
     * Field HTTPSampleResult#queryString of result is modified in the 2 cases
     * 
     * @param method
     *            String HTTP method
     * @param result
     *            {@link HTTPSampleResult}
     * @param httpRequest
     *            {@link HttpRequestBase}
     * @param localContext
     *            {@link HttpContext}
     * @throws IOException
     *             when posting data fails due to I/O
     */
    protected void handleMethod(String method, HTTPSampleResult result, HttpRequestBase httpRequest,
            HttpContext localContext) throws IOException {
        // Handle the various methods
        if (httpRequest instanceof HttpPost) {
            String postBody = sendPostData((HttpPost) httpRequest);
            result.setQueryString(postBody);
        } else if (httpRequest instanceof HttpEntityEnclosingRequestBase) {
            String entityBody = sendEntityData((HttpEntityEnclosingRequestBase) httpRequest);
            result.setQueryString(entityBody);
        }
    }

    /**
     * Create HTTPSampleResult filling url, method and SampleLabel.
     * Monitor field is computed calling isMonitor()
     * @param url URL
     * @param method HTTP Method
     * @return {@link HTTPSampleResult}
     */
    protected HTTPSampleResult createSampleResult(URL url, String method) {
        HTTPSampleResult res = new HTTPSampleResult();
        res.setMonitor(isMonitor());

        res.setSampleLabel(url.toString()); // May be replaced later
        res.setHTTPMethod(method);
        res.setURL(url);

        return res;
    }

    /**
     * Execute request either as is or under PrivilegedAction 
     * if a Subject is available for url
     * @param httpClient the {@link HttpClient} to be used to execute the httpRequest
     * @param httpRequest the {@link HttpRequest} to be executed
     * @param localContext th {@link HttpContext} to be used for execution
     * @param url the target url (will be used to look up a possible subject for the execution)
     * @return the result of the execution of the httpRequest
     * @throws IOException
     * @throws ClientProtocolException
     */
    private HttpResponse executeRequest(final HttpClient httpClient, final HttpRequestBase httpRequest,
            final HttpContext localContext, final URL url) throws IOException, ClientProtocolException {
        AuthManager authManager = getAuthManager();
        if (authManager != null) {
            Subject subject = authManager.getSubjectForUrl(url);
            if (subject != null) {
                try {
                    return Subject.doAs(subject, new PrivilegedExceptionAction<HttpResponse>() {

                        @Override
                        public HttpResponse run() throws Exception {
                            return httpClient.execute(httpRequest, localContext);
                        }
                    });
                } catch (PrivilegedActionException e) {
                    log.error("Can't execute httpRequest with subject:" + subject, e);
                    throw new RuntimeException("Can't execute httpRequest with subject:" + subject, e);
                }
            }
        }
        return httpClient.execute(httpRequest, localContext);
    }

    /**
     * Holder class for all fields that define an HttpClient instance;
     * used as the key to the ThreadLocal map of HttpClient instances.
     */
    private static final class HttpClientKey {

        private final String target; // protocol://[user:pass@]host:[port]
        private final boolean hasProxy;
        private final String proxyHost;
        private final int proxyPort;
        private final String proxyUser;
        private final String proxyPass;

        private final int hashCode; // Always create hash because we will always need it

        /**
         * @param url URL Only protocol and url authority are used (protocol://[user:pass@]host:[port])
         * @param hasProxy has proxy
         * @param proxyHost proxy host
         * @param proxyPort proxy port
         * @param proxyUser proxy user
         * @param proxyPass proxy password
         */
        public HttpClientKey(URL url, boolean hasProxy, String proxyHost, int proxyPort, String proxyUser,
                String proxyPass) {
            // N.B. need to separate protocol from authority otherwise http://server would match https://erver (<= sic, not typo error)
            // could use separate fields, but simpler to combine them
            this.target = url.getProtocol() + "://" + url.getAuthority();
            this.hasProxy = hasProxy;
            this.proxyHost = proxyHost;
            this.proxyPort = proxyPort;
            this.proxyUser = proxyUser;
            this.proxyPass = proxyPass;
            this.hashCode = getHash();
        }

        private int getHash() {
            int hash = 17;
            hash = hash * 31 + (hasProxy ? 1 : 0);
            if (hasProxy) {
                hash = hash * 31 + getHash(proxyHost);
                hash = hash * 31 + proxyPort;
                hash = hash * 31 + getHash(proxyUser);
                hash = hash * 31 + getHash(proxyPass);
            }
            hash = hash * 31 + target.hashCode();
            return hash;
        }

        // Allow for null strings
        private int getHash(String s) {
            return s == null ? 0 : s.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof HttpClientKey)) {
                return false;
            }
            HttpClientKey other = (HttpClientKey) obj;
            if (this.hasProxy) { // otherwise proxy String fields may be null
                return this.hasProxy == other.hasProxy && this.proxyPort == other.proxyPort
                        && this.proxyHost.equals(other.proxyHost) && this.proxyUser.equals(other.proxyUser)
                        && this.proxyPass.equals(other.proxyPass) && this.target.equals(other.target);
            }
            // No proxy, so don't check proxy fields
            return this.hasProxy == other.hasProxy && this.target.equals(other.target);
        }

        @Override
        public int hashCode() {
            return hashCode;
        }

        // For debugging
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(target);
            if (hasProxy) {
                sb.append(" via ");
                sb.append(proxyUser);
                sb.append("@");
                sb.append(proxyHost);
                sb.append(":");
                sb.append(proxyPort);
            }
            return sb.toString();
        }
    }

    private HttpClient setupClient(URL url, SampleResult res) {

        Map<HttpClientKey, HttpClient> mapHttpClientPerHttpClientKey = HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY
                .get();

        final String host = url.getHost();
        String proxyHost = getProxyHost();
        int proxyPort = getProxyPortInt();
        String proxyPass = getProxyPass();
        String proxyUser = getProxyUser();

        // static proxy is the globally define proxy eg command line or properties
        boolean useStaticProxy = isStaticProxy(host);
        // dynamic proxy is the proxy defined for this sampler
        boolean useDynamicProxy = isDynamicProxy(proxyHost, proxyPort);
        boolean useProxy = useStaticProxy || useDynamicProxy;

        // if both dynamic and static are used, the dynamic proxy has priority over static
        if (!useDynamicProxy) {
            proxyHost = PROXY_HOST;
            proxyPort = PROXY_PORT;
            proxyUser = PROXY_USER;
            proxyPass = PROXY_PASS;
        }

        // Lookup key - must agree with all the values used to create the HttpClient.
        HttpClientKey key = new HttpClientKey(url, useProxy, proxyHost, proxyPort, proxyUser, proxyPass);

        HttpClient httpClient = null;
        if (this.testElement.isConcurrentDwn()) {
            httpClient = (HttpClient) JMeterContextService.getContext().getSamplerContext().get(HTTPCLIENT_TOKEN);
        }

        if (httpClient == null) {
            httpClient = mapHttpClientPerHttpClientKey.get(key);
        }

        if (httpClient != null && resetSSLContext
                && HTTPConstants.PROTOCOL_HTTPS.equalsIgnoreCase(url.getProtocol())) {
            ((AbstractHttpClient) httpClient).clearRequestInterceptors();
            ((AbstractHttpClient) httpClient).clearResponseInterceptors();
            httpClient.getConnectionManager().closeIdleConnections(1L, TimeUnit.MICROSECONDS);
            httpClient = null;
            JsseSSLManager sslMgr = (JsseSSLManager) SSLManager.getInstance();
            sslMgr.resetContext();
            resetSSLContext = false;
        }

        if (httpClient == null) { // One-time init for this client

            HttpParams clientParams = new DefaultedHttpParams(new BasicHttpParams(), DEFAULT_HTTP_PARAMS);

            DnsResolver resolver = this.testElement.getDNSResolver();
            if (resolver == null) {
                resolver = SystemDefaultDnsResolver.INSTANCE;
            }
            MeasuringConnectionManager connManager = new MeasuringConnectionManager(createSchemeRegistry(),
                    resolver, TIME_TO_LIVE, VALIDITY_AFTER_INACTIVITY_TIMEOUT);

            // Modern browsers use more connections per host than the current httpclient default (2)
            // when using parallel download the httpclient and connection manager are shared by the downloads threads
            // to be realistic JMeter must set an higher value to DefaultMaxPerRoute
            if (this.testElement.isConcurrentDwn()) {
                try {
                    int maxConcurrentDownloads = Integer.parseInt(this.testElement.getConcurrentPool());
                    connManager.setDefaultMaxPerRoute(
                            Math.max(maxConcurrentDownloads, connManager.getDefaultMaxPerRoute()));
                } catch (NumberFormatException nfe) {
                    // no need to log -> will be done by the sampler
                }
            }

            httpClient = new DefaultHttpClient(connManager, clientParams) {
                @Override
                protected HttpRequestRetryHandler createHttpRequestRetryHandler() {
                    return new DefaultHttpRequestRetryHandler(RETRY_COUNT, false); // set retry count
                }
            };

            if (IDLE_TIMEOUT > 0) {
                ((AbstractHttpClient) httpClient).setKeepAliveStrategy(IDLE_STRATEGY);
            }
            // see https://issues.apache.org/jira/browse/HTTPCORE-397
            ((AbstractHttpClient) httpClient).setReuseStrategy(DefaultClientConnectionReuseStrategy.INSTANCE);
            ((AbstractHttpClient) httpClient).addResponseInterceptor(RESPONSE_CONTENT_ENCODING);
            ((AbstractHttpClient) httpClient).addResponseInterceptor(METRICS_SAVER); // HACK
            ((AbstractHttpClient) httpClient).addRequestInterceptor(METRICS_RESETTER);

            // Override the default schemes as necessary
            SchemeRegistry schemeRegistry = httpClient.getConnectionManager().getSchemeRegistry();

            if (SLOW_HTTP != null) {
                schemeRegistry.register(SLOW_HTTP);
            }

            // Set up proxy details
            if (useProxy) {

                HttpHost proxy = new HttpHost(proxyHost, proxyPort);
                clientParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

                if (proxyUser.length() > 0) {
                    ((AbstractHttpClient) httpClient).getCredentialsProvider().setCredentials(
                            new AuthScope(proxyHost, proxyPort),
                            new NTCredentials(proxyUser, proxyPass, localHost, PROXY_DOMAIN));
                }
            }

            // Bug 52126 - we do our own cookie handling
            clientParams.setParameter(ClientPNames.COOKIE_POLICY, CookieSpecs.IGNORE_COOKIES);

            if (log.isDebugEnabled()) {
                log.debug("Created new HttpClient: @" + System.identityHashCode(httpClient) + " " + key.toString());
            }

            mapHttpClientPerHttpClientKey.put(key, httpClient); // save the agent for next time round
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Reusing the HttpClient: @" + System.identityHashCode(httpClient) + " " + key.toString());
            }
        }

        if (this.testElement.isConcurrentDwn()) {
            JMeterContextService.getContext().getSamplerContext().put(HTTPCLIENT_TOKEN, httpClient);
        }

        // TODO - should this be done when the client is created?
        // If so, then the details need to be added as part of HttpClientKey
        setConnectionAuthorization(httpClient, url, getAuthManager(), key);

        return httpClient;
    }

    /**
     * Setup LazySchemeSocketFactory
     * @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=58099"
     */
    private static SchemeRegistry createSchemeRegistry() {
        final SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); //$NON-NLS-1$
        registry.register(new Scheme("https", 443, new LazySchemeSocketFactory())); //$NON-NLS-1$
        return registry;
    }

    /**
     * Setup following elements on httpRequest:
     * <ul>
     * <li>ConnRoutePNames.LOCAL_ADDRESS enabling IP-SPOOFING</li>
     * <li>Socket and connection timeout</li>
     * <li>Redirect handling</li>
     * <li>Keep Alive header or Connection Close</li>
     * <li>Calls setConnectionHeaders to setup headers</li>
     * <li>Calls setConnectionCookie to setup Cookie</li>
     * </ul>
     * 
     * @param url
     *            {@link URL} of the request
     * @param httpRequest
     *            http request for the request
     * @param res
     *            sample result to set cookies on
     * @throws IOException
     *             if hostname/ip to use could not be figured out
     */
    protected void setupRequest(URL url, HttpRequestBase httpRequest, HTTPSampleResult res) throws IOException {

        HttpParams requestParams = httpRequest.getParams();

        // Set up the local address if one exists
        final InetAddress inetAddr = getIpSourceAddress();
        if (inetAddr != null) {// Use special field ip source address (for pseudo 'ip spoofing')
            requestParams.setParameter(ConnRoutePNames.LOCAL_ADDRESS, inetAddr);
        } else if (localAddress != null) {
            requestParams.setParameter(ConnRoutePNames.LOCAL_ADDRESS, localAddress);
        } else { // reset in case was set previously
            requestParams.removeParameter(ConnRoutePNames.LOCAL_ADDRESS);
        }

        int rto = getResponseTimeout();
        if (rto > 0) {
            requestParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, rto);
        }

        int cto = getConnectTimeout();
        if (cto > 0) {
            requestParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, cto);
        }

        requestParams.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, getAutoRedirects());

        // a well-behaved browser is supposed to send 'Connection: close'
        // with the last request to an HTTP server. Instead, most browsers
        // leave it to the server to close the connection after their
        // timeout period. Leave it to the JMeter user to decide.
        if (getUseKeepAlive()) {
            httpRequest.setHeader(HTTPConstants.HEADER_CONNECTION, HTTPConstants.KEEP_ALIVE);
        } else {
            httpRequest.setHeader(HTTPConstants.HEADER_CONNECTION, HTTPConstants.CONNECTION_CLOSE);
        }

        setConnectionHeaders(httpRequest, url, getHeaderManager(), getCacheManager());

        String cookies = setConnectionCookie(httpRequest, url, getCookieManager());

        if (res != null) {
            res.setCookies(cookies);
        }

    }

    /**
     * Set any default request headers to include
     *
     * @param request the HttpRequest to be used
     */
    protected void setDefaultRequestHeaders(HttpRequest request) {
        // Method left empty here, but allows subclasses to override
    }

    /**
     * Gets the ResponseHeaders
     *
     * @param response
     *            containing the headers
     * @param localContext {@link HttpContext}
     * @return string containing the headers, one per line
     */
    private String getResponseHeaders(HttpResponse response, HttpContext localContext) {
        StringBuilder headerBuf = new StringBuilder();
        headerBuf.append(response.getStatusLine());// header[0] is not the status line...
        headerBuf.append("\n"); // $NON-NLS-1$

        Header[] rh = response.getAllHeaders();
        for (Header responseHeader : rh) {
            writeResponseHeader(headerBuf, responseHeader);
        }
        return headerBuf.toString();
    }

    /**
     * Write responseHeader to headerBuffer
     * @param headerBuffer {@link StringBuilder}
     * @param responseHeader {@link Header}
     */
    private void writeResponseHeader(StringBuilder headerBuffer, Header responseHeader) {
        headerBuffer.append(responseHeader.getName()).append(": ") // $NON-NLS-1$
                .append(responseHeader.getValue()).append("\n"); // $NON-NLS-1$
    }

    /**
     * Extracts all the required cookies for that particular URL request and
     * sets them in the <code>HttpMethod</code> passed in.
     *
     * @param request <code>HttpRequest</code> for the request
     * @param url <code>URL</code> of the request
     * @param cookieManager the <code>CookieManager</code> containing all the cookies
     * @return a String containing the cookie details (for the response)
     * May be null
     */
    protected String setConnectionCookie(HttpRequest request, URL url, CookieManager cookieManager) {
        String cookieHeader = null;
        if (cookieManager != null) {
            cookieHeader = cookieManager.getCookieHeaderForURL(url);
            if (cookieHeader != null) {
                request.setHeader(HTTPConstants.HEADER_COOKIE, cookieHeader);
            }
        }
        return cookieHeader;
    }

    /**
     * Extracts all the required non-cookie headers for that particular URL request and
     * sets them in the <code>HttpMethod</code> passed in
     *
     * @param request
     *            <code>HttpRequest</code> which represents the request
     * @param url
     *            <code>URL</code> of the URL request
     * @param headerManager
     *            the <code>HeaderManager</code> containing all the cookies
     *            for this <code>UrlConfig</code>
     * @param cacheManager the CacheManager (may be null)
     */
    protected void setConnectionHeaders(HttpRequestBase request, URL url, HeaderManager headerManager,
            CacheManager cacheManager) {
        if (headerManager != null) {
            CollectionProperty headers = headerManager.getHeaders();
            if (headers != null) {
                for (JMeterProperty jMeterProperty : headers) {
                    org.apache.jmeter.protocol.http.control.Header header = (org.apache.jmeter.protocol.http.control.Header) jMeterProperty
                            .getObjectValue();
                    String n = header.getName();
                    // Don't allow override of Content-Length
                    // TODO - what other headers are not allowed?
                    if (!HTTPConstants.HEADER_CONTENT_LENGTH.equalsIgnoreCase(n)) {
                        String v = header.getValue();
                        if (HTTPConstants.HEADER_HOST.equalsIgnoreCase(n)) {
                            int port = getPortFromHostHeader(v, url.getPort());
                            v = v.replaceFirst(":\\d+$", ""); // remove any port specification // $NON-NLS-1$ $NON-NLS-2$
                            if (port != -1) {
                                if (port == url.getDefaultPort()) {
                                    port = -1; // no need to specify the port if it is the default
                                }
                            }
                            request.getParams().setParameter(ClientPNames.VIRTUAL_HOST, new HttpHost(v, port));
                        } else {
                            request.addHeader(n, v);
                        }
                    }
                }
            }
        }
        if (cacheManager != null) {
            cacheManager.setHeaders(url, request);
        }
    }

    /**
     * Get port from the value of the Host header, or return the given
     * defaultValue
     *
     * @param hostHeaderValue
     *            value of the http Host header
     * @param defaultValue
     *            value to be used, when no port could be extracted from
     *            hostHeaderValue
     * @return integer representing the port for the host header
     */
    private int getPortFromHostHeader(String hostHeaderValue, int defaultValue) {
        String[] hostParts = hostHeaderValue.split(":");
        if (hostParts.length > 1) {
            String portString = hostParts[hostParts.length - 1];
            if (portString.matches("^\\d+$")) {
                return Integer.parseInt(portString);
            }
        }
        return defaultValue;
    }

    /**
     * Get all the request headers for the <code>HttpMethod</code>
     *
     * @param method
     *            <code>HttpMethod</code> which represents the request
     * @return the headers as a string
     */
    private String getConnectionHeaders(HttpRequest method) {
        if (method != null) {
            // Get all the request headers
            StringBuilder hdrs = new StringBuilder(100);
            Header[] requestHeaders = method.getAllHeaders();
            for (Header requestHeader : requestHeaders) {
                // Exclude the COOKIE header, since cookie is reported separately in the sample
                if (!HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(requestHeader.getName())) {
                    writeResponseHeader(hdrs, requestHeader);
                }
            }

            return hdrs.toString();
        }
        return ""; ////$NON-NLS-1$
    }

    /**
     * Setup credentials for url AuthScope but keeps Proxy AuthScope credentials
     * @param client HttpClient
     * @param url URL
     * @param authManager {@link AuthManager}
     * @param key key
     */
    private void setConnectionAuthorization(HttpClient client, URL url, AuthManager authManager,
            HttpClientKey key) {
        CredentialsProvider credentialsProvider = ((AbstractHttpClient) client).getCredentialsProvider();
        if (authManager != null) {
            if (authManager.hasAuthForURL(url)) {
                authManager.setupCredentials(client, url, credentialsProvider, localHost);
            } else {
                credentialsProvider.clear();
            }
        } else {
            Credentials credentials = null;
            AuthScope authScope = null;
            if (key.hasProxy && !StringUtils.isEmpty(key.proxyUser)) {
                authScope = new AuthScope(key.proxyHost, key.proxyPort);
                credentials = credentialsProvider.getCredentials(authScope);
            }
            credentialsProvider.clear();
            if (credentials != null) {
                credentialsProvider.setCredentials(authScope, credentials);
            }
        }
    }

    // Helper class so we can generate request data without dumping entire file contents
    private static class ViewableFileBody extends FileBody {
        private boolean hideFileData;

        public ViewableFileBody(File file, String mimeType) {
            super(file, mimeType);
            hideFileData = false;
        }

        @Override
        public void writeTo(final OutputStream out) throws IOException {
            if (hideFileData) {
                out.write("<actual file content, not shown here>".getBytes());// encoding does not really matter here
            } else {
                super.writeTo(out);
            }
        }
    }

    // TODO needs cleaning up
    /**
     * 
     * @param post {@link HttpPost}
     * @return String posted body if computable
     * @throws IOException if sending the data fails due to I/O
     */
    protected String sendPostData(HttpPost post) throws IOException {
        // Buffer to hold the post body, except file content
        StringBuilder postedBody = new StringBuilder(1000);
        HTTPFileArg[] files = getHTTPFiles();

        final String contentEncoding = getContentEncodingOrNull();
        final boolean haveContentEncoding = contentEncoding != null;

        // Check if we should do a multipart/form-data or an
        // application/x-www-form-urlencoded post request
        if (getUseMultipartForPost()) {
            // If a content encoding is specified, we use that as the
            // encoding of any parameter values
            Charset charset = null;
            if (haveContentEncoding) {
                charset = Charset.forName(contentEncoding);
            } else {
                charset = MIME.DEFAULT_CHARSET;
            }

            if (log.isDebugEnabled()) {
                log.debug("Building multipart with:getDoBrowserCompatibleMultipart():"
                        + getDoBrowserCompatibleMultipart() + ", with charset:" + charset + ", haveContentEncoding:"
                        + haveContentEncoding);
            }
            // Write the request to our own stream
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create().setCharset(charset);
            if (getDoBrowserCompatibleMultipart()) {
                multipartEntityBuilder.setLaxMode();
            } else {
                multipartEntityBuilder.setStrictMode();
            }
            // Create the parts
            // Add any parameters
            for (JMeterProperty jMeterProperty : getArguments()) {
                HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
                String parameterName = arg.getName();
                if (arg.isSkippable(parameterName)) {
                    continue;
                }
                StringBody stringBody = new StringBody(arg.getValue(), ContentType.create("text/plain", charset));
                FormBodyPart formPart = FormBodyPartBuilder.create(parameterName, stringBody).build();
                multipartEntityBuilder.addPart(formPart);
            }

            // Add any files
            // Cannot retrieve parts once added to the MultiPartEntity, so have to save them here.
            ViewableFileBody[] fileBodies = new ViewableFileBody[files.length];
            for (int i = 0; i < files.length; i++) {
                HTTPFileArg file = files[i];

                File reservedFile = FileServer.getFileServer().getResolvedFile(file.getPath());
                fileBodies[i] = new ViewableFileBody(reservedFile, file.getMimeType());
                multipartEntityBuilder.addPart(file.getParamName(), fileBodies[i]);
            }

            HttpEntity entity = multipartEntityBuilder.build();
            post.setEntity(entity);

            if (entity.isRepeatable()) {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                for (ViewableFileBody fileBody : fileBodies) {
                    fileBody.hideFileData = true;
                }
                entity.writeTo(bos);
                for (ViewableFileBody fileBody : fileBodies) {
                    fileBody.hideFileData = false;
                }
                bos.flush();
                // We get the posted bytes using the encoding used to create it
                postedBody.append(new String(bos.toByteArray(), contentEncoding == null ? "US-ASCII" // $NON-NLS-1$ this is the default used by HttpClient
                        : contentEncoding));
                bos.close();
            } else {
                postedBody.append("<Multipart was not repeatable, cannot view what was sent>"); // $NON-NLS-1$
            }

            //            // Set the content type TODO - needed?
            //            String multiPartContentType = multiPart.getContentType().getValue();
            //            post.setHeader(HEADER_CONTENT_TYPE, multiPartContentType);

        } else { // not multipart
            // Check if the header manager had a content type header
            // This allows the user to specify his own content-type for a POST request
            Header contentTypeHeader = post.getFirstHeader(HTTPConstants.HEADER_CONTENT_TYPE);
            boolean hasContentTypeHeader = contentTypeHeader != null && contentTypeHeader.getValue() != null
                    && contentTypeHeader.getValue().length() > 0;
            // If there are no arguments, we can send a file as the body of the request
            // TODO: needs a multiple file upload scenerio
            if (!hasArguments() && getSendFileAsPostBody()) {
                // If getSendFileAsPostBody returned true, it's sure that file is not null
                HTTPFileArg file = files[0];
                if (!hasContentTypeHeader) {
                    // Allow the mimetype of the file to control the content type
                    if (file.getMimeType() != null && file.getMimeType().length() > 0) {
                        post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, file.getMimeType());
                    } else {
                        post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE,
                                HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
                    }
                }

                FileEntity fileRequestEntity = new FileEntity(new File(file.getPath()), (ContentType) null);// TODO is null correct?
                post.setEntity(fileRequestEntity);

                // We just add placeholder text for file content
                postedBody.append("<actual file content, not shown here>");
            } else {
                // In a post request which is not multipart, we only support
                // parameters, no file upload is allowed

                // If a content encoding is specified, we set it as http parameter, so that
                // the post body will be encoded in the specified content encoding
                if (haveContentEncoding) {
                    post.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, contentEncoding);
                }

                // If none of the arguments have a name specified, we
                // just send all the values as the post body
                if (getSendParameterValuesAsPostBody()) {
                    // Allow the mimetype of the file to control the content type
                    // This is not obvious in GUI if you are not uploading any files,
                    // but just sending the content of nameless parameters
                    // TODO: needs a multiple file upload scenerio
                    if (!hasContentTypeHeader) {
                        HTTPFileArg file = files.length > 0 ? files[0] : null;
                        if (file != null && file.getMimeType() != null && file.getMimeType().length() > 0) {
                            post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE, file.getMimeType());
                        } else {
                            // TODO - is this the correct default?
                            post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE,
                                    HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
                        }
                    }

                    // Just append all the parameter values, and use that as the post body
                    StringBuilder postBody = new StringBuilder();
                    for (JMeterProperty jMeterProperty : getArguments()) {
                        HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
                        // Note: if "Encoded?" is not selected, arg.getEncodedValue is equivalent to arg.getValue
                        if (haveContentEncoding) {
                            postBody.append(arg.getEncodedValue(contentEncoding));
                        } else {
                            postBody.append(arg.getEncodedValue());
                        }
                    }
                    // Let StringEntity perform the encoding
                    StringEntity requestEntity = new StringEntity(postBody.toString(), contentEncoding);
                    post.setEntity(requestEntity);
                    postedBody.append(postBody.toString());
                } else {
                    // It is a normal post request, with parameter names and values

                    // Set the content type
                    if (!hasContentTypeHeader) {
                        post.setHeader(HTTPConstants.HEADER_CONTENT_TYPE,
                                HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED);
                    }
                    // Add the parameters
                    PropertyIterator args = getArguments().iterator();
                    List<NameValuePair> nvps = new ArrayList<>();
                    String urlContentEncoding = contentEncoding;
                    if (urlContentEncoding == null || urlContentEncoding.length() == 0) {
                        // Use the default encoding for urls
                        urlContentEncoding = EncoderCache.URL_ARGUMENT_ENCODING;
                    }
                    while (args.hasNext()) {
                        HTTPArgument arg = (HTTPArgument) args.next().getObjectValue();
                        // The HTTPClient always urlencodes both name and value,
                        // so if the argument is already encoded, we have to decode
                        // it before adding it to the post request
                        String parameterName = arg.getName();
                        if (arg.isSkippable(parameterName)) {
                            continue;
                        }
                        String parameterValue = arg.getValue();
                        if (!arg.isAlwaysEncoded()) {
                            // The value is already encoded by the user
                            // Must decode the value now, so that when the
                            // httpclient encodes it, we end up with the same value
                            // as the user had entered.
                            parameterName = URLDecoder.decode(parameterName, urlContentEncoding);
                            parameterValue = URLDecoder.decode(parameterValue, urlContentEncoding);
                        }
                        // Add the parameter, httpclient will urlencode it
                        nvps.add(new BasicNameValuePair(parameterName, parameterValue));
                    }
                    UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nvps, urlContentEncoding);
                    post.setEntity(entity);
                    if (entity.isRepeatable()) {
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        post.getEntity().writeTo(bos);
                        bos.flush();
                        // We get the posted bytes using the encoding used to create it
                        if (contentEncoding != null) {
                            postedBody.append(new String(bos.toByteArray(), contentEncoding));
                        } else {
                            postedBody.append(new String(bos.toByteArray(), SampleResult.DEFAULT_HTTP_ENCODING));
                        }
                        bos.close();
                    } else {
                        postedBody.append("<RequestEntity was not repeatable, cannot view what was sent>");
                    }
                }
            }
        }
        return postedBody.toString();
    }

    // TODO merge put and post methods as far as possible.
    // e.g. post checks for multipart form/files, and if not, invokes sendData(HttpEntityEnclosingRequestBase)

    /**
     * Creates the entity data to be sent.
     * <p>
     * If there is a file entry with a non-empty MIME type we use that to
     * set the request Content-Type header, otherwise we default to whatever
     * header is present from a Header Manager.
     * <p>
     * If the content charset {@link #getContentEncoding()} is null or empty 
     * we use the HC4 default provided by {@link HTTP#DEF_CONTENT_CHARSET} which is
     * ISO-8859-1.
     * 
     * @param entity to be processed, e.g. PUT or PATCH
     * @return the entity content, may be empty
     * @throws  UnsupportedEncodingException for invalid charset name
     * @throws IOException cannot really occur for ByteArrayOutputStream methods
     */
    protected String sendEntityData(HttpEntityEnclosingRequestBase entity) throws IOException {
        // Buffer to hold the entity body
        StringBuilder entityBody = new StringBuilder(1000);
        boolean hasEntityBody = false;

        final HTTPFileArg[] files = getHTTPFiles();
        // Allow the mimetype of the file to control the content type
        // This is not obvious in GUI if you are not uploading any files,
        // but just sending the content of nameless parameters
        final HTTPFileArg file = files.length > 0 ? files[0] : null;
        String contentTypeValue = null;
        if (file != null && file.getMimeType() != null && file.getMimeType().length() > 0) {
            contentTypeValue = file.getMimeType();
            entity.setHeader(HEADER_CONTENT_TYPE, contentTypeValue); // we provide the MIME type here
        }

        // Check for local contentEncoding (charset) override; fall back to default for content body
        // we do this here rather so we can use the same charset to retrieve the data
        final String charset = getContentEncoding(HTTP.DEF_CONTENT_CHARSET.name());

        // Only create this if we are overriding whatever default there may be
        // If there are no arguments, we can send a file as the body of the request

        if (!hasArguments() && getSendFileAsPostBody()) {
            hasEntityBody = true;

            // If getSendFileAsPostBody returned true, it's sure that file is not null
            File reservedFile = FileServer.getFileServer().getResolvedFile(files[0].getPath());
            FileEntity fileRequestEntity = new FileEntity(reservedFile); // no need for content-type here
            entity.setEntity(fileRequestEntity);
        }
        // If none of the arguments have a name specified, we
        // just send all the values as the entity body
        else if (getSendParameterValuesAsPostBody()) {
            hasEntityBody = true;

            // Just append all the parameter values, and use that as the entity body
            StringBuilder entityBodyContent = new StringBuilder();
            for (JMeterProperty jMeterProperty : getArguments()) {
                HTTPArgument arg = (HTTPArgument) jMeterProperty.getObjectValue();
                // Note: if "Encoded?" is not selected, arg.getEncodedValue is equivalent to arg.getValue
                if (charset != null) {
                    entityBodyContent.append(arg.getEncodedValue(charset));
                } else {
                    entityBodyContent.append(arg.getEncodedValue());
                }
            }
            StringEntity requestEntity = new StringEntity(entityBodyContent.toString(), charset);
            entity.setEntity(requestEntity);
        }
        // Check if we have any content to send for body
        if (hasEntityBody) {
            // If the request entity is repeatable, we can send it first to
            // our own stream, so we can return it
            final HttpEntity entityEntry = entity.getEntity();
            if (entityEntry.isRepeatable()) {
                entityBody.append("<actual file content, not shown here>");
            } else { // this probably cannot happen
                entityBody.append("<RequestEntity was not repeatable, cannot view what was sent>");
            }
        }
        return entityBody.toString(); // may be the empty string
    }

    /**
     * 
     * @return the value of {@link #getContentEncoding()}; forced to null if empty
     */
    private String getContentEncodingOrNull() {
        return getContentEncoding(null);
    }

    /**
     * @param dflt the default to be used
     * @return the value of {@link #getContentEncoding()}; default if null or empty
     */
    private String getContentEncoding(String dflt) {
        String ce = getContentEncoding();
        if (isNullOrEmptyTrimmed(ce)) {
            return dflt;
        } else {
            return ce;
        }
    }

    private void saveConnectionCookies(HttpResponse method, URL u, CookieManager cookieManager) {
        if (cookieManager != null) {
            Header[] hdrs = method.getHeaders(HTTPConstants.HEADER_SET_COOKIE);
            for (Header hdr : hdrs) {
                cookieManager.addCookieFromHeader(hdr.getValue(), u);
            }
        }
    }

    @Override
    protected void notifyFirstSampleAfterLoopRestart() {
        log.debug("notifyFirstSampleAfterLoopRestart");
        resetSSLContext = !USE_CACHED_SSL_CONTEXT;
    }

    @Override
    protected void threadFinished() {
        log.debug("Thread Finished");
        closeThreadLocalConnections();
    }

    /**
     * 
     */
    private void closeThreadLocalConnections() {
        // Does not need to be synchronised, as all access is from same thread
        Map<HttpClientKey, HttpClient> mapHttpClientPerHttpClientKey = HTTPCLIENTS_CACHE_PER_THREAD_AND_HTTPCLIENTKEY
                .get();
        if (mapHttpClientPerHttpClientKey != null) {
            for (HttpClient cl : mapHttpClientPerHttpClientKey.values()) {
                ((AbstractHttpClient) cl).clearRequestInterceptors();
                ((AbstractHttpClient) cl).clearResponseInterceptors();
                ((AbstractHttpClient) cl).close();
                cl.getConnectionManager().shutdown();
            }
            mapHttpClientPerHttpClientKey.clear();
        }
    }

    @Override
    public boolean interrupt() {
        HttpUriRequest request = currentRequest;
        if (request != null) {
            currentRequest = null; // don't try twice
            try {
                request.abort();
            } catch (UnsupportedOperationException e) {
                log.warn("Could not abort pending request", e);
            }
        }
        return request != null;
    }

}