<|start_filename|>httpizza/src/main/java/sexy/code/ResponseBody.java<|end_filename|>
package sexy.code;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.net.HttpURLConnection;

public class ResponseBody implements Closeable {

    private final String contentType;

    private final long contentLength;

    private final BufferedInputStream inputStream;

    private HttpURLConnection connection;

    ResponseBody(final String contentType, final long contentLength, final BufferedInputStream inputStream, HttpURLConnection connection) {
        this.contentType = contentType;
        this.contentLength = contentLength;
        this.inputStream = inputStream;
        this.connection = connection;
    }

    @Override
    public void close() {
        Util.closeQuietly(inputStream);
        connection.disconnect();
        connection = null;
    }

    public String contentType() {
        return contentType;
    }

    public BufferedInputStream byteStream() {
        return inputStream;
    }

    public long contentLength() {
        return contentLength;
    }

    public byte[] bytes() throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            byte[] buffer = new byte[1024];
            int n = 0;
            while (-1 != (n = inputStream.read(buffer))) {
                os.write(buffer, 0, n);
            }
            return os.toByteArray();
        } finally {
            Util.closeQuietly(os);
            Util.closeQuietly(this);
        }
    }

    public final String string() throws IOException {
        return new String(bytes(), Util.UTF_8);
    }
}


<|start_filename|>httpizza/src/test/java/sexy/code/UrlComponentEncodingTester.java<|end_filename|>
/*
 * Copyright (C) 2015 Square, Inc.
 *
 * 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 sexy.code;

import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.junit.Assert.fail;

/**
 * Tests how each code point is encoded and decoded in the context of each URL component.
 */
class UrlComponentEncodingTester {

    private static final int UNICODE_2 = 0x07ff; // Arbitrary code point that's 2 bytes in UTF-8.

    private static final int UNICODE_3 = 0xffff; // Arbitrary code point that's 3 bytes in UTF-8.

    private static final int UNICODE_4 = 0x10ffff; // Arbitrary code point that's 4 bytes in UTF-8.

    /**
     * The default encode set for the ASCII range. The specific rules vary per-component: for example,
     * '?' may be identity-encoded in a fragment, but must be percent-encoded in a path.
     * <p/>
     * See https://url.spec.whatwg.org/#percent-encoded-bytes
     */
    private static final Map<Integer, Encoding> defaultEncodings;

    static {
        Map<Integer, Encoding> map = new LinkedHashMap<>();
        map.put(0x0, Encoding.PERCENT); // Null character
        map.put(0x1, Encoding.PERCENT); // Start of Header
        map.put(0x2, Encoding.PERCENT); // Start of Text
        map.put(0x3, Encoding.PERCENT); // End of Text
        map.put(0x4, Encoding.PERCENT); // End of Transmission
        map.put(0x5, Encoding.PERCENT); // Enquiry
        map.put(0x6, Encoding.PERCENT); // Acknowledgment
        map.put(0x7, Encoding.PERCENT); // Bell
        map.put((int) '\b', Encoding.PERCENT); // Backspace
        map.put((int) '\t', Encoding.SKIP);    // Horizontal Tab
        map.put((int) '\n', Encoding.SKIP);    // Line feed
        map.put(0xb, Encoding.PERCENT); // Vertical Tab
        map.put((int) '\f', Encoding.SKIP);    // Form feed
        map.put((int) '\r', Encoding.SKIP);    // Carriage return
        map.put(0xe, Encoding.PERCENT); // Shift Out
        map.put(0xf, Encoding.PERCENT); // Shift In
        map.put(0x10, Encoding.PERCENT); // Data Link Escape
        map.put(0x11, Encoding.PERCENT); // Device Control 1 (oft. XON)
        map.put(0x12, Encoding.PERCENT); // Device Control 2
        map.put(0x13, Encoding.PERCENT); // Device Control 3 (oft. XOFF)
        map.put(0x14, Encoding.PERCENT); // Device Control 4
        map.put(0x15, Encoding.PERCENT); // Negative Acknowledgment
        map.put(0x16, Encoding.PERCENT); // Synchronous idle
        map.put(0x17, Encoding.PERCENT); // End of Transmission Block
        map.put(0x18, Encoding.PERCENT); // Cancel
        map.put(0x19, Encoding.PERCENT); // End of Medium
        map.put(0x1a, Encoding.PERCENT); // Substitute
        map.put(0x1b, Encoding.PERCENT); // Escape
        map.put(0x1c, Encoding.PERCENT); // File Separator
        map.put(0x1d, Encoding.PERCENT); // Group Separator
        map.put(0x1e, Encoding.PERCENT); // Record Separator
        map.put(0x1f, Encoding.PERCENT); // Unit Separator
        map.put((int) ' ', Encoding.PERCENT);
        map.put((int) '!', Encoding.IDENTITY);
        map.put((int) '"', Encoding.PERCENT);
        map.put((int) '#', Encoding.PERCENT);
        map.put((int) '$', Encoding.IDENTITY);
        map.put((int) '%', Encoding.IDENTITY);
        map.put((int) '&', Encoding.IDENTITY);
        map.put((int) '\'', Encoding.IDENTITY);
        map.put((int) '(', Encoding.IDENTITY);
        map.put((int) ')', Encoding.IDENTITY);
        map.put((int) '*', Encoding.IDENTITY);
        map.put((int) '+', Encoding.IDENTITY);
        map.put((int) ',', Encoding.IDENTITY);
        map.put((int) '-', Encoding.IDENTITY);
        map.put((int) '.', Encoding.IDENTITY);
        map.put((int) '/', Encoding.IDENTITY);
        map.put((int) '0', Encoding.IDENTITY);
        map.put((int) '1', Encoding.IDENTITY);
        map.put((int) '2', Encoding.IDENTITY);
        map.put((int) '3', Encoding.IDENTITY);
        map.put((int) '4', Encoding.IDENTITY);
        map.put((int) '5', Encoding.IDENTITY);
        map.put((int) '6', Encoding.IDENTITY);
        map.put((int) '7', Encoding.IDENTITY);
        map.put((int) '8', Encoding.IDENTITY);
        map.put((int) '9', Encoding.IDENTITY);
        map.put((int) ':', Encoding.IDENTITY);
        map.put((int) ';', Encoding.IDENTITY);
        map.put((int) '<', Encoding.PERCENT);
        map.put((int) '=', Encoding.IDENTITY);
        map.put((int) '>', Encoding.PERCENT);
        map.put((int) '?', Encoding.PERCENT);
        map.put((int) '@', Encoding.IDENTITY);
        map.put((int) 'A', Encoding.IDENTITY);
        map.put((int) 'B', Encoding.IDENTITY);
        map.put((int) 'C', Encoding.IDENTITY);
        map.put((int) 'D', Encoding.IDENTITY);
        map.put((int) 'E', Encoding.IDENTITY);
        map.put((int) 'F', Encoding.IDENTITY);
        map.put((int) 'G', Encoding.IDENTITY);
        map.put((int) 'H', Encoding.IDENTITY);
        map.put((int) 'I', Encoding.IDENTITY);
        map.put((int) 'J', Encoding.IDENTITY);
        map.put((int) 'K', Encoding.IDENTITY);
        map.put((int) 'L', Encoding.IDENTITY);
        map.put((int) 'M', Encoding.IDENTITY);
        map.put((int) 'N', Encoding.IDENTITY);
        map.put((int) 'O', Encoding.IDENTITY);
        map.put((int) 'P', Encoding.IDENTITY);
        map.put((int) 'Q', Encoding.IDENTITY);
        map.put((int) 'R', Encoding.IDENTITY);
        map.put((int) 'S', Encoding.IDENTITY);
        map.put((int) 'T', Encoding.IDENTITY);
        map.put((int) 'U', Encoding.IDENTITY);
        map.put((int) 'V', Encoding.IDENTITY);
        map.put((int) 'W', Encoding.IDENTITY);
        map.put((int) 'X', Encoding.IDENTITY);
        map.put((int) 'Y', Encoding.IDENTITY);
        map.put((int) 'Z', Encoding.IDENTITY);
        map.put((int) '[', Encoding.IDENTITY);
        map.put((int) '\\', Encoding.IDENTITY);
        map.put((int) ']', Encoding.IDENTITY);
        map.put((int) '^', Encoding.IDENTITY);
        map.put((int) '_', Encoding.IDENTITY);
        map.put((int) '`', Encoding.PERCENT);
        map.put((int) 'a', Encoding.IDENTITY);
        map.put((int) 'b', Encoding.IDENTITY);
        map.put((int) 'c', Encoding.IDENTITY);
        map.put((int) 'd', Encoding.IDENTITY);
        map.put((int) 'e', Encoding.IDENTITY);
        map.put((int) 'f', Encoding.IDENTITY);
        map.put((int) 'g', Encoding.IDENTITY);
        map.put((int) 'h', Encoding.IDENTITY);
        map.put((int) 'i', Encoding.IDENTITY);
        map.put((int) 'j', Encoding.IDENTITY);
        map.put((int) 'k', Encoding.IDENTITY);
        map.put((int) 'l', Encoding.IDENTITY);
        map.put((int) 'm', Encoding.IDENTITY);
        map.put((int) 'n', Encoding.IDENTITY);
        map.put((int) 'o', Encoding.IDENTITY);
        map.put((int) 'p', Encoding.IDENTITY);
        map.put((int) 'q', Encoding.IDENTITY);
        map.put((int) 'r', Encoding.IDENTITY);
        map.put((int) 's', Encoding.IDENTITY);
        map.put((int) 't', Encoding.IDENTITY);
        map.put((int) 'u', Encoding.IDENTITY);
        map.put((int) 'v', Encoding.IDENTITY);
        map.put((int) 'w', Encoding.IDENTITY);
        map.put((int) 'x', Encoding.IDENTITY);
        map.put((int) 'y', Encoding.IDENTITY);
        map.put((int) 'z', Encoding.IDENTITY);
        map.put((int) '{', Encoding.IDENTITY);
        map.put((int) '|', Encoding.IDENTITY);
        map.put((int) '}', Encoding.IDENTITY);
        map.put((int) '~', Encoding.IDENTITY);
        map.put(0x7f, Encoding.PERCENT); // Delete
        map.put(UNICODE_2, Encoding.PERCENT);
        map.put(UNICODE_3, Encoding.PERCENT);
        map.put(UNICODE_4, Encoding.PERCENT);
        defaultEncodings = Collections.unmodifiableMap(map);
    }

    private final Map<Integer, Encoding> encodings;

    private final StringBuilder uriEscapedCodePoints = new StringBuilder();

    public UrlComponentEncodingTester() {
        this.encodings = new LinkedHashMap<>(defaultEncodings);
    }

    public UrlComponentEncodingTester override(Encoding encoding, int... codePoints) {
        for (int codePoint : codePoints) {
            encodings.put(codePoint, encoding);
        }
        return this;
    }

    public UrlComponentEncodingTester identityForNonAscii() {
        encodings.put(UNICODE_2, Encoding.IDENTITY);
        encodings.put(UNICODE_3, Encoding.IDENTITY);
        encodings.put(UNICODE_4, Encoding.IDENTITY);
        return this;
    }

    /**
     * Configure a character to be skipped but only for conversion to and from {@code java.net.URI}.
     * That class is more strict than the others.
     */
    public UrlComponentEncodingTester skipForUri(int... codePoints) {
        uriEscapedCodePoints.append(new String(codePoints, 0, codePoints.length));
        return this;
    }

    public UrlComponentEncodingTester test(Component component) {
        for (Map.Entry<Integer, Encoding> entry : encodings.entrySet()) {
            Encoding encoding = entry.getValue();
            int codePoint = entry.getKey();
            testEncodeAndDecode(codePoint, component);
            if (encoding == Encoding.SKIP) {
                continue;
            }

            testParseOriginal(codePoint, encoding, component);
            testParseAlreadyEncoded(codePoint, encoding, component);
            testToUrl(codePoint, encoding, component);
            testFromUrl(codePoint, encoding, component);

            if (codePoint != '%') {
                boolean uriEscaped = uriEscapedCodePoints.indexOf(
                        Encoding.IDENTITY.encode(codePoint)) != -1;
                testUri(codePoint, encoding, component, uriEscaped);
            }
        }
        return this;
    }

    private void testParseAlreadyEncoded(int codePoint, Encoding encoding, Component component) {
        String encoded = encoding.encode(codePoint);
        String urlString = component.urlString(encoded);
        HttpUrl url = HttpUrl.parse(urlString);
        if (!component.encodedValue(url).equals(encoded)) {
            fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
        }
    }

    private void testEncodeAndDecode(int codePoint, Component component) {
        String expected = Encoding.IDENTITY.encode(codePoint);
        HttpUrl.Builder builder = HttpUrl.parse("http://host/").newBuilder();
        component.set(builder, expected);
        HttpUrl url = builder.build();
        String actual = component.get(url);
        if (!expected.equals(actual)) {
            fail(String.format("Roundtrip %s %#x %s", component, codePoint, url));
        }
    }

    private void testParseOriginal(int codePoint, Encoding encoding, Component component) {
        String encoded = encoding.encode(codePoint);
        if (encoding != Encoding.PERCENT) {
            return;
        }
        String identity = Encoding.IDENTITY.encode(codePoint);
        String urlString = component.urlString(identity);
        HttpUrl url = HttpUrl.parse(urlString);

        String s = component.encodedValue(url);
        if (!s.equals(encoded)) {
            fail(String.format("Encoding %s %#02x using %s", component, codePoint, encoding));
        }
    }

    private void testToUrl(int codePoint, Encoding encoding, Component component) {
        String encoded = encoding.encode(codePoint);
        HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
        URL javaNetUrl = httpUrl.url();
        if (!javaNetUrl.toString().equals(javaNetUrl.toString())) {
            fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
        }
    }

    private void testFromUrl(int codePoint, Encoding encoding, Component component) {
        String encoded = encoding.encode(codePoint);
        HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
        HttpUrl toAndFromJavaNetUrl = HttpUrl.get(httpUrl.url());
        if (!toAndFromJavaNetUrl.equals(httpUrl)) {
            fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
        }
    }

    private void testUri(
            int codePoint, Encoding encoding, Component component, boolean uriEscaped) {
        String string = new String(new int[]{codePoint}, 0, 1);
        String encoded = encoding.encode(codePoint);
        HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
        URI uri = httpUrl.uri();
        HttpUrl toAndFromUri = HttpUrl.get(uri);
        if (uriEscaped) {
            // The URI has more escaping than the HttpURL. Check that the decoded values still match.
            if (uri.toString().equals(httpUrl.toString())) {
                fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
            }
            if (!component.get(toAndFromUri).equals(string)) {
                fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
            }
        } else {
            // Check that the URI and HttpURL have the exact same escaping.
            if (!toAndFromUri.equals(httpUrl)) {
                fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
            }
            if (!uri.toString().equals(httpUrl.toString())) {
                fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
            }
        }
    }

    public enum Encoding {
        IDENTITY {
            public String encode(int codePoint) {
                return new String(new int[]{codePoint}, 0, 1);
            }
        },

        PERCENT {
            public String encode(int codePoint) {
                String str = IDENTITY.encode(codePoint);
                byte[] bytes = str.getBytes(Util.UTF_8);
                StringBuilder sb = new StringBuilder();
                for (byte b : bytes) {
                    sb.append(String.format("%%%02X", b));
                }
                return sb.toString();
            }
        },

        SKIP;

        public String encode(int codePoint) {
            throw new UnsupportedOperationException();
        }
    }

    public enum Component {
        USER {
            @Override
            public String urlString(String value) {
                return "http://" + value + "@example.com/";
            }

            @Override
            public String encodedValue(HttpUrl url) {
                return url.encodedUsername();
            }

            @Override
            public void set(HttpUrl.Builder builder, String value) {
                builder.username(value);
            }

            @Override
            public String get(HttpUrl url) {
                return url.username();
            }
        },
        PASSWORD {
            @Override
            public String urlString(String value) {
                return "http://:" + value + "@example.com/";
            }

            @Override
            public String encodedValue(HttpUrl url) {
                return url.encodedPassword();
            }

            @Override
            public void set(HttpUrl.Builder builder, String value) {
                builder.password(value);
            }

            @Override
            public String get(HttpUrl url) {
                return url.password();
            }
        },
        PATH {
            @Override
            public String urlString(String value) {
                return "http://example.com/a" + value + "z/";
            }

            @Override
            public String encodedValue(HttpUrl url) {
                String path = url.encodedPath();
                return path.substring(2, path.length() - 2);
            }

            @Override
            public void set(HttpUrl.Builder builder, String value) {
                builder.addPathSegment("a" + value + "z");
            }

            @Override
            public String get(HttpUrl url) {
                String pathSegment = url.pathSegments().get(0);
                return pathSegment.substring(1, pathSegment.length() - 1);
            }
        },
        QUERY {
            @Override
            public String urlString(String value) {
                return "http://example.com/?a" + value + "z";
            }

            @Override
            public String encodedValue(HttpUrl url) {
                String query = url.encodedQuery();
                return query.substring(1, query.length() - 1);
            }

            @Override
            public void set(HttpUrl.Builder builder, String value) {
                builder.query("a" + value + "z");
            }

            @Override
            public String get(HttpUrl url) {
                String query = url.query();
                return query.substring(1, query.length() - 1);
            }
        },
        FRAGMENT {
            @Override
            public String urlString(String value) {
                return "http://example.com/#a" + value + "z";
            }

            @Override
            public String encodedValue(HttpUrl url) {
                String fragment = url.encodedFragment();
                return fragment.substring(1, fragment.length() - 1);
            }

            @Override
            public void set(HttpUrl.Builder builder, String value) {
                builder.fragment("a" + value + "z");
            }

            @Override
            public String get(HttpUrl url) {
                String fragment = url.fragment();
                return fragment.substring(1, fragment.length() - 1);
            }
        };

        public abstract String urlString(String value);

        public abstract String encodedValue(HttpUrl url);

        public abstract void set(HttpUrl.Builder builder, String value);

        public abstract String get(HttpUrl url);
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/HttPizza.java<|end_filename|>
package sexy.code;

import java.util.concurrent.Executor;

public class HttPizza {

    private final HttpConfig httpConfig;

    HttPizza(HttpConfig config) {
        this.httpConfig = config;
    }

    public HttPizza() {
        this(new HttpConfig());
    }

    public Call newCall(final Request request) {
        return new Call(new HttpEngine(httpConfig), request);
    }

    public Request.Builder newRequest() {
        return new Request.Builder();
    }

    public static class Builder {

        private final HttpConfig httpConfig = new HttpConfig();

        /**
         * Set the connect timeout in milliseconds. The default is set to 10 seconds.
         *
         * @see java.net.URLConnection#setConnectTimeout(int)
         */
        public Builder connectTimeout(int timeoutMillis) {
            httpConfig.connectTimeout = timeoutMillis;
            return this;
        }

        /**
         * Set the read timeout in milliseconds. The default is set to 10 seconds.
         *
         * @see java.net.URLConnection#setReadTimeout(int)
         */
        public Builder readTimeout(int timeoutMillis) {
            httpConfig.readTimeout = timeoutMillis;
            return this;
        }

        public Builder httpExecutor(Executor executor) {
            httpConfig.httpExecutor = executor;
            return this;
        }

        public Builder callbackExecutor(Executor executor) {
            httpConfig.callbackExecutor = executor;
            return this;
        }

        public HttPizza build() {
            return new HttPizza(httpConfig);
        }
    }
}


<|start_filename|>httpizza/src/test/java/sexy/code/CallTestCase.java<|end_filename|>
package sexy.code;

import java.io.IOException;

import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

public abstract class CallTestCase {

    protected HttPizza client;

    protected MockWebServer server;

    protected abstract Request prepareRequest(HttpUrl url) throws Exception;

    protected abstract void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception;

    protected abstract void verifyResponse(HttpUrl url, Response response) throws Exception;

    protected void setUp() throws Exception {
        server = new MockWebServer();
        server.start();
    }

    protected void tearDown() throws Exception {
        try {
            client = null;
            server.shutdown();
            server = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    protected HttpUrl fullUrl(String path) {
        return convertHttpUrl(server.url(path));
    }

    public static HttpUrl convertHttpUrl(okhttp3.HttpUrl okHttpUrl) {
        return HttpUrl.parse(okHttpUrl.toString());
    }

    public static String urlPathWithQuery(HttpUrl url) {
        String queryString = url.encodedQuery() == null ? "" : "?" + url.encodedQuery();
        return url.encodedPath() + queryString;
    }

    public void test(String path) throws Exception {
        setUp();

        client = new HttPizza();
        HttpUrl url = fullUrl(path);

        Request request = prepareRequest(url);
        Response response = client.newCall(request).execute();

        verifyRequest(url, server.takeRequest());
        verifyResponse(url, response);

        tearDown();
    }

    public void testAsync(String path) throws Exception {
        setUp();

        client = new HttPizza.Builder()
                .httpExecutor(new SynchronousExecutor())
                .callbackExecutor(new SynchronousExecutor())
                .build();
        final HttpUrl url = fullUrl(path);

        Request request = prepareRequest(url);
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Response response) {
                try {
                    verifyResponse(url, response);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

            @Override
            public void onFailure(Throwable t) {
                throw new RuntimeException(t);
            }
        });

        verifyRequest(url, server.takeRequest());

        tearDown();
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/HttpEngine.java<|end_filename|>
package sexy.code;

import android.os.Build;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

class HttpEngine {

    private final HttpConfig config;

    HttpEngine(final HttpConfig config) {
        this.config = config;
    }

    public Response execute(final Request request) throws IOException {
        final URL httpUrl = request.url().url();
        final HttpURLConnection urlConnection = (HttpURLConnection) httpUrl.openConnection();

        urlConnection.setRequestMethod(request.method());
        urlConnection.setReadTimeout(config.readTimeout);
        urlConnection.setConnectTimeout(config.connectTimeout);
        urlConnection.setDoInput(true);

        final Map<String, String> headers = request.headers();
        if (headers != null && !headers.isEmpty()) {
            addRequestProperties(urlConnection, headers);
        }

        if (isOutput(request)) {
            doOutput(urlConnection, request.body());
        }

        urlConnection.connect();

        final int statusCode = urlConnection.getResponseCode();
        final String message = urlConnection.getResponseMessage();
        final Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
        final ResponseBody body = doInput(urlConnection);
        return new Response(request, statusCode, message, responseHeaders, body);
    }

    public Executor httpExecutor() {
        return config.httpExecutor;
    }

    public Executor callbackExecutor() {
        return config.callbackExecutor;
    }

    private static void addRequestProperties(final HttpURLConnection urlConnection, final Map<String, String> headers) {
        for (final Map.Entry<String, String> entry : headers.entrySet()) {
            final String value = entry.getValue();
            if (value != null) {
                urlConnection.addRequestProperty(entry.getKey(), value);
            }
        }
    }

    private static boolean isOutput(final Request request) {
        if (request.body() == null) {
            return false;
        }
        final String method = request.method();
        return Request.METHOD_POST.equals(method) || Request.METHOD_PUT.equals(method);
    }

    private static void doOutput(final HttpURLConnection urlConnection, final RequestBody body)
            throws IOException {
        urlConnection.setDoOutput(true);
        final String contentType = body.contentType().toString();
        if (contentType != null) {
            urlConnection.addRequestProperty("Content-Type", contentType);
        }
        final long contentLength = body.contentLength();
        if (contentLength > 0) {
            setFixedLengthStreamingMode(urlConnection, contentLength);
            urlConnection.addRequestProperty("Content-Length", Long.toString(contentLength));
        } else {
            urlConnection.setChunkedStreamingMode(0);
        }
        BufferedOutputStream os = null;
        try {
            os = new BufferedOutputStream(urlConnection.getOutputStream());
            body.writeTo(os);
        } finally {
            Util.closeQuietly(os);
        }
    }

    private static ResponseBody doInput(final HttpURLConnection urlConnection) throws IOException {
        final String contentType = urlConnection.getContentType();
        final long contentLength = urlConnection.getContentLength();
        return new ResponseBody(contentType, contentLength, new BufferedInputStream(getResponseStream(urlConnection)), urlConnection);
    }

    private static InputStream getResponseStream(final HttpURLConnection urlConnection) {
        try {
            return urlConnection.getInputStream();
        } catch (final IOException e) {
            return urlConnection.getErrorStream();
        }
    }

    private static void setFixedLengthStreamingMode(final HttpURLConnection urlConnection, final long contentLength) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            urlConnection.setFixedLengthStreamingMode(contentLength);
        } else {
            urlConnection.setFixedLengthStreamingMode((int) contentLength);
        }
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/Response.java<|end_filename|>
package sexy.code;

import java.util.List;
import java.util.Map;

public class Response {

    private final ResponseBody body;

    private final Request request;

    private final int code;

    private final String message;

    private final Map<String, List<String>> headers;

    Response(final Request request, final int code, String message, final Map<String, List<String>> headers, final ResponseBody body) {
        this.request = request;
        this.code = code;
        this.message = message;
        this.headers = headers;
        this.body = body;
    }

    public Request getRequest() {
        return request;
    }

    public boolean isSuccessful() {
        return (code / 100) == 2;
    }

    public int code() {
        return code;
    }

    public String message() {
        return message;
    }

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

    public ResponseBody body() {
        return body;
    }
}


<|start_filename|>httpizza/src/test/java/sexy/code/SynchronousExecutor.java<|end_filename|>
package sexy.code;

import java.util.concurrent.Executor;

public class SynchronousExecutor implements Executor {

    @Override
    public void execute(Runnable command) {
        command.run();
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/Callback.java<|end_filename|>
package sexy.code;

public interface Callback {

    void onResponse(Response response);

    void onFailure(Throwable t);
}


<|start_filename|>httpizza/src/main/java/sexy/code/Call.java<|end_filename|>
package sexy.code;

import java.io.IOException;
import java.util.concurrent.Executor;

public class Call {

    private final Request request;

    private final HttpEngine engine;

    Call(final HttpEngine httpEngine, final Request request) {
        this.engine = httpEngine;
        this.request = request;
    }

    public Response execute() throws IOException {
        try {
            return engine.execute(request);
        } catch (RuntimeException e) {
            throw new IOException(e);
        }
    }

    public void enqueue(final Callback callback) {
        final Executor callbackExecutor = engine.callbackExecutor();
        engine.httpExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    final Response response = execute();
                    callbackExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            callback.onResponse(response);
                        }
                    });
                } catch (final Throwable t) {
                    callbackExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            callback.onFailure(t);
                        }
                    });
                }
            }
        });
    }
}


<|start_filename|>httpizza/src/test/java/sexy/code/CallTest.java<|end_filename|>
package sexy.code;

import org.junit.Test;

import java.net.HttpURLConnection;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.RecordedRequest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class CallTest {

    public static final MediaType MEDIA_TYPE_PLAIN_TEXT = MediaType.parse("text/plain; charset=utf-8");

    private CallTestCase getTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .setBody("body")
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .get()
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("GET", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(0, recordedRequest.getBody().size());
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
            assertNull(recordedRequest.getHeader("Content-Length"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertEquals("body", response.body().string());
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void get() throws Exception {
        getTestCase.test("/path?query1=1&query2=2");
        getTestCase.testAsync("/path?query1=1&query2=2");
    }

    private CallTestCase postTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .setBody("body")
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .post(RequestBody.create(MEDIA_TYPE_PLAIN_TEXT, "requestBody"))
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("POST", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(11, recordedRequest.getBody().size());
            assertEquals("11", recordedRequest.getHeader("Content-Length"));
            assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertEquals("body", response.body().string());
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void post() throws Exception {
        postTestCase.test("/path?query1=1&query2=2");
        postTestCase.testAsync("/path?query1=1&query2=2");
    }

    private CallTestCase zeroLengthPostTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .setBody("body")
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .post(RequestBody.create(MEDIA_TYPE_PLAIN_TEXT, ""))
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("POST", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(0, recordedRequest.getBody().size());
            assertEquals(null, recordedRequest.getHeader("Content-Length"));
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertEquals("body", response.body().string());
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void postZeroLength() throws Exception {
        zeroLengthPostTestCase.test("/path?query1=1&query2=2");
        zeroLengthPostTestCase.testAsync("/path?query1=1&query2=2");
    }

    private CallTestCase putTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .setBody("body")
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .put(RequestBody.create(MEDIA_TYPE_PLAIN_TEXT, "requestBody"))
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("PUT", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(11, recordedRequest.getBody().size());
            assertEquals("11", recordedRequest.getHeader("Content-Length"));
            assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertEquals("body", response.body().string());
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void put() throws Exception {
        putTestCase.test("/path?query1=1&query2=2");
        putTestCase.testAsync("/path?query1=1&query2=2");
    }

    private CallTestCase deleteTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .setBody("body")
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .delete()
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("DELETE", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(0, recordedRequest.getBody().size());
            assertEquals(null, recordedRequest.getHeader("Content-Length"));
            assertNull(recordedRequest.getHeader("Content-Type"));
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertEquals("body", response.body().string());
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void delete() throws Exception {
        deleteTestCase.test("/path?query1=1&query2=2");
        deleteTestCase.testAsync("/path?query1=1&query2=2");
    }

    private CallTestCase headTestCase = new CallTestCase() {
        @Override
        protected Request prepareRequest(HttpUrl url) throws Exception {
            MockResponse mockResponse = new MockResponse()
                    .setResponseCode(HttpURLConnection.HTTP_OK)
                    .addHeader("Content-Type: text/plain");
            server.enqueue(mockResponse);

            return client.newRequest()
                    .url(url)
                    .header("User-Agent", "UnitTest")
                    .method("HEAD", null)
                    .build();
        }

        @Override
        protected void verifyRequest(HttpUrl url, RecordedRequest recordedRequest) throws Exception {
            assertEquals("HEAD", recordedRequest.getMethod());
            assertEquals(urlPathWithQuery(url), recordedRequest.getPath());
            assertEquals(0, recordedRequest.getBody().size());
            assertEquals(null, recordedRequest.getHeader("Content-Length"));
            assertNull(recordedRequest.getHeader("Content-Type"));
            assertEquals("UnitTest", recordedRequest.getHeader("User-Agent"));
        }

        @Override
        protected void verifyResponse(HttpUrl url, Response response) throws Exception {
            assertEquals(200, response.code());
            assertEquals("text/plain", response.headers().get("Content-Type").get(0));
            assertTrue(response.isSuccessful());
        }
    };

    @Test
    public void head() throws Exception {
        headTestCase.test("/path?query1=1&query2=2");
        headTestCase.testAsync("/path?query1=1&query2=2");
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/Util.java<|end_filename|>
package sexy.code;

import java.io.Closeable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class Util {

    /**
     * A cheap and type-safe constant for the UTF-8 Charset.
     */
    public static final Charset UTF_8 = Charset.forName("UTF-8");

    private Util() {
    }

    public static void closeQuietly(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (RuntimeException rethrown) {
            throw rethrown;
        } catch (Exception ignore) {

        }
    }

    public static void checkOffsetAndCount(long arrayLength, long offset, long count) {
        if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    /** Returns an immutable copy of {@code list}. */
    public static <T> List<T> immutableList(List<T> list) {
        return Collections.unmodifiableList(new ArrayList<>(list));
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/HttpConfig.java<|end_filename|>
package sexy.code;

import android.os.Handler;
import android.os.Looper;

import java.util.concurrent.Executor;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

class HttpConfig {

    public int connectTimeout = (int) TimeUnit.SECONDS.toMillis(10);

    public int readTimeout = (int) TimeUnit.SECONDS.toMillis(10);

    public Executor httpExecutor = new ThreadPoolExecutor(1, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

    public Executor callbackExecutor = new Executor() {

        Handler mainHandler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(Runnable command) {
            mainHandler.post(command);
        }
    };
}


<|start_filename|>httpizza/src/main/java/sexy/code/RequestBody.java<|end_filename|>
package sexy.code;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

/**
 * Parts of the code were copied from OkHttp and modified.
 */
public abstract class RequestBody {

    /** Returns the Content-Type header for this body. */
    public abstract MediaType contentType();

    public abstract void writeTo(final BufferedOutputStream os) throws IOException;

    public long contentLength() throws IOException {
        return -1;
    }

    /**
     * Returns a new request body that transmits {@code content}. If {@code contentType} is non-null
     * and lacks a charset, this will use UTF-8.
     */
    public static RequestBody create(MediaType contentType, String content) {
        Charset charset = Util.UTF_8;
        if (contentType != null) {
            charset = contentType.charset();
            if (charset == null) {
                charset = Util.UTF_8;
                contentType = MediaType.parse(contentType + "; charset=utf-8");
            }
        }
        byte[] bytes = content.getBytes(charset);
        return create(contentType, bytes);
    }

    /** Returns a new request body that transmits {@code content}. */
    public static RequestBody create(final MediaType contentType, final byte[] content) {
        return create(contentType, content, 0, content.length);
    }

    /** Returns a new request body that transmits {@code content}. */
    public static RequestBody create(final MediaType contentType, final byte[] content, final int offset, final int byteCount) {
        if (content == null) {
            throw new NullPointerException("content == null");
        }
        Util.checkOffsetAndCount(content.length, offset, byteCount);
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                return byteCount;
            }

            @Override
            public void writeTo(BufferedOutputStream os) throws IOException {
                os.write(content, offset, byteCount);
            }
        };
    }

    /** Returns a new request body that transmits the content of {@code file}. */
    public static RequestBody create(final MediaType contentType, final File file) {
        if (file == null) {
            throw new NullPointerException("content == null");
        }

        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                return file.length();
            }

            @Override
            public void writeTo(BufferedOutputStream os) throws IOException {
                InputStream fileInputStream = new BufferedInputStream(new FileInputStream(file));

                try {
                    int d;
                    while ((d = fileInputStream.read()) != -1) {
                        os.write(d);
                    }
                } finally {
                    Util.closeQuietly(fileInputStream);
                }
            }
        };
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/Request.java<|end_filename|>
package sexy.code;

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

public class Request {

    public static final String METHOD_GET = "GET";

    public static final String METHOD_POST = "POST";

    public static final String METHOD_PUT = "PUT";

    public static final String METHOD_DELETE = "DELETE";

    public static final String METHOD_HEAD = "HEAD";

    public static final String METHOD_OPTIONS = "OPTIONS";

    public static final String METHOD_TRACE = "TRACE";

    private HttpUrl url;

    private String method;

    private Map<String, String> headers;

    private RequestBody body;

    Request(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = builder.headers;
        this.body = builder.body;
    }

    public HttpUrl url() {
        return url;
    }

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

    public String method() {
        return method;
    }

    public RequestBody body() {
        return body;
    }

    public static class Builder {

        private HttpUrl url;

        private String method;

        private Map<String, String> headers;

        private RequestBody body;

        Builder() {
            method = METHOD_GET;
            headers = new HashMap<>();
        }

        public Builder url(HttpUrl url) {
            if (url == null) {
                throw new IllegalArgumentException("url == null");
            }
            this.url = url;
            return this;
        }

        public Builder url(String url) {
            if (url == null) {
                throw new IllegalArgumentException("url == null");
            }
            HttpUrl parsed = HttpUrl.parse(url);
            if (parsed == null) {
                throw new IllegalArgumentException("unexpected url: " + url);
            }
            return url(parsed);
        }

        public Builder head() {
            return method(METHOD_HEAD, null);
        }

        public Builder get() {
            return method(METHOD_GET, null);
        }

        public Builder post(RequestBody body) {
            return method(METHOD_POST, body);
        }

        /**
         * HttpUrlConnection does not support sending request body with DELETE request
         */
        public Builder delete() {
            return method(METHOD_DELETE, null);
        }

        public Builder put(RequestBody body) {
            return method(METHOD_PUT, body);
        }

        public Builder header(String name, String value) {
            checkHeaderNameAndValue(name, value);
            headers.put(name, value);
            return this;
        }

        public Builder method(String method, RequestBody body) {
            this.method = method;
            this.body = body;
            return this;
        }

        public Request build() {
            if (url == null) {
                throw new IllegalStateException("url == null");
            }
            return new Request(this);
        }

        private void checkHeaderNameAndValue(String name, String value) {
            if (name == null) {
                throw new IllegalArgumentException("name == null");
            }
            if (name.isEmpty()) {
                throw new IllegalArgumentException("name is empty");
            }
            for (int i = 0, length = name.length(); i < length; i++) {
                char c = name.charAt(i);
                if (c <= '\u001f' || c >= '\u007f') {
                    throw new IllegalArgumentException(String.format("Unexpected char %#04x at %d in header name: %s", (int) c, i, name));
                }
            }
            if (value == null) {
                throw new IllegalArgumentException("value == null");
            }
            for (int i = 0, length = value.length(); i < length; i++) {
                char c = value.charAt(i);
                if (c <= '\u001f' || c >= '\u007f') {
                    throw new IllegalArgumentException(
                            String.format("Unexpected char %#04x at %d in %s value: %s", (int) c, i, name, value));
                }
            }
        }
    }
}


<|start_filename|>httpizza/src/main/java/sexy/code/FormBody.java<|end_filename|>
/*
 * Copyright (C) 2014 Square, Inc.
 *
 * 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 sexy.code;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static sexy.code.HttpUrl.FORM_ENCODE_SET;
import static sexy.code.HttpUrl.percentDecode;

/**
 * Copied from OkHttp and changed so that it does not depend on Okio but uses java NIO instead.
 */
public final class FormBody extends RequestBody {

    private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");

    private final List<String> encodedNames;

    private final List<String> encodedValues;

    private FormBody(List<String> encodedNames, List<String> encodedValues) {
        this.encodedNames = Util.immutableList(encodedNames);
        this.encodedValues = Util.immutableList(encodedValues);
    }

    /** The number of key-value pairs in this form-encoded body. */
    public int size() {
        return encodedNames.size();
    }

    public String encodedName(int index) {
        return encodedNames.get(index);
    }

    public String name(int index) {
        return percentDecode(encodedName(index), true);
    }

    public String encodedValue(int index) {
        return encodedValues.get(index);
    }

    public String value(int index) {
        return percentDecode(encodedValue(index), true);
    }

    @Override
    public MediaType contentType() {
        return CONTENT_TYPE;
    }

    @Override
    public void writeTo(BufferedOutputStream os) throws IOException {
        writeOrCountBytes(os, false);
    }

    @Override
    public long contentLength() throws IOException {
        return writeOrCountBytes(null, true);
    }

    /**
     * Either writes this request to {@code sink} or measures its content length. We have one method
     * do double-duty to make sure the counting and content are consistent, particularly when it comes
     * to awkward operations like measuring the encoded length of header strings, or the
     * length-in-digits of an encoded integer.
     */
    private long writeOrCountBytes(BufferedOutputStream os, boolean countBytes) throws IOException {
        long byteCount = 0L;

        StringBuilder sb = new StringBuilder();

        for (int i = 0, size = encodedNames.size(); i < size; i++) {
            if (i > 0) {
                sb.append('&');
            }

            sb.append(encodedNames.get(i));
            sb.append('=');
            sb.append(encodedValues.get(i));
        }

        byte[] bytes = sb.toString().getBytes(Util.UTF_8);

        if (countBytes) {
            byteCount = bytes.length;
        } else {
            os.write(bytes);
        }

        return byteCount;
    }

    public static final class Builder {

        private final List<String> names = new ArrayList<>();

        private final List<String> values = new ArrayList<>();

        public Builder add(String name, String value) {
            names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true));
            values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true));
            return this;
        }

        public Builder addEncoded(String name, String value) {
            names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, true, false, true, true));
            values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, true, false, true, true));
            return this;
        }

        public FormBody build() {
            return new FormBody(names, values);
        }
    }
}
