package com.akamai.edgegrid.signer;

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.Builder;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;


/**
 * Library-agnostic representation of an HTTP request. This object is immutable, so you probably
 * want to build an instance using {@link RequestBuilder}. Extenders of
 * {@link AbstractEdgeGridRequestSigner} will need to build one of these as part of their
 * implementation.
 *
 * @author mgawinec@akamai.com
 * @author mmeyer@akamai.com
 */
public class Request implements Comparable<Request> {

    private final byte[] body;
    private final String method;
    private final URI uriWithQuery;
    private final Map<String, List<String>> headers;

    private Request(RequestBuilder b) {
        this.body = b.body;
        this.method = b.method;
        this.headers = b.headers;
        this.uriWithQuery = b.uriWithQuery;
    }

    /**
     * Returns a new builder. The returned builder is equivalent to the builder
     * generated by {@link RequestBuilder}.
     *
     * @return a fresh {@link RequestBuilder}
     */
    public static RequestBuilder builder() {
        return new RequestBuilder();
    }

    @Override
    public int compareTo(Request that) {
        return new CompareToBuilder()
                .append(this.body, that.body)
                .append(this.headers, that.headers)
                .append(this.method, that.method)
                .append(this.uriWithQuery, that.uriWithQuery)
                .build();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) return false;
        if (getClass() != o.getClass()) return false;
        final Request that = (Request) o;
        return compareTo(that) == 0;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.JSON_STYLE)
                .append("body", body)
                .append("headers", headers)
                .append("method", method)
                .append("uriWithQuery", uriWithQuery)
                .build();
    }

    byte[] getBody() {
        return body;
    }

    Map<String, List<String>> getHeaders() {
        return headers;
    }

    String getMethod() {
        return method;
    }

    URI getUriWithQuery() {
        return uriWithQuery;
    }

    /**
     * Creates a new builder. The returned builder is equivalent to the builder
     * generated by {@link Request#builder()}.
     */
    public static class RequestBuilder implements Builder<Request> {

        private byte[] body = new byte[]{};
        private Map<String, List<String>> headers = new HashMap<>();
        private String method;
        private URI uriWithQuery;

        /**
         * Sets a content of HTTP request body. If not set, body is empty by default.
         *
         * @param requestBody a request body, in bytes
         * @return reference back to this builder instance
         */
        public RequestBuilder body(byte[] requestBody) {
            Validate.notNull(body, "body cannot be blank");
            this.body = Arrays.copyOf(requestBody, requestBody.length);
            return this;
        }

        /**
         * <p>
         * Adds a single header for an HTTP request. This can be called multiple times to add as
         * many headers as needed.
         * </p>
         * <p>
         * <i><b>WARNING:</b>{@link #headers(Map)} will replace the headers set by this method with
         * an unmodifiable {@link Map}. {@link #headers(Map)} and {@link #header(String, String)}
         * should not be used in the same builder instance.</i>
         * </p>
         *
         * @param headerName a header name
         * @param value a header value
         * @return reference back to this builder instance
         */
        public RequestBuilder header(String headerName, String value) {
            Validate.notEmpty(headerName, "headerName cannot be empty");
            Validate.notEmpty(value, "value cannot be empty");
            List<String> values = headers.get(headerName);
            if (values == null) {
                values = new LinkedList<>();
                headers.put(headerName, values);
            }
            values.add(value);
            return this;
        }

        /**
         * <p>
         * Sets headers of HTTP request.
         * </p>
         * <p>
         * <i><b>WARNING:</b>This method sets an unmodifiable {@link Map} for the headers, so
         * subsequent calls to {@link #header(String, String)} will not work. {@link #headers(Map)}
         * and {@link #header(String, String)} should not be used in the same builder instance.</i>
         * </p>
         *
         * @param headers a {@link Map} of {@link List} of headers
         * @return reference back to this builder instance
         */
        public RequestBuilder headers(Map<String, List<String>> headers) {
            Validate.notNull(headers, "headers cannot be null");
            this.headers = Collections.unmodifiableMap(headers);
            return this;
        }

        /**
         * Sets HTTP method: GET, PUT, POST, DELETE. Mandatory to set.
         *
         * @param method an HTTP method
         * @return reference back to this builder instance
         */
        public RequestBuilder method(String method) {
            Validate.notBlank(method, "method cannot be blank");
            this.method = method;
            return this;
        }

        /**
         * Sets absolute URI of HTTP request including query string. Mandatory to set.
         *
         * @param uriWithQuery a {@link URI}
         * @return reference back to this builder instance
         */
        public RequestBuilder uriWithQuery(URI uriWithQuery) {
            Validate.notNull(uriWithQuery, "uriWithQuery cannot be blank");
            this.uriWithQuery = uriWithQuery;
            return this;
        }

        /**
         * Returns a newly-created immutable HTTP request.
         */
        @Override
        public Request build() {
            Validate.notNull(body, "body cannot be blank");
            Validate.notBlank(method, "method cannot be blank");
            Validate.notNull(uriWithQuery, "uriWithQuery cannot be blank");
            return new Request(this);
        }

    }

}

@@ -205,16 +205,42 @@
 til.Map;
+%0Aimport java.util.Objects;
 %0A%0Aimport
@@ -2078,32 +2078,147 @@
 t) == 0;%0A    %7D%0A%0A
+    @Override%0A    public int hashCode() %7B%0A        return Objects.hash(body, headers, method, uriWithQuery);%0A    %7D%0A%0A
     @Override%0A  

Add Request#hashCode()