/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.restassured.internal.http;

import groovy.lang.Closure;
import io.restassured.config.DecoderConfig;
import io.restassured.config.EncoderConfig;
import io.restassured.config.OAuthConfig;
import io.restassured.http.ContentType;
import io.restassured.http.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.protocol.HttpContext;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.MethodClosure;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;

/**
 *

 * Groovy DSL for easily making HTTP requests, and handling request and response
 * data.  This class adds a number of convenience mechanisms built on top of
 * Apache HTTPClient for things like URL-encoded POSTs and REST requests that
 * require building and parsing JSON or XML.  Convenient access to a few common
 * authentication methods is also available.

 *

 *

 *
Conventions

 *
HTTPBuilder has properties for default headers, URI, contentType, etc.
 * All of these values are also assignable (and in many cases, in much finer
 * detail) from the {@link RequestConfigDelegate} as well.  In any cases where the value
 * is not set on the delegate (from within a request closure,) the builder's
 * default value is used.

 *

 *
For instance, any methods that do not take a uri parameter
 * assume you will set the uri property in the request closure or
 * use HTTPBuilder's assigned {@link #getUri() default URI}.

 *

 *

 *
Response Parsing

 *
By default, HTTPBuilder uses {@link ContentType#ANY} as the default
 * content-type.  This means the value of the request's Accept
 * header is */*, and the response parser is determined
 * based on the response content-type header.

 *

 *
If any contentType is given (either in
 * {@link #setContentType(Object)} or as a request method parameter), the
 * builder will attempt to parse the response using that content-type,
 * regardless of what the server actually responds with.

 *

 *

 *
Examples:

 * Perform an HTTP GET and print the response:
 *

 *   def http = new HTTPBuilder('http://www.google.com')
 *
 *   http.get( path : '/search',
 *             contentType : TEXT,
 *             query : [q:'Groovy'] ) { resp, reader ->
 *     println "response status: ${resp.statusLine}"
 *     println 'Response data: -----'
 *     System.out << reader
 *     println '\n--------------------'
 *   }
 *

 *

 * Long form for other HTTP methods, and response-code-specific handlers.
 * This is roughly equivalent to the above example.
 *

 *

 *   def http = new HTTPBuilder('http://www.google.com/search?q=groovy')
 *
 *   http.request( GET, TEXT ) { req ->
 *
 *     // executed for all successful responses:
 *     response.success = { resp, reader ->
 *       println 'my response handler!'
 *       assert resp.statusLine.statusCode == 200
 *       println resp.statusLine
 *       System.out << reader // print response stream
 *     }
 *
 *     // executed only if the response status code is 401:
 *     response.'404' = { resp ->
 *       println 'not found!'
 *     }
 *   }
 *

 *

 * You can also set a default response handler called for any status
 * code > 399 that is not matched to a specific handler. Setting the value
 * outside a request closure means it will apply to all future requests with
 * this HTTPBuilder instance:
 *

 *   http.handler.failure = { resp ->
 *     println "Unexpected failure: ${resp.statusLine}"
 *   }
 *

 *

 *

 * And...  Automatic response parsing for registered content types!
 *

 *

 *   http.request( 'http://ajax.googleapis.com', GET, JSON ) {
 *     uri.path = '/ajax/services/search/web'
 *     uri.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
 *
 *     response.success = { resp, json ->
 *       assert json.size() == 3
 *       println "Query response: "
 *       json.responseData.results.each {
 *         println "  ${it.titleNoFormatting} : ${it.visibleUrl}"
 *       }
 *     }
 *   }
 *

 *
 * @author Tom Nichols
 */
public abstract class HTTPBuilder {

    protected AbstractHttpClient client;
    protected URIBuilder defaultURI = null;
    protected AuthConfig auth;

    protected final Log log = LogFactory.getLog(getClass());

    protected Object defaultContentType = ContentType.ANY;
    protected Object defaultRequestContentType = null;
    protected final Map<Object, Closure> defaultResponseHandlers = new StringHashMap<Closure>(buildDefaultResponseHandlers());
    protected ContentEncodingRegistry contentEncodingHandler;

    protected final Map<Object, Object> defaultRequestHeaders = new StringHashMap<Object>();

    protected EncoderRegistry encoders;
    protected HttpResponseContentTypeFinder parsers = new HttpResponseContentTypeFinder();
    private EncoderConfig encoderConfig;
    private boolean urlEncodingEnabled;

    public HTTPBuilder(boolean urlEncodingEnabled, EncoderConfig encoderConfig, DecoderConfig decoderConfig, OAuthConfig oAuthConfig,
                       AbstractHttpClient client) {
        this.client = client;
        this.auth = new AuthConfig(this, oAuthConfig);
        this.contentEncodingHandler = new ContentEncodingRegistry(decoderConfig);
        this.setContentEncoding(ContentEncoding.Type.GZIP, ContentEncoding.Type.DEFLATE);
        this.encoderConfig = encoderConfig == null ? new EncoderConfig() : encoderConfig;
        this.urlEncodingEnabled = urlEncodingEnabled;
        encoders = new EncoderRegistry();
        if (this.encoderConfig.hasContentEncoders()) {
            Map<String, ContentType> customEncoders = this.encoderConfig.contentEncoders();
            for (Map.Entry<String, ContentType> entry : customEncoders.entrySet()) {
                // Get the pre-defined encoder for the given content-type
                Closure actualEncoder = encoders.getAt(entry.getValue().getContentTypeStrings()[0]);
                encoders.putAt(entry.getKey(), actualEncoder);
            }
        }
    }

    /**
     * Give a default URI to be used for all request methods that don't
     * explicitly take a URI parameter.
     *
     * @param defaultURI either a {@link URL}, {@link URI} or object whose
     *                   toString() produces a valid URI string.  See
     *                   {@link URIBuilder#convertToURI(Object)}.
     * @throws URISyntaxException if the given argument does not represent a valid URI
     */
    public HTTPBuilder(Object defaultURI, boolean urlEncodingEnabled, EncoderConfig encoderConfig, DecoderConfig decoderConfig, OAuthConfig oAuthConfig,
                       AbstractHttpClient client) {
        this(urlEncodingEnabled, encoderConfig, decoderConfig, oAuthConfig, client);
        try {
            this.defaultURI = new URIBuilder(URIBuilder.convertToURI(defaultURI), this.urlEncodingEnabled, this.encoderConfig);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Give a default URI to be used for all request methods that don't
     * explicitly take a URI parameter, and a default content-type to be used
     * for request encoding and response parsing.
     *
     * @param defaultURI         either a {@link URL}, {@link URI} or object whose
     *                           toString() produces a valid URI string.  See
     *                           {@link URIBuilder#convertToURI(Object)}.
     * @param defaultContentType content-type string.  See {@link ContentType}
     *                           for common types.
     * @throws URISyntaxException if the uri argument does not represent a valid URI
     */
    public HTTPBuilder(Object defaultURI, Object defaultContentType, boolean urlEncodingEnabled, EncoderConfig encoderConfig,
                       DecoderConfig decoderConfig, OAuthConfig oAuthConfig, AbstractHttpClient client) throws URISyntaxException {
        this(urlEncodingEnabled, encoderConfig, decoderConfig, oAuthConfig, client);
        this.defaultURI = new URIBuilder(URIBuilder.convertToURI(defaultURI), urlEncodingEnabled, this.encoderConfig);
        this.defaultContentType = defaultContentType;
    }

    /**
     *
Convenience method to perform an HTTP GET.  It will use the HTTPBuilder's
     * {@link #getHandler() registered response handlers} to handle success or
     * failure status codes.  By default, the success response
     * handler will attempt to parse the data and simply return the parsed
     * object.

     *

     *
Note: If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object)
     * default success response handler}, be sure to read the
     * caveat regarding streaming response data.

     *
     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @return whatever was returned from the response closure.
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     * @throws IOException
     * @throws ClientProtocolException
     * @see #getHandler()
     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
     * @see #defaultFailureHandler(HttpResponseDecorator)
     */
    public Object get(Map<String, ?> args)
            throws ClientProtocolException, IOException, URISyntaxException {
        return this.get(args, null);
    }

    /**
     *
Convenience method to perform an HTTP GET.  The response closure will
     * be called only on a successful response.

     *

     *
A 'failed' response (i.e. any HTTP status code > 399) will be handled
     * by the registered 'failure' handler.  The
     * {@link #defaultFailureHandler(HttpResponseDecorator) default failure handler}
     * throws an {@link HttpResponseException}.

     *
     * @param args            see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @param responseClosure code to handle a successful HTTP response
     * @return any value returned by the response closure.
     * @throws ClientProtocolException
     * @throws IOException
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     */
    public Object get(Map<String, ?> args, Closure responseClosure)
            throws ClientProtocolException, IOException, URISyntaxException {
        RequestConfigDelegate delegate = new RequestConfigDelegate(new HttpGet(),
                this.defaultContentType,
                this.defaultRequestHeaders,
                this.defaultResponseHandlers);

        delegate.setPropertiesFromMap(args);
        if (responseClosure != null) delegate.getResponse().put(
                Status.SUCCESS, responseClosure);
        return this.doRequest(delegate);
    }

    /**
     *
Convenience method to perform an HTTP POST.  It will use the HTTPBuilder's
     * {@link #getHandler() registered response handlers} to handle success or
     * failure status codes.  By default, the success response
     * handler will attempt to parse the data and simply return the parsed
     * object.

     *

     *
Note: If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object)
     * default success response handler}, be sure to read the
     * caveat regarding streaming response data.

     *
     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @return whatever was returned from the response closure.
     * @throws IOException
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     * @throws ClientProtocolException
     * @see #getHandler()
     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
     * @see #defaultFailureHandler(HttpResponseDecorator)
     */
    public Object post(Map<String, ?> args)
            throws ClientProtocolException, URISyntaxException, IOException {
        return this.post(args, null);
    }

    /**
     *

     * Convenience method to perform an HTTP form POST.  The response closure will be
     * called only on a successful response.

     *

     *
A 'failed' response (i.e. any
     * HTTP status code > 399) will be handled by the registered 'failure'
     * handler.  The {@link #defaultFailureHandler(HttpResponseDecorator) default
     * failure handler} throws an {@link HttpResponseException}.

     *

     *
The request body (specified by a body named parameter)
     * will be converted to a url-encoded form string unless a different
     * requestContentType named parameter is passed to this method.
     * (See {@link EncoderRegistry#encodeForm(Map)}.)

     *
     * @param args            see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @param responseClosure code to handle a successful HTTP response
     * @return any value returned by the response closure.
     * @throws ClientProtocolException
     * @throws IOException
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     */
    public Object post(Map<String, ?> args, Closure responseClosure)
            throws URISyntaxException, ClientProtocolException, IOException {
        RequestConfigDelegate delegate = new RequestConfigDelegate(new HttpPost(),
                this.defaultContentType,
                this.defaultRequestHeaders,
                this.defaultResponseHandlers);

        /* by default assume the request body will be URLEncoded, but allow
             the 'requestContentType' named argument to override this if it is
             given */
        delegate.setRequestContentType(ContentType.URLENC.toString());
        delegate.setPropertiesFromMap(args);

        if (responseClosure != null) delegate.getResponse().put(
                Status.SUCCESS.toString(), responseClosure);

        return this.doRequest(delegate);
    }

    /**
     *
Convenience method to perform an HTTP PATCH.  It will use the HTTPBuilder's
     * {@link #getHandler() registered response handlers} to handle success or
     * failure status codes.  By default, the success response
     * handler will attempt to parse the data and simply return the parsed
     * object.

     *

     *
Note: If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object)
     * default success response handler}, be sure to read the
     * caveat regarding streaming response data.

     *
     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @return whatever was returned from the response closure.
     * @throws IOException
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     * @throws ClientProtocolException
     * @see #getHandler()
     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
     * @see #defaultFailureHandler(HttpResponseDecorator)
     */
    public Object patch(Map<String, ?> args)
            throws ClientProtocolException, URISyntaxException, IOException {
        return this.patch(args, null);
    }

    /**
     *

     * Convenience method to perform an HTTP form PATCH.  The response closure will be
     * called only on a successful response.

     *

     *
A 'failed' response (i.e. any
     * HTTP status code > 399) will be handled by the registered 'failure'
     * handler.  The {@link #defaultFailureHandler(HttpResponseDecorator) default
     * failure handler} throws an {@link HttpResponseException}.

     *

     *
The request body (specified by a body named parameter)
     * will be converted to a url-encoded form string unless a different
     * requestContentType named parameter is passed to this method.
     * (See {@link EncoderRegistry#encodeForm(Map)}.)

     *
     * @param args            see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
     * @param responseClosure code to handle a successful HTTP response
     * @return any value returned by the response closure.
     * @throws ClientProtocolException
     * @throws IOException
     * @throws URISyntaxException      if a uri argument is given which does not
     *                                 represent a valid URI
     */
    public Object patch(Map<String, ?> args, Closure responseClosure)
            throws URISyntaxException, ClientProtocolException, IOException {
        RequestConfigDelegate delegate = new RequestConfigDelegate(new HttpPatch(),
                this.defaultContentType,
                this.defaultRequestHeaders,
                this.defaultResponseHandlers);

        /* by default assume the request body will be URLEncoded, but allow
             the 'requestContentType' named argument to override this if it is
             given */
        delegate.setRequestContentType(ContentType.URLENC.toString());
        delegate.setPropertiesFromMap(args);

        if (responseClosure != null) {
            delegate.getResponse().put(Status.SUCCESS.toString(), responseClosure);
            delegate.getResponse().put(Status.FAILURE.toString(), responseClosure);
        }

        return this.doRequest(delegate);
    }

    /**
     * Make an HTTP request to the default URI, and parse using the default
     * content-type.
     *
     * @param method        {@link HttpRequestFactory HTTP method}
     * @param configClosure request configuration options
     * @return whatever value was returned by the executed response handler.
     * @throws ClientProtocolException
     * @throws IOException
     * @see #request(Object, HttpRequestFactory, Object, Closure)
     */
    public Object request(String method, boolean hasBody, Closure configClosure) throws ClientProtocolException, IOException {
        return this.doRequest(this.defaultURI.toURI(), method, this.defaultContentType, hasBody, configClosure);
    }

    /**
     * Make an HTTP request using the default URI, with the given method,
     * content-type, and configuration.
     *
     * @param method        {@link HttpRequestFactory HTTP method}
     * @param contentType   either a {@link ContentType} or valid content-type string.
     * @param configClosure request configuration options
     * @return whatever value was returned by the executed response handler.
     * @throws ClientProtocolException
     * @throws IOException
     * @see #request(Object, HttpRequestFactory, Object, Closure)
     */
    public Object request(String method, Object contentType, boolean hasBody, Closure configClosure)
            throws ClientProtocolException, IOException {
        return this.doRequest(this.defaultURI.toURI(), method, contentType, hasBody, configClosure);
    }

    /**
     * Make a request for the given HTTP method and content-type, with
     * additional options configured in the configClosure.  See
     * {@link RequestConfigDelegate} for options.
     *
     * @param uri           either a {@link URL}, {@link URI} or object whose
     *                      toString() produces a valid URI string.  See
     *                      {@link URIBuilder#convertToURI(Object)}.
     * @param method        {@link HttpRequestFactory HTTP method}
     * @param contentType   either a {@link ContentType} or valid content-type string.
     * @param configClosure closure from which to configure options like
     *                      {@link RequestConfigDelegate#getUri() uri.path},
     *                      {@link URIBuilder#setQuery(Map) request parameters},
     *                      {@link RequestConfigDelegate#setHeaders(Map) headers},
     *                      {@link RequestConfigDelegate#setBody(Object, Object) request body} and
     *                      {@link RequestConfigDelegate#getResponse() response handlers}.
     * @return whatever value was returned by the executed response handler.
     * @throws ClientProtocolException
     * @throws IOException
     * @throws URISyntaxException      if the uri argument does not represent a valid URI
     */
    public Object request(Object uri, String method, Object contentType, boolean hasBody, Closure configClosure)
            throws ClientProtocolException, IOException, URISyntaxException {
        return this.doRequest(URIBuilder.convertToURI(uri), method, contentType, hasBody, configClosure);
    }

    /**
     * Create a {@link RequestConfigDelegate} from the given arguments, execute the
     * config closure, then pass the delegate to {@link #doRequest(RequestConfigDelegate)},
     * which actually executes the request.
     */
    protected Object doRequest(URI uri, String method, Object contentType, boolean hasBody, Closure configClosure) throws IOException {
        HttpRequestBase reqMethod = HttpRequestFactory.createHttpRequest(uri, method, hasBody);
        RequestConfigDelegate delegate = new RequestConfigDelegate(reqMethod, contentType,
                this.defaultRequestHeaders,
                this.defaultResponseHandlers);
        configClosure.setDelegate(delegate);
        configClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
        configClosure.call(reqMethod);

        return this.doRequest(delegate);
    }

    /**
     * A copy of HTTP builders doRequest method with two exceptions.
     *

     *
The exception is that the entity's content is not closed if no body matchers are specified.

     *
If headers contain a list of elements the headers are added and not overridden

     *

     */
    protected abstract Object doRequest(final RequestConfigDelegate delegate)
            throws ClientProtocolException, IOException;

    /**
     * Parse the response data based on the given content-type.
     * If the given content-type is {@link ContentType#ANY}, the
     * content-type header from the response will be used to
     * determine how to parse the response.
     *
     * @param resp
     * @param contentType
     * @return whatever was returned from the parser retrieved for the given
     * content-type, or null if no parser could be found for this
     * content-type.  The parser will also return null if the
     * response does not contain any content (e.g. in response to a HEAD request).
     * @throws HttpResponseException if there is a error parsing the response
     */
    protected Object parseResponse(HttpResponse resp, Object contentType)
            throws IOException {
        // For HEAD or OPTIONS requests, there should be no response entity.
        if (resp.getEntity() == null) {
            log.debug("Response contains no entity.  Parsed data is null.");
            return null;
        }
        // first, start with the _given_ content-type
        String responseContentType = contentType.toString();
        // if the given content-type is ANY ("*/*") then use the response content-type
        try {
            if (ContentType.ANY.toString().equals(responseContentType))
                responseContentType = HttpResponseContentTypeFinder.findContentType(resp);
        } catch (RuntimeException ex) {
            /* if for whatever reason we can't determine the content-type, but
                * still want to attempt to parse the data, use the BINARY
                * content-type so that the response will be buffered into a
                * ByteArrayInputStream. */
            responseContentType = ContentType.BINARY.toString();
        }

        Object parsedData = null;
        log.debug("Parsing response as: " + responseContentType);
        parsedData = resp.getEntity().getContent();
        if (parsedData == null) log.debug("Parser returned null!");
        else log.debug("Parsed data to instance of: " + parsedData.getClass());
        return parsedData;
    }

    /**
     * Creates default response handlers for {@link Status#SUCCESS success} and
     * {@link Status#FAILURE failure} status codes.  This is used to populate
     * the handler map when a new HTTPBuilder instance is created.
     *
     * @return the default response handler map.
     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
     * @see #defaultFailureHandler(HttpResponseDecorator)
     */
    protected Map<Object, Closure> buildDefaultResponseHandlers() {
        Map<Object, Closure> map = new StringHashMap<Closure>();
        map.put(Status.SUCCESS,
                new MethodClosure(this, "defaultSuccessHandler"));
        map.put(Status.FAILURE,
                new MethodClosure(this, "defaultFailureHandler"));

        return map;
    }

    /**
     *
This is the default response.success handler.  It will be
     * executed if the response is not handled by a status-code-specific handler
     * (i.e. response.'200'= {..}) and no generic 'success' handler
     * is given (i.e. response.success = {..}.)  This handler simply
     * returns the parsed data from the response body.  In most cases you will
     * probably want to define a response.success = {...} handler
     * from the request closure, which will replace the response handler defined
     * by this method.

     *

     *
In practice, a user-supplied response handler closure is
     * designed to handle streaming content so it can be read directly from
     * the response stream without buffering, which will be much more efficient.
     * Therefore, it is recommended that request method variants be used which
     * explicitly accept a response handler closure in these cases.

     *
     * @param resp       HTTP response
     * @param parsedData parsed data as resolved from this instance's {@link HttpResponseContentTypeFinder}
     * @return the parsed data object (whatever the parser returns).
     * @throws ResponseParseException if there is an error buffering a streaming
     *                                response.
     */
    protected Object defaultSuccessHandler(HttpResponseDecorator resp, Object parsedData)
            throws ResponseParseException {
        try {
            //If response is streaming, buffer it in a byte array:
            if (parsedData instanceof InputStream) {
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                DefaultGroovyMethods.leftShift(buffer, (InputStream) parsedData);
                parsedData = new ByteArrayInputStream(buffer.toByteArray());
            } else if (parsedData instanceof Reader) {
                StringWriter buffer = new StringWriter();
                DefaultGroovyMethods.leftShift(buffer, (Reader) parsedData);
                parsedData = new StringReader(buffer.toString());
            } else if (parsedData instanceof Closeable)
                log.debug("Parsed data is streaming, but will be accessible after " +
                        "the network connection is closed.  Use at your own risk!");
            return parsedData;
        } catch (IOException ex) {
            throw new ResponseParseException(resp, ex);
        }
    }

    /**
     * This is the default response.failure handler.  It will be
     * executed if no status-code-specific handler is set (i.e.
     * response.'404'= {..}).  This default handler will throw a
     * {@link HttpResponseException} when executed.  In most cases you
     * will want to define your own response.failure = {...}
     * handler from the request closure, if you don't want an exception to be
     * thrown for 4xx and 5xx status responses.
     *
     * @param resp
     * @throws HttpResponseException
     */
    protected void defaultFailureHandler(HttpResponseDecorator resp) throws HttpResponseException {
        throw new HttpResponseException(resp);
    }

    /**
     * Retrieve the map of response code handlers.  Each map key is a response
     * code as a string (i.e. '401') or either 'success' or 'failure'.  Use this
     * to set default response handlers, e.g.
     *
builder.handler.'401' = { resp -> println "${resp.statusLine}" }

     *
     * @return
     * @see Status
     */
    public Map<?, Closure> getHandler() {
        return this.defaultResponseHandlers;
    }

    /**
     * Retrieve the map of registered response content-type parsers.  Use
     * this to set default response parsers, e.g.
     *

     * builder.parser.'text/javascript' = { resp ->
     * 	  return resp.entity.content // just returns an InputStream
     * }

     *
     * @return
     */
    public HttpResponseContentTypeFinder getParser() {
        return this.parsers;
    }

    /**
     * Set the default content type that will be used to select the appropriate
     * request encoder and response parser.  The {@link ContentType} enum holds
     * some common content-types that may be used, i.e.

     * import static ContentType.*
     * builder.contentType = XML
     *

     * Setting the default content-type does three things:
     *

     *
It tells the builder to encode any {@link RequestConfigDelegate#setBody(Object, Object)
     * request body} as this content-type.  Calling {@link
     * RequestConfigDelegate#setRequestContentType(String)} can override this
     * on a per-request basis.

     *
Tells the builder to parse any response as this content-type,
     * regardless of any content-type header that is sent in the
     * response.

     *
Sets the Accept header to this content-type for all
     * requests (see {@link ContentType#getAcceptHeader()}).  Note
     * that any Accept header explicitly set either in
     * {@link #setHeaders(Map)} or {@link RequestConfigDelegate#setHeaders(Map)}
     * will override this value.

     *

     *
Additionally, if the content-type is set to {@link ContentType#ANY},
     * HTTPBuilder will rely on the content-type response
     * header to determine how to parse the response data.  This allows the user
     * to rely on response headers if they are accurate, or ignore them and
     * forcibly use a certain response parser if so desired.

     *

     *
This value is a default and may always be overridden on a per-request
     * basis by using the {@link #request(Method, Closure)}
     * builder.request( Method, ContentType, Closure )} method or passing a
     * contentType named parameter.
     *
     * @param ct either a {@link ContentType} or string value (i.e. "text/xml".)
     * @see EncoderRegistry
     * @see HttpResponseContentTypeFinder
     */
    public void setContentType(Object ct) {
        this.defaultContentType = ct;
    }

    /**
     * @return default content type used for request and response.
     */
    public Object getContentType() {
        return this.defaultContentType;
    }

    /**
     * Set acceptable request and response content-encodings.
     *
     * @param encodings each Object should be either a
     *                  {@link ContentEncoding.Type} value, or a content-encoding
     *                  string that is known by the {@link ContentEncodingRegistry}
     * @see ContentEncodingRegistry
     */
    public void setContentEncoding(Object... encodings) {
        this.contentEncodingHandler.setInterceptors(client, encodings);
    }

    /**
     * Set the default URI used for requests that do not explicitly take a
     * uri param.
     *
     * @param uri either a {@link URL}, {@link URI} or object whose
     *            toString() produces a valid URI string.  See
     *            {@link URIBuilder#convertToURI(Object)}.
     * @throws URISyntaxException if the uri argument does not represent a valid URI
     */
    public void setUri(Object uri) throws URISyntaxException {
        this.defaultURI = new URIBuilder(URIBuilder.convertToURI(uri), urlEncodingEnabled, encoderConfig);
    }

    /**
     * Get the default URI used for requests that do not explicitly take a
     * uri param.
     *
     * @return a {@link URIBuilder} instance.  Note that the return type is Object
     * simply so that it matches with its JavaBean {@link #setUri(Object)}
     * counterpart.
     */
    public Object getUri() {
        return defaultURI;
    }

    /**
     * Set the default headers to add to all requests made by this builder
     * instance.  These values will replace any previously set default headers.
     *
     * @param headers map of header names & values.
     */
    public void setHeaders(Map<?, ?> headers) {
        this.defaultRequestHeaders.clear();
        if (headers == null) return;
        for (Object key : headers.keySet()) {
            Object val = headers.get(key);
            if (val == null) continue;
            this.defaultRequestHeaders.put(key.toString(), val.toString());
        }
    }

    /**
     * Get the map of default headers that will be added to all requests.
     * This is a 'live' collection so it may be used to add or remove default
     * values.
     *
     * @return the map of default header names and values.
     */
    public Map<?, ?> getHeaders() {
        return this.defaultRequestHeaders;
    }

    /**
     * Return the underlying HTTPClient that is used to handle HTTP requests.
     *
     * @return the client instance.
     */
    public AbstractHttpClient getClient() {
        return this.client;
    }

    /**
     * Set a custom registry used to handle different
     * content-encoding types in responses.
     *
     * @param cer
     */
    public void setContentEncodingRegistry(ContentEncodingRegistry cer) {
        this.contentEncodingHandler = cer;
    }

    /**
     * Set the default HTTP proxy to be used for all requests.
     *
     * @param host   host name or IP
     * @param port   port, or -1 for the default port
     * @param scheme usually "http" or "https," or null for the default
     * @see HttpHost#HttpHost(String, int, String)
     */
    public void setProxy(String host, int port, String scheme) {
        getClient().getParams().setParameter(
                ConnRoutePNames.DEFAULT_PROXY,
                new HttpHost(host, port, scheme));
    }

    /**
     * Release any system resources held by this instance.
     *
     * @see ClientConnectionManager#shutdown()
     */
    public void shutdown() {
        client.getConnectionManager().shutdown();
    }

    /**
     *
Encloses all properties and method calls used within the
     * {@link HTTPBuilder#request(Object, HttpRequestFactory, Object, Closure)} 'config'
     * closure argument.  That is, an instance of this class is set as the
     * closure's delegate.  This allows the user to configure various parameters
     * within the scope of a single request.

     *

     *
All properties of this class are available from within the closure.
     * For example, you can manipulate various aspects of the
     * {@link HTTPBuilder#setUri(Object) default request URI} for this request
     * by calling uri.path = '/api/location'.  This allows for the
     * ability to modify parameters per-request while leaving any values set
     * directly on the HTTPBuilder instance unchanged for subsequent requests.
     *

     */
    protected class RequestConfigDelegate {
        private HttpRequestBase request;
        private Object contentType;
        private String requestContentType;
        private Map<Object, Closure> responseHandlers = new StringHashMap<Closure>();
        private URIBuilder uri;
        private Map<Object, Object> headers = new StringHashMap<Object>();
        private HttpContextDecorator context = new HttpContextDecorator();

        public RequestConfigDelegate(HttpRequestBase request, Object contentType,
                                     Map<?, ?> defaultRequestHeaders,
                                     Map<?, Closure> defaultResponseHandlers) {
            if (request == null) throw new IllegalArgumentException(
                    "Internal error - HttpRequest instance cannot be null");
            this.request = request;
            this.headers.putAll(defaultRequestHeaders);
            this.contentType = contentType;
            if (defaultRequestContentType != null)
                this.requestContentType = defaultRequestContentType.toString();
            this.responseHandlers.putAll(defaultResponseHandlers);
            URI uri = request.getURI();
            if (uri != null) this.uri = new URIBuilder(uri, urlEncodingEnabled, encoderConfig);
        }

        public RequestConfigDelegate(Map<String, ?> args, HttpRequestBase request, Closure successHandler)
                throws URISyntaxException {
            this(request, defaultContentType, defaultRequestHeaders, defaultResponseHandlers);
            if (successHandler != null)
                this.responseHandlers.put(Status.SUCCESS.toString(), successHandler);
            setPropertiesFromMap(args);
        }

        /**
         * Use this object to manipulate parts of the request URI, like
         * query params and request path.  Example:
         *

         * builder.request(GET,XML) {
         *   uri.path = '../other/request.jsp'
         *   uri.query = [p1:1, p2:2]
         *   ...
         * }

         *

         *
This method signature returns Object so that the
         * complementary {@link #setUri(Object)} method can accept various
         * types.

         *
         * @return {@link URIBuilder} to manipulate the request URI
         */
        public URIBuilder getUri() {
            return this.uri;
        }

        /**
         *
Set the entire URI to be used for this request.  Acceptable
         * parameter types are:
         *

         *
URL

         *
URI

         *
URIBuilder

         *

         * Any other parameter type will be assumed that its
         * toString() method produces a valid URI.

         *

         *
Note that if you want to change just a portion of the request URI,
         * (e.g. the host, port, path, etc.) you can call {@link #getUri()}
         * which will return a {@link URIBuilder} which can manipulate portions
         * of the request URI.

         *
         * @param uri the URI to use for this request.
         * @throws URISyntaxException if an argument is given that is not a valid URI
         * @see URIBuilder#convertToURI(Object)
         */
        public void setUri(Object uri) throws URISyntaxException {
            if (uri instanceof URIBuilder) this.uri = (URIBuilder) uri;
            this.uri = new URIBuilder(URIBuilder.convertToURI(uri), urlEncodingEnabled, encoderConfig);
        }

        /**
         * Directly access the Apache HttpClient instance that will
         * be used to execute this request.
         *
         * @see HttpRequestBase
         */
        protected HttpRequestBase getRequest() {
            return this.request;
        }

        /**
         * Get the content-type of any data sent in the request body and the
         * expected response content-type.  If the request content-type is
         * expected to differ from the response content-type (i.e. a URL-encoded
         * POST that should return an HTML page) then this value will be used
         * for the response content-type, while
         * {@link #setRequestContentType(String)} should be used for the request.
         *
         * @return whatever value was assigned via {@link #setContentType(Object)}
         * or passed from the {@link HTTPBuilder#defaultContentType} when this
         * RequestConfigDelegate instance was constructed.
         */
        protected Object getContentType() {
            return this.contentType;
        }

        /**
         * Set the content-type used for any data in the request body, as well
         * as the Accept content-type that will be used for parsing
         * the response. The value should be either a {@link ContentType} value
         * or a String, i.e. "text/plain".  This will default to
         * {@link HTTPBuilder#getContentType()} for requests that do not
         * explicitly pass a contentType parameter (such as
         * {@link HTTPBuilder#request(HttpRequestFactory, Object, Closure)}).
         *
         * @param ct the value that will be used for the Content-Type
         *           and Accept request headers.
         */
        protected void setContentType(Object ct) {
            if (ct == null) this.contentType = defaultContentType;
            else this.contentType = ct;
        }

        /**
         * The request content-type, if different from the {@link #contentType}.
         *
         * @return
         */
        protected String getRequestContentType() {
            if (this.requestContentType != null) return this.requestContentType;
            else return this.getContentType().toString();
        }

        /**
         *
Assign a different content-type for the request than is expected for
         * the response.  This is useful if i.e. you want to post URL-encoded
         * form data but expect the response to be XML or HTML.  The
         * {@link #getContentType()} will always control the Accept
         * header, and will be used for the request content unless this
         * value is also explicitly set.

         *
Note that this method is used internally; calls within a request
         * configuration closure should call {@link #send(Object, Object)}
         * to set the request body and content-type at the same time.

         *
         * @param ct either a {@link ContentType} value or a valid content-type
         *           String.
         */
        protected void setRequestContentType(String ct) {
            this.requestContentType = ct;
        }

        /**
         * Valid arguments:
         *

         *
uri

Either a URI, URL, or object whose
         * toString() method produces a valid URI string.
         * If this parameter is not supplied, the HTTPBuilder's default
         * URI is used.

         *
path

Request path that is merged with the URI

         *
query

Map of URL query parameters

         *
headers

Map of HTTP headers

         *
contentType

Request content type and Accept header.
         * If not supplied, the HTTPBuilder's default content-type is used.

         *
requestContentType

content type for the request, if it
         * is different from the expected response content-type

         *
body

Request body that will be encoded based on the given contentType

         *

         *
         * @param args named parameters to set properties on this delegate.
         * @throws URISyntaxException if the uri argument does not represent a valid URI
         */
        @SuppressWarnings("unchecked")
        protected void setPropertiesFromMap(Map<String, ?> args) throws URISyntaxException {
            if (args == null) return;
            if (args.get("url") != null) throw new IllegalArgumentException(
                    "The 'url' parameter is deprecated; use 'uri' instead");
            Object uri = args.get("uri");
            if (uri == null) uri = defaultURI;
            if (uri == null) throw new IllegalStateException(
                    "Default URI is null, and no 'uri' parameter was given");
            this.uri = new URIBuilder(URIBuilder.convertToURI(uri), urlEncodingEnabled, encoderConfig);

            Map query = (Map) args.get("params");
            if (query != null) {
                log.debug("'params' argument is deprecated; use 'query' instead.");
                this.uri.setQuery(query);
            }
            query = (Map) args.get("query");
            if (query != null) this.uri.setQuery(query);
            Map headers = (Map) args.get("headers");
            if (headers != null) this.getHeaders().putAll(headers);

            Object path = args.get("path");
            if (path != null) this.uri.setPath(path.toString());

            Object contentType = args.get("contentType");
            if (contentType != null) this.setContentType(contentType);

            contentType = args.get("requestContentType");
            if (contentType != null) this.setRequestContentType(contentType.toString());

            Object body = args.get("body");
            if (body != null) this.setBody(this.getRequestContentType(), body);
        }

        /**
         * Set request headers.  These values will be merged
         * with any {@link HTTPBuilder#getHeaders() default request headers.}
         * (The assumption is you'll probably want to add a bunch of headers to
         * whatever defaults you've already set).  If you only want to
         * use values set here, simply call {@link #getHeaders() headers.clear()}
         * first.
         */
        public void setHeaders(Map<?, ?> newHeaders) {
            this.headers.putAll(newHeaders);
        }

        /**
         *
Get request headers (including any default headers set on this
         * {@link HTTPBuilder#setHeaders(Map) HTTPBuilder instance}).  Note that
         * this will not include any Accept, Content-Type,
         * or Content-Encoding headers that are automatically
         * handled by any encoder or parsers in effect.  Note that any values
         * set here will override any of those automatically assigned
         * values.

         *

         *
Example: headers.'Accept-Language' = 'en, en-gb;q=0.8'

         *
         * @return a map of HTTP headers that will be sent in the request.
         */
        public Map<?, ?> getHeaders() {
            return this.headers;
        }

        /**
         * Convenience method to set a request content-type at the same time
         * the request body is set.  This is a variation of
         * {@link #setBody(Object, Object)} that allows for a different content-type
         * than what is expected for the response.
         *

         *
Example:
         *

         * http.request(POST,HTML) {
         *
         *   /* request data is interpreted as a JsonBuilder closure by
         *      HTTPBuilder's default EncoderRegistry implementation * /
         *   send( 'text/javascript' ) {
         *     a : ['one','two','three']
         *   }
         *
         *   // response content-type is what was specified in the outer request() argument:
         *   response.success = { resp, html ->
         *
         *   }
         * }
         *

         * The send call is equivalent to the following:
         *

         *   requestContentType = 'text/javascript'
         *   body = { a : ['one','two','three'] }
         *

         *
         * @param contentType either a {@link ContentType} or equivalent
         *                    content-type string like "text/xml"
         * @param requestBody
         */
        public void send(Object contentType, Object requestBody) {
            this.setRequestContentType(contentType.toString());
            this.setBody(requestBody);
        }

        private void setBody(Object requestBody) {
            setBody(getRequestContentType(), requestBody);
        }

        /**
         * Set the request body.  This value may be of any type supported by
         * the associated {@link EncoderRegistry request encoder}.  That is,
         * the value of body will be interpreted by the encoder
         * associated with the current {@link #getRequestContentType() request
         * content-type}.
         *
         * @param body data or closure interpreted as the request body
         * @see #send(Object, Object)
         */
        public void setBody(Object requestContentType, Object body) {
            if (!(request instanceof HttpEntityEnclosingRequest))
                throw new IllegalArgumentException(
                        "Cannot set a request body for a " + request.getMethod() + " method");
            Closure encoder = encoders.getAt(requestContentType);
            HttpEntity entity = (HttpEntity) encoder.call(requestContentType, body);

            ((HttpEntityEnclosingRequest) this.request).setEntity(entity);
        }

        /**
         * Get the proper response handler for the response code.  This is called
         * by the {@link HTTPBuilder} class in order to find the proper handler
         * based on the response status code.
         *
         * @param statusCode HTTP response status code
         * @return the response handler
         */
        protected Closure findResponseHandler(int statusCode) {
            Closure handler = this.getResponse().get(Integer.toString(statusCode));
            if (handler == null) handler =
                    this.getResponse().get(Status.find(statusCode).toString());
            return handler;
        }

        /**
         * Access the response handler map to set response parsing logic.
         * i.e.

         * builder.request( GET, XML ) {
         *   response.success = { xml ->
         *      /* for XML content type, the default parser
         *         will return an XmlSlurper * /
         *   	xml.root.children().each { println it }
         *   }
         * }

         *
         * @return
         */
        public Map<Object, Closure> getResponse() {
            return this.responseHandlers;
        }

        /**
         * Get the {@link HttpContext} that will be used for this request.  By
         * default, a new context is created for each request.
         *
         * @return
         * @see ClientContext
         */
        public HttpContextDecorator getContext() {
            return this.context;
        }

        /**
         * Set the {@link HttpContext} that will be used for this request.
         *
         * @param ctx
         */
        public void setContext(HttpContext ctx) {
            this.context = new HttpContextDecorator(ctx);
        }
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.restassured.builder;

import io.restassured.config.EncoderConfig;
import io.restassured.config.ObjectMapperConfig;
import io.restassured.internal.mapping.ObjectMapperSerializationContextImpl;
import io.restassured.internal.mapping.ObjectMapping;
import io.restassured.internal.multipart.MultiPartSpecificationImpl;
import io.restassured.mapper.ObjectMapper;
import io.restassured.mapper.ObjectMapperType;
import io.restassured.specification.MultiPartSpecification;
import org.apache.commons.lang3.Validate;

import java.io.File;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Builder for creating more advanced multi-part requests.
 *

 * Usage example:
 *

 * File myFile = ..
 * given().multiPart(new MultiPartSpecBuilder(myFile).with().fileName("some-name.txt").and().with().mimeType("application/vnd.mycompany+text").build()). ..
 *

 */
public class MultiPartSpecBuilder {

    private final ObjectMapper explicitObjectMapper;
    private final ObjectMapperType explicitObjectMapperType;
    private Object content;
    private String controlName;
    private String mimeType;
    private String charset;
    private String fileName;
    private Map<String, String> headers;
    private boolean isControlNameExplicit;
    private boolean isFileNameExplicit;

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(Object content) {
        Validate.notNull(content, "Multi-part content cannot be null");
        this.content = content;
        this.controlName = "file";
        this.isControlNameExplicit = false;
        this.isFileNameExplicit = false;
        this.explicitObjectMapper = null;
        this.explicitObjectMapperType = null;
        this.headers = new TreeMap<String, String>();
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(Object content, ObjectMapperType objectMapperType) {
        Validate.notNull(content, "Multi-part content cannot be null");
        Validate.notNull(objectMapperType, "Object mapper type cannot be null");
        this.explicitObjectMapperType = objectMapperType;
        this.explicitObjectMapper = null;
        this.content = content;
        this.controlName = "file";
        this.isControlNameExplicit = false;
        this.isFileNameExplicit = false;
        this.headers = new TreeMap<String, String>();
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(Object content, ObjectMapper objectMapper) {
        Validate.notNull(content, "Multi-part content cannot be null");
        Validate.notNull(objectMapper, "Object mapper cannot be null");
        this.explicitObjectMapper = objectMapper;
        this.explicitObjectMapperType = null;
        this.content = content;
        this.controlName = "file";
        this.isControlNameExplicit = false;
        this.isFileNameExplicit = false;
        this.headers = new TreeMap<String, String>();
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(InputStream content) {
        this((Object) content);
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(String content) {
        this((Object) content);
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(byte[] content) {
        this((Object) content);
    }

    /**
     * Create a new multi-part specification with control name equal to file.
     *
     * @param content The content to include in the multi-part specification.
     */
    public MultiPartSpecBuilder(File content) {
        this((Object) content);
    }

    /**
     * Specify the control name of this multi-part.
     *
     * @param controlName The control name to use. Default is file.
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder controlName(String controlName) {
        Validate.notEmpty(controlName, "Control name cannot be empty");
        this.controlName = controlName;
        this.isControlNameExplicit = true;
        return this;
    }

    /**
     * Specify the file name of this multi-part. Note that this is only applicable for input streams, byte arrays and files
     * and not string content.
     *
     * @param fileName The file name to use.
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder fileName(String fileName) {
        this.fileName = fileName;
        this.isFileNameExplicit = true;
        return this;
    }

    /**
     * Add a header to this multipart specification.
     *
     * @param name The name of the header
     * @param value The value of the header
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder header(String name, String value) {
        Validate.notEmpty(name, "Header name cannot be empty");
        Validate.notEmpty(value, "Header value cannot be empty");

        // Replace previous header if exists
        final Set<String> headerNames = headers.keySet();
        final String trimmedName = name.trim();
        for (String headerName : headerNames) {
            if (headerName.equalsIgnoreCase(trimmedName)) {
                headers.remove(headerName);
            }
        }
        // Put the name header in the header list
        headers.put(name, value);
        return this;
    }

    /**
     * Set the headers for this multipart specification (replaces previous headers)
     *
     * @param headers The headers as a map
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder headers(Map<String, String> headers) {
        if (headers == null) {
            this.headers = new HashMap<String, String>();
        } else {
            this.headers = new HashMap<String, String>(headers);
        }
        return this;
    }

    /**
     * Specify the mime-type for this multi-part.
     *
     * @param mimeType The mime-type
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder mimeType(String mimeType) {
        Validate.notEmpty(mimeType, "Mime-type cannot be empty");
        this.mimeType = mimeType;
        return this;
    }

    /**
     * Specify the charset for this charset.
     *
     * @param charset The charset to use
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder charset(String charset) {
        Validate.notEmpty(charset, "Charset cannot be empty");
        if (content instanceof byte[] || content instanceof InputStream) {
            throw new IllegalArgumentException("Cannot specify charset input streams or byte arrays.");
        }
        this.charset = charset;
        return this;
    }

    /**
     * Just a method that can be used as syntactic sugar.
     *
     * @return The same instance of the MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder with() {
        return this;
    }

    /**
     * Just a method that can be used as syntactic sugar.
     *
     * @return The same instance of the MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder and() {
        return this;
    }

    /**
     * Specify the charset for this charset.
     *
     * @param charset The charset to use
     * @return An instance of MultiPartSpecBuilder
     */
    public MultiPartSpecBuilder charset(Charset charset) {
        Validate.notNull(charset, "Charset cannot be null");
        this.charset = charset.toString();
        return this;
    }

    /**
     * Set the filename of the multi-part to empty (none). This means that the "filename" part will be excluded in the multi-part request.
     *

     * This is the same as calling {@link #fileName(String)} with null.
     *

     *
     * @return An instance of MultiPartSpecBuilder
     * @see #fileName(String)
     */
    public MultiPartSpecBuilder emptyFileName() {
        return fileName(null);
    }

    public MultiPartSpecification build() {
        MultiPartSpecificationImpl spec = new MultiPartSpecificationImpl();
        spec.setCharset(charset);
        applyContentToSpec(spec);
        spec.setControlName(controlName);
        spec.setFileName(fileName);
        spec.setMimeType(mimeType);
        spec.setControlNameSpecifiedExplicitly(isControlNameExplicit);
        spec.setFileNameSpecifiedExplicitly(isFileNameExplicit);
        spec.setHeaders(headers);
        return spec;
    }

    private void applyContentToSpec(MultiPartSpecificationImpl spec) {
        final Object actualContent;
        if (explicitObjectMapper != null) {
            ObjectMapperSerializationContextImpl ctx = new ObjectMapperSerializationContextImpl();
            ctx.setObject(content);
            ctx.setContentType(mimeType);
            actualContent = explicitObjectMapper.serialize(ctx);
        } else if (explicitObjectMapperType != null) {
            actualContent = ObjectMapping.serialize(content, mimeType, null, explicitObjectMapperType, new ObjectMapperConfig(), new EncoderConfig());
        } else {
            actualContent = content;
        }
        spec.setContent(actualContent);
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.restassured.internal.http;

import io.restassured.config.DecoderConfig;
import io.restassured.internal.http.ContentEncoding.Type;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.AbstractHttpClient;

import java.util.HashMap;
import java.util.Map;

/**
 * Keeps track of available content-encoding handlers.
 *
 * @author Tom Nichols
 */
public class ContentEncodingRegistry {

    private final boolean useNoWrapForInflateDecoding;
    protected Map<String, ContentEncoding> availableEncoders;

    public ContentEncodingRegistry(DecoderConfig decoderConfig) {
        useNoWrapForInflateDecoding = (decoderConfig == null ? DecoderConfig.decoderConfig() : decoderConfig).shouldUseNoWrapForInflateDecoding();
        availableEncoders = getDefaultEncoders();
    }

    /**
     * This implementation adds a {@link GZIPEncoding} and {@link DeflateEncoding}
     * handler to the registry.  Override this method to provide a different set
     * of defaults.
     *
     * @return a map to content-encoding strings to {@link ContentEncoding} handlers.
     */
    protected Map<String, ContentEncoding> getDefaultEncoders() {
        Map<String, ContentEncoding> map = new HashMap<String, ContentEncoding>();
        map.put(Type.GZIP.toString(), new GZIPEncoding());
        map.put(Type.DEFLATE.toString(), new DeflateEncoding(useNoWrapForInflateDecoding));
        return map;
    }

    /**
     * Add the request and response interceptors to the {@link HttpClient},
     * which will provide transparent decoding of the given content-encoding
     * types.  This method is called by HTTPBuilder and probably should not need
     * be modified by sub-classes.
     *
     * @param client    client on which to set the request and response interceptors
     * @param encodings encoding name (either a {@link ContentEncoding.Type} or
     *                  a content-encoding string.
     */
    void setInterceptors(final AbstractHttpClient client, Object... encodings) {
        // remove any encoding interceptors that are already set
        client.removeRequestInterceptorByClass(ContentEncoding.RequestInterceptor.class);
        client.removeResponseInterceptorByClass(ContentEncoding.ResponseInterceptor.class);

        for (Object encName : encodings) {
            ContentEncoding enc = availableEncoders.get(encName.toString());
            if (enc == null) continue;
            client.addRequestInterceptor(enc.getRequestInterceptor());
            client.addResponseInterceptor(enc.getResponseInterceptor());
        }
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.restassured.internal.http;

import com.github.scribejava.core.builder.api.DefaultApi10a;
import com.github.scribejava.core.builder.api.DefaultApi20;
import com.github.scribejava.core.model.*;
import com.github.scribejava.core.oauth.OAuth10aService;
import com.github.scribejava.core.oauth.OAuth20Service;
import com.github.scribejava.core.oauth.OAuthService;
import io.restassured.authentication.OAuthSignature;
import io.restassured.internal.TrustAndKeystoreSpecImpl;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.EntityEnclosingRequestWrapper;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.util.List;
import java.util.Map;

/**
 * Encapsulates all configuration related to HTTP authentication methods.
 *
 * @author Tom Nichols
 * @author johanhaleby
 */
public class AuthConfig {
    private static final int UNDEFINED_PORT = -1;
    private static final int DEFAULT_HTTPS_PORT = 443;
    protected HTTPBuilder builder;
    private final io.restassured.config.OAuthConfig raOAuthConfig;

    public AuthConfig(HTTPBuilder builder, io.restassured.config.OAuthConfig restAssuredOAuthConfig) {
        this.builder = builder;
        this.raOAuthConfig = restAssuredOAuthConfig;
    }

    /**
     * Set authentication credentials to be used for the current
     * {@link HTTPBuilder#getUri() default host}.  This method name is a bit of
     * a misnomer, since these credentials will actually work for "digest"
     * authentication as well.
     *
     * @param user
     * @param pass
     */
    public void basic(String user, String pass) {
        URI uri = ((URIBuilder) builder.getUri()).toURI();
        if (uri == null) throw new IllegalStateException("a default URI must be set");
        this.basic(uri.getHost(), uri.getPort(), user, pass);
    }

    /**
     * Set authentication credentials to be used for the given host and port.
     *
     * @param host
     * @param port
     * @param user
     * @param pass
     */
    public void basic(String host, int port, String user, String pass) {
        builder.getClient().getCredentialsProvider().setCredentials(
                new AuthScope(host, port),
                new UsernamePasswordCredentials(user, pass)
        );
    }

    /**
     * Set NTLM authentication credentials to be used for the current
     * {@link HTTPBuilder#getUri() default host}.
     *
     * @param user
     * @param pass
     */
    public void ntlm(String user, String pass, String workstation, String domain) {
        URI uri = ((URIBuilder) builder.getUri()).toURI();
        if (uri == null) throw new IllegalStateException("a default URI must be set");
        this.ntlm(uri.getHost(), uri.getPort(), user, pass,workstation,domain);
    }
    /**
     * Set NTLM authentication credentials to be used for the given host and port.
     *
     * @param host
     * @param port
     * @param user
     * @param pass
     * @param workstation
     * @param domain
     */
    public void ntlm(String host, int port, String user, String pass, String workstation, String domain) {
        builder.getClient().getCredentialsProvider().setCredentials(
                new AuthScope(host, port),
                new NTCredentials(user, pass, workstation, domain)
        );
    }

    /**
     * Sets a certificate to be used for SSL authentication. See {@link Class#getResource(String)} for how to get a URL from a resource
     * on the classpath.
     *
     * @param keyStorePath               URL to a JKS keystore where the certificate is stored.
     * @param keyStorePassword           password to decrypt the keystore
     * @param keyStoreType               The certificate type
     * @param keyStore                   The key store
     * @param trustStorePath             URL to a trust store
     * @param trustStorePassword         password to decrypt the trust store
     * @param trustStoreType             The certificate type
     * @param trustStore                 The trust store
     * @param port                       The SSL port
     * @param hostnameVerifier           The X509HostnameVerifier to use
     * @param sslConnectionSocketFactory The SSLConnectionSocketFactory to use
     */
    public void certificate(Object keyStorePath, String keyStorePassword, String keyStoreType, KeyStore keyStore,
                            Object trustStorePath, String trustStorePassword, String trustStoreType, KeyStore trustStore,
                            int port, X509HostnameVerifier hostnameVerifier, SSLSocketFactory sslConnectionSocketFactory) {
        TrustAndKeystoreSpecImpl spec = new TrustAndKeystoreSpecImpl();
        URI uri = ((URIBuilder) builder.getUri()).toURI();
        if (uri == null) throw new IllegalStateException("a default URI must be set");
        spec.setKeyStoreType(keyStoreType);
        spec.setKeyStorePassword(keyStorePassword);
        spec.setKeyStorePath(keyStorePath);
        spec.setKeyStore(keyStore);
        spec.setTrustStoreType(trustStoreType);
        spec.setTrustStorePassword(trustStorePassword);
        spec.setTrustStorePath(trustStorePath);
        spec.setTrustStore(trustStore);
        spec.setPort(port);
        spec.setX509HostnameVerifier(hostnameVerifier);
        spec.setFactory(sslConnectionSocketFactory);

        int portSpecifiedInUri = uri.getPort();
        spec.apply(builder, portSpecifiedInUri == UNDEFINED_PORT ? DEFAULT_HTTPS_PORT : portSpecifiedInUri);
    }

    /**
     *
OAuth sign all requests.  Note that this currently does not
     * wait for a WWW-Authenticate challenge before sending the
     * the OAuth header.  All requests to all domains will be signed for this
     * instance.

     *

     *
This assumes you've already generated an accessToken and
     * secretToken for the site you're targeting.  For More information
     * on how to achieve this, see the
     * Scribe documentation.

     *
     * @param consumerKey    null if you want to unset
     *                       OAuth handling and stop signing requests.
     * @param consumerSecret
     * @param accessToken
     * @param secretToken
     * @since 0.5.1
     */
    public void oauth(String consumerKey, String consumerSecret,
                      String accessToken, String secretToken) {
        this.builder.client.removeRequestInterceptorByClass(OAuthSigner.class);
        if (consumerKey != null) {
            this.builder.client.addRequestInterceptor(new OAuthSigner(
                    consumerKey, consumerSecret, accessToken, secretToken, OAuthSignature.HEADER,
                    raOAuthConfig.shouldAddEmptyAccessOAuthTokenToBaseString()));
        }
    }

    public void oauth(String consumerKey, String consumerSecret,
                      String accessToken, String secretToken, OAuthSignature signature) {
        this.builder.client.removeRequestInterceptorByClass(OAuthSigner.class);
        if (consumerKey != null) {
            this.builder.client.addRequestInterceptor(new OAuthSigner(
                    consumerKey, consumerSecret, accessToken, secretToken,
                    signature, raOAuthConfig.shouldAddEmptyAccessOAuthTokenToBaseString()));
        }
    }

    /**
     *
OAuth2 sign all requests.  Note that this currently does not
     * wait for a WWW-Authenticate challenge before sending the
     * the OAuth header.  All requests to all domains will be signed for this
     * instance.

     *

     *
This assumes you've already generated an accessToken
     * for the site you're targeting.  For More information
     * on how to achieve this, see the
     * Scribe documentation.

     *
     * @param accessToken
     * @since 0.5.1
     */
    public void oauth2(String accessToken) {
        this.builder.client.removeRequestInterceptorByClass(OAuthSigner.class);
        if (accessToken != null) {
            this.builder.client.addRequestInterceptor(new OAuthSigner(accessToken, OAuthSignature.HEADER));
        }
    }

    public void oauth2(String accessToken, OAuthSignature signature) {
        this.builder.client.removeRequestInterceptorByClass(OAuthSigner.class);
        if (accessToken != null) {
            this.builder.client.addRequestInterceptor(new OAuthSigner(accessToken, signature));
        }
    }

    static class OAuthSigner implements HttpRequestInterceptor {
        protected OAuthConfig oauthConfig;
        protected Token token;
        protected OAuth10aService service;
        protected SignatureType type = SignatureType.Header;
        protected OAuthSignature signature;
        protected boolean isOAuth1 = true;
        protected boolean addEmptyTokenToBaseString;

        public OAuthSigner(String consumerKey, String consumerSecret,
                           String accessToken, String secretToken, OAuthSignature signature, Boolean addEmptyTokenToBaseString) {

            this.oauthConfig = new OAuthConfig(consumerKey, consumerSecret,
                    null, getOAuthSigntureType(signature), null, null, null, null, null, null, null);
            this.token = new OAuth1AccessToken(accessToken, secretToken);
            this.signature = signature;
            this.addEmptyTokenToBaseString = addEmptyTokenToBaseString;
        }

        public OAuthSigner(String accessToken, OAuthSignature signature) {
            this.token = new OAuth2AccessToken(accessToken, "");
            this.signature = signature;
            isOAuth1 = false;
        }

        public void process(HttpRequest request, HttpContext ctx) throws HttpException, IOException {
            try {
                HttpHost host = (HttpHost) ctx.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
                final URI requestURI = new URI(host.toURI()).resolve(request.getRequestLine().getUri());

                Verb verb = Verb.valueOf(request.getRequestLine().getMethod().toUpperCase());
                OAuthRequest oauthRequest = new OAuthRequest(verb, requestURI.toString(), null);
                this.service = (OAuth10aService) getOauthService(isOAuth1, addEmptyTokenToBaseString);

                if (request instanceof EntityEnclosingRequestWrapper) {
                    HttpEntity entity = ((EntityEnclosingRequestWrapper) request).getEntity();
                    if (entity != null) {
                        List<NameValuePair> params = URLEncodedUtils.parse(entity);
                        for (NameValuePair param : params) {
                            String value = param.getValue() == null ? "" : param.getValue();
                            oauthRequest.addBodyParameter(param.getName(), value);
                        }
                    }
                }

                service.signRequest((OAuth1AccessToken) token, oauthRequest);

                if (signature == OAuthSignature.HEADER) {
                    //If signature is to be added as header
                    for (Map.Entry<String, String> entry : oauthRequest.getHeaders().entrySet()) {
                        request.setHeader(entry.getKey(), entry.getValue());
                    }
                } else {
                    //If signature is to be added as query param
                    URI uri = new URI(oauthRequest.getCompleteUrl());
                    ((RequestWrapper) request).setURI(uri);
                }

            } catch (URISyntaxException ex) {
                throw new HttpException("Error rebuilding request URI", ex);
            }
        }

        private OAuthService getOauthService(boolean oauth1, final boolean useEmptyOAuthToken) {
            OAuthService service;
            if (oauth1) {
                DefaultApi10a api = new DefaultApi10a() {
                    @Override
                    public String getRequestTokenEndpoint() {
                        return null;
                    }

                    @Override
                    public String getAuthorizationUrl(OAuth1RequestToken arg0) {
                        return null;
                    }

                    @Override
                    public String getAccessTokenEndpoint() {
                        return null;
                    }

                    @Override
                    public boolean isEmptyOAuthTokenParamIsRequired() {
                        return useEmptyOAuthToken;
                    }
                };
                service = new OAuth10aService(api, oauthConfig);
            } else {
                DefaultApi20 api = new DefaultApi20() {
                    @Override
                    public String getAuthorizationUrl(OAuthConfig arg0) {
                        return null;
                    }

                    @Override
                    public String getAccessTokenEndpoint() {
                        return null;
                    }
                };
                service = new OAuth20Service(api, oauthConfig);
            }
            return service;
        }

        private static SignatureType getOAuthSigntureType(OAuthSignature signature) {
            SignatureType signatureType;
            if (signature == OAuthSignature.HEADER)
                signatureType = SignatureType.Header;
            else
                signatureType = SignatureType.QueryString;
            return signatureType;
        }

    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.restassured.internal.http;

import io.restassured.http.ContentType;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;

/**
 * @see ContentType
 * @author Tom Nichols
 */
public class HttpResponseContentTypeFinder {
    /**
     * Helper method to get the content-type string from the response
     * (no charset).
     * @param resp
     */
    public static String findContentType(HttpResponse resp) {
        Header contentTypeHeader = resp.getFirstHeader(HttpHeaders.CONTENT_TYPE);

        if ( contentTypeHeader == null )
            throw new IllegalArgumentException( "Response does not have a content-type header" );
        try {
            return contentTypeHeader.getValue();
        }
        catch ( RuntimeException ex ) {  // NPE or OOB Exceptions
            throw new IllegalArgumentException( "Could not parse content-type from response" );
        }
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.restassured.http;

import io.restassured.internal.MultiValueEntity;

import java.util.*;

import static io.restassured.internal.common.assertion.AssertParameter.notNull;

/**
 * Represents collection of headers
 */
public class Headers implements Iterable {

    private final MultiValueEntity headers;

    public Headers(Header... headers) {
        this(Arrays.asList(headers));
    }

    public Headers(List headers) {
        notNull(headers, "Headers");
        this.headers = new MultiValueEntity<>(headers);
    }

    /**
     * @return The size of the headers
     */
    public int size() {
        return headers.size();
    }

    /**
     * @return true if one or more headers are defined, false otherwise.
     */
    public boolean exist() {
        return headers.exist();
    }

    /**
     * See if a header with the given name exists
     *
     * @param headerName The name of the header to check
     * @return true if the header exists
     */
    public boolean hasHeaderWithName(String headerName) {
        return headers.hasEntityWithName(headerName);
    }

    /**
     * @return All headers as a list.
     */
    protected List list() {
        return headers.list();
    }

    /**
     * Get a single header with the supplied name. If there are several headers match the headerName then
     * the last one is returned.
     *
     * @param headerName The name of the header to find
     * @return The found header or null if no header was found.
     */
    public Header get(String headerName) {
        notNull(headerName, "Header name");
        return headers.get(headerName);
    }

    /**
     * Get a single header with the supplied name. If there are several headers match the headerName then
     * the last one is returned.
     *
     * @param headerName The name of the header to find
     * @return The found header value or null if no header was found.
     */
    public String getValue(String headerName) {
        return headers.getValue(headerName);
    }

    /**
     * Get all header values of the header with supplied name. If there's only one header matching the headerName then
     * a list with only that header value is returned.
     *
     * @param headerName The name of the header to find
     * @return The found header values or empty list if no header was found.
     */
    public List<String> getValues(String headerName) {
        return headers.getValues(headerName);
    }

    /**
     * Get all headers with the supplied name. If there's only one header matching the headerName then
     * a list with only that header is returned.
     *
     * @param headerName The name of the header to find
     * @return The found headers or empty list if no header was found.
     */
    public List getList(String headerName) {
        return headers.getList(headerName);
    }

    /**
     * @return Headers iterator
     */
    public Iterator iterator() {
        return headers.iterator();
    }

    /**
     * @return All headers as a list
     */
    public List asList() {
        return headers.asList();
    }

    /**
     * An alternative way to create a Headers object from the constructor.
     *
     * @param header            The header to be included
     * @param additionalHeaders Additional headers to be included (optional)
     * @return A new headers object containing the specified headers
     */
    public static Headers headers(Header header, Header... additionalHeaders) {
        notNull(header, "Header");
        final List headerList = new LinkedList();
        headerList.add(header);
        if (additionalHeaders != null) {
            Collections.addAll(headerList, additionalHeaders);
        }
        return new Headers(headerList);
    }

    @Override
    public String toString() {
        return headers.toString();
    }
}