chromium/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlResponse.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net.test;

import org.chromium.net.UrlResponseInfo;

import java.io.UnsupportedEncodingException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

// TODO(kirchman): Update this to explain inter-class usage once other classes land.
/**
 *
 * Fake response model for UrlRequest used by Fake Cronet.
 */
public class FakeUrlResponse {
    private final int mHttpStatusCode;
    // Entries to mAllHeadersList should never be mutated.
    private final List<Map.Entry<String, String>> mAllHeadersList;
    private final boolean mWasCached;
    private final String mNegotiatedProtocol;
    private final String mProxyServer;
    private final byte[] mResponseBody;

    private static <T extends Object> T getNullableOrDefault(T nullableObject, T defaultObject) {
        if (nullableObject != null) {
            return nullableObject;
        }
        return defaultObject;
    }

    /**
     * Constructs a {@link FakeUrlResponse} from a {@link FakeUrlResponse.Builder}.
     * @param builder the {@link FakeUrlResponse.Builder} to create the response from
     */
    private FakeUrlResponse(Builder builder) {
        mHttpStatusCode = builder.mHttpStatusCode;
        mAllHeadersList = Collections.unmodifiableList(new ArrayList<>(builder.mAllHeadersList));
        mWasCached = builder.mWasCached;
        mNegotiatedProtocol = builder.mNegotiatedProtocol;
        mProxyServer = builder.mProxyServer;
        mResponseBody = builder.mResponseBody;
    }

    /**
     * Constructs a {@link FakeUrlResponse} from a {@link UrlResponseInfo}. All nullable fields in
     * the {@link UrlResponseInfo} are initialized to the default value if the provided value is
     * null.
     *
     * @param info the {@link UrlResponseInfo} used to initialize this object's fields
     */
    public FakeUrlResponse(UrlResponseInfo info) {
        mHttpStatusCode = info.getHttpStatusCode();
        mAllHeadersList = Collections.unmodifiableList(new ArrayList<>(info.getAllHeadersAsList()));
        mWasCached = info.wasCached();
        mNegotiatedProtocol =
                getNullableOrDefault(
                        info.getNegotiatedProtocol(), Builder.DEFAULT_NEGOTIATED_PROTOCOL);
        mProxyServer = getNullableOrDefault(info.getProxyServer(), Builder.DEFAULT_PROXY_SERVER);
        mResponseBody = Builder.DEFAULT_RESPONSE_BODY;
    }

    /** Builds a {@link FakeUrlResponse}. */
    public static class Builder {
        private static final int DEFAULT_HTTP_STATUS_CODE = 200;
        private static final List<Map.Entry<String, String>> INTERNAL_INITIAL_HEADERS_LIST =
                new ArrayList<>();
        private static final boolean DEFAULT_WAS_CACHED = false;
        private static final String DEFAULT_NEGOTIATED_PROTOCOL = "";
        private static final String DEFAULT_PROXY_SERVER = "";
        private static final byte[] DEFAULT_RESPONSE_BODY = new byte[0];

        private int mHttpStatusCode = DEFAULT_HTTP_STATUS_CODE;
        // Entries to mAllHeadersList should never be mutated.
        private List<Map.Entry<String, String>> mAllHeadersList =
                new ArrayList<>(INTERNAL_INITIAL_HEADERS_LIST);
        private boolean mWasCached = DEFAULT_WAS_CACHED;
        private String mNegotiatedProtocol = DEFAULT_NEGOTIATED_PROTOCOL;
        private String mProxyServer = DEFAULT_PROXY_SERVER;
        private byte[] mResponseBody = DEFAULT_RESPONSE_BODY;

        /** Constructs a {@link FakeUrlResponse.Builder} with the default parameters. */
        public Builder() {}

        /**
         * Constructs a {@link FakeUrlResponse.Builder} from a source {@link FakeUrlResponse}.
         *
         * @param source a {@link FakeUrlResponse} to copy into this {@link FakeUrlResponse.Builder}
         */
        private Builder(FakeUrlResponse source) {
            mHttpStatusCode = source.getHttpStatusCode();
            mAllHeadersList = new ArrayList<>(source.getAllHeadersList());
            mWasCached = source.getWasCached();
            mNegotiatedProtocol = source.getNegotiatedProtocol();
            mProxyServer = source.getProxyServer();
            mResponseBody = source.getResponseBody();
        }

        /**
         * Sets the HTTP status code. The default value is 200.
         *
         * @param httpStatusCode for {@link UrlResponseInfo.getHttpStatusCode()}
         * @return the builder with the corresponding HTTP status code set
         */
        public Builder setHttpStatusCode(int httpStatusCode) {
            mHttpStatusCode = httpStatusCode;
            return this;
        }

        /**
         * Adds a response header to built {@link FakeUrlResponse}s.
         *
         * @param name  the name of the header key, for example, "location" for a redirect header
         * @param value the header value
         * @return the builder with the corresponding header set
         */
        public Builder addHeader(String name, String value) {
            mAllHeadersList.add(new AbstractMap.SimpleEntry<>(name, value));
            return this;
        }

        /**
         * Sets result of {@link UrlResponseInfo.wasCached()}. The default wasCached value is false.
         *
         * @param wasCached for {@link UrlResponseInfo.wasCached()}
         * @return the builder with the corresponding wasCached field set
         */
        public Builder setWasCached(boolean wasCached) {
            mWasCached = wasCached;
            return this;
        }

        /**
         * Sets result of {@link UrlResponseInfo.getNegotiatedProtocol()}. The default negotiated
         * protocol is an empty string.
         *
         * @param negotiatedProtocol for {@link UrlResponseInfo.getNegotiatedProtocol()}
         * @return the builder with the corresponding negotiatedProtocol field set
         */
        public Builder setNegotiatedProtocol(String negotiatedProtocol) {
            mNegotiatedProtocol = negotiatedProtocol;
            return this;
        }

        /**
         * Sets result of {@link UrlResponseInfo.getProxyServer()}. The default proxy server is an
         * empty string.
         *
         * @param proxyServer for {@link UrlResponseInfo.getProxyServer()}
         * @return the builder with the corresponding proxyServer field set
         */
        public Builder setProxyServer(String proxyServer) {
            mProxyServer = proxyServer;
            return this;
        }

        /**
         * Sets the response body for a response. The default response body is an empty byte array.
         *
         * @param body all the information the server returns
         * @return the builder with the corresponding responseBody field set
         */
        public Builder setResponseBody(byte[] body) {
            mResponseBody = body;
            return this;
        }

        /**
         * Constructs a {@link FakeUrlResponse} from this {@link FakeUrlResponse.Builder}.
         *
         * @return a FakeUrlResponse with all fields set according to this builder
         */
        public FakeUrlResponse build() {
            return new FakeUrlResponse(this);
        }
    }

    /**
     * Returns the HTTP status code.
     *
     * @return the HTTP status code.
     */
    int getHttpStatusCode() {
        return mHttpStatusCode;
    }

    /**
     * Returns an unmodifiable list of the response header key and value pairs.
     *
     * @return an unmodifiable list of response header key and value pairs
     */
    List<Map.Entry<String, String>> getAllHeadersList() {
        return mAllHeadersList;
    }

    /**
     * Returns the wasCached value for this response.
     *
     * @return the wasCached value for this response
     */
    boolean getWasCached() {
        return mWasCached;
    }

    /**
     * Returns the protocol (for example 'quic/1+spdy/3') negotiated with the server.
     *
     * @return the protocol negotiated with the server
     */
    String getNegotiatedProtocol() {
        return mNegotiatedProtocol;
    }

    /**
     * Returns the proxy server that was used for the request.
     *
     * @return the proxy server that was used for the request
     */
    String getProxyServer() {
        return mProxyServer;
    }

    /**
     * Returns the body of the response as a byte array. Used for {@link UrlRequest.Callback}
     * {@code read()} callback.
     *
     * @return the response body
     */
    byte[] getResponseBody() {
        return mResponseBody;
    }

    /**
     * Returns a mutable builder representation of this {@link FakeUrlResponse}
     *
     * @return a {@link FakeUrlResponse.Builder} with all fields copied from this instance.
     */
    public Builder toBuilder() {
        return new Builder(this);
    }

    @Override
    public boolean equals(Object otherObj) {
        if (!(otherObj instanceof FakeUrlResponse)) {
            return false;
        }
        FakeUrlResponse other = (FakeUrlResponse) otherObj;
        return (mHttpStatusCode == other.mHttpStatusCode
                && mAllHeadersList.equals(other.mAllHeadersList)
                && mWasCached == other.mWasCached
                && mNegotiatedProtocol.equals(other.mNegotiatedProtocol)
                && mProxyServer.equals(other.mProxyServer)
                && Arrays.equals(mResponseBody, other.mResponseBody));
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mHttpStatusCode,
                mAllHeadersList,
                mWasCached,
                mNegotiatedProtocol,
                mProxyServer,
                Arrays.hashCode(mResponseBody));
    }

    @Override
    public String toString() {
        StringBuilder outputString = new StringBuilder();
        outputString.append("HTTP Status Code: " + mHttpStatusCode);
        outputString.append(" Headers: " + mAllHeadersList.toString());
        outputString.append(" Was Cached: " + mWasCached);
        outputString.append(" Negotiated Protocol: " + mNegotiatedProtocol);
        outputString.append(" Proxy Server: " + mProxyServer);
        outputString.append(" Response Body ");
        try {
            String bodyString = new String(mResponseBody, "UTF-8");
            outputString.append("(UTF-8): " + bodyString);
        } catch (UnsupportedEncodingException e) {
            outputString.append("(hexadecimal): " + getHexStringFromBytes(mResponseBody));
        }
        return outputString.toString();
    }

    private String getHexStringFromBytes(byte[] bytes) {
        StringBuilder bytesToHexStringBuilder = new StringBuilder();
        for (byte b : mResponseBody) {
            bytesToHexStringBuilder.append(String.format("%02x", b));
        }
        return bytesToHexStringBuilder.toString();
    }
}