OkHttpGitHubConnector.java

package org.kohsuke.github.extras.okhttp3;

import okhttp3.*;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.*;
import org.kohsuke.github.connector.GitHubConnector;
import org.kohsuke.github.connector.GitHubConnectorRequest;
import org.kohsuke.github.connector.GitHubConnectorResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
 * {@link GitHubConnector} for {@link OkHttpClient}.
 * <p>
 * Unlike {@link #DEFAULT}, OkHttp supports response caching. Making a conditional request against GitHub API and
 * receiving a 304 response does not count against the rate limit. See
 * http://developer.github.com/v3/#conditional-requests
 *
 * @author Liam Newman
 */
public class OkHttpGitHubConnector implements GitHubConnector {
    private static final String HEADER_NAME = "Cache-Control";
    private final String maxAgeHeaderValue;

    private final OkHttpClient client;

    /**
     * Instantiates a new Ok http connector.
     *
     * @param client
     *            the client
     */
    public OkHttpGitHubConnector(OkHttpClient client) {
        this(client, 0);
    }

    /**
     * Instantiates a new Ok http connector.
     *
     * @param client
     *            the client
     * @param cacheMaxAge
     *            the cache max age
     */
    public OkHttpGitHubConnector(OkHttpClient client, int cacheMaxAge) {

        OkHttpClient.Builder builder = client.newBuilder();

        builder.connectionSpecs(TlsConnectionSpecs());
        this.client = builder.build();
        if (cacheMaxAge >= 0 && this.client != null && this.client.cache() != null) {
            maxAgeHeaderValue = new CacheControl.Builder().maxAge(cacheMaxAge, TimeUnit.SECONDS).build().toString();
        } else {
            maxAgeHeaderValue = null;
        }
    }

    @Override
    public GitHubConnectorResponse send(GitHubConnectorRequest request) throws IOException {
        Request.Builder builder = new Request.Builder().url(request.url());
        if (maxAgeHeaderValue != null && request.header(HEADER_NAME) == null) {
            // By default OkHttp honors max-age, meaning it will use local cache
            // without checking the network within that timeframe.
            // However, that can result in stale data being returned during that time so
            // we force network-based checking no matter how often the query is made.
            // OkHttp still automatically does ETag checking and returns cached data when
            // GitHub reports 304, but those do not count against rate limit.
            builder.header(HEADER_NAME, maxAgeHeaderValue);
        }

        for (Map.Entry<String, List<String>> e : request.allHeaders().entrySet()) {
            List<String> v = e.getValue();
            if (v != null) {
                builder.addHeader(e.getKey(), String.join(", ", v));
            }
        }

        RequestBody body = null;
        if (request.hasBody()) {
            body = RequestBody.create(IOUtils.toByteArray(request.body()));
        }
        builder.method(request.method(), body);
        Request okhttpRequest = builder.build();
        Response okhttpResponse = client.newCall(okhttpRequest).execute();

        return new OkHttpGitHubConnectorResponse(request, okhttpResponse);
    }

    /** Returns connection spec with TLS v1.2 in it */
    private List<ConnectionSpec> TlsConnectionSpecs() {
        return Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
    }

    /**
     * Initial response information when a response is initially received and before the body is processed.
     *
     * Implementation specific to {@link okhttp3.Response}.
     */
    private static class OkHttpGitHubConnectorResponse extends GitHubConnectorResponse.ByteArrayResponse {

        @Nonnull
        private final Response response;

        OkHttpGitHubConnectorResponse(@Nonnull GitHubConnectorRequest request, @Nonnull Response response) {
            super(request, response.code(), response.headers().toMultimap());
            this.response = response;
        }

        @CheckForNull
        @Override
        protected InputStream rawBodyStream() throws IOException {
            ResponseBody body = response.body();
            if (body != null) {
                return body.byteStream();
            } else {
                return null;
            }
        }

        @Override
        public void close() throws IOException {
            super.close();
            response.close();
        }
    }
}