OkHttpClientHandler.java

/*
 * Copyright 2016-2017 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 net.morimekta.providence.client.okhttp;

import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PApplicationExceptionType;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PServiceCall;
import net.morimekta.providence.PServiceCallHandler;
import net.morimekta.providence.PServiceCallInstrumentation;
import net.morimekta.providence.PServiceCallType;
import net.morimekta.providence.client.HttpClientConnectException;
import net.morimekta.providence.client.HttpResponseException;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.serializer.DefaultSerializerProvider;
import net.morimekta.providence.serializer.Serializer;
import net.morimekta.providence.serializer.SerializerProvider;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.URL;
import java.util.function.Supplier;

import static net.morimekta.providence.PServiceCallInstrumentation.NS_IN_MILLIS;
import static net.morimekta.util.io.IOUtils.readString;

/**
 * HTTP client handler using the OkHTTP client interface.
 */
public class OkHttpClientHandler implements PServiceCallHandler {
    private final OkHttpClient       factory;
    private final SerializerProvider serializerProvider;
    private final Serializer         requestSerializer;
    private final Supplier<URL>      urlSupplier;
    private final PServiceCallInstrumentation instrumentation;

    /**
     * Create a HTTP client with default transport, serialization and no instrumentation.
     *
     * @param urlSupplier The HTTP url supplier.
     */
    public OkHttpClientHandler(@Nonnull Supplier<URL> urlSupplier) {
        this(urlSupplier, new OkHttpClient());
    }

    /**
     * Create a HTTP client with default serialization and no instrumentation.
     *
     * @param urlSupplier The HTTP url supplier.
     * @param factory     The HTTP request factory.
     */
    public OkHttpClientHandler(@Nonnull Supplier<URL> urlSupplier,
                               @Nonnull OkHttpClient factory) {
        this(urlSupplier, factory, new DefaultSerializerProvider());
    }

    /**
     * Create a HTTP client with no instrumentation.
     *
     * @param urlSupplier        The HTTP url supplier.
     * @param factory            The HTTP request factory.
     * @param serializerProvider The serializer provider.
     */
    public OkHttpClientHandler(@Nonnull Supplier<URL> urlSupplier,
                               @Nonnull OkHttpClient factory,
                               @Nonnull SerializerProvider serializerProvider) {
        this(urlSupplier, factory, serializerProvider, PServiceCallInstrumentation.NOOP);
    }

    /**
     * Create a HTTP client.
     *
     * @param urlSupplier        The HTTP url supplier.
     * @param factory            The HTTP request factory.
     * @param serializerProvider The serializer provider.
     * @param instrumentation    The service call instrumentation.
     */
    public OkHttpClientHandler(@Nonnull Supplier<URL> urlSupplier,
                               @Nonnull OkHttpClient factory,
                               @Nonnull SerializerProvider serializerProvider,
                               @Nonnull PServiceCallInstrumentation instrumentation) {
        this.urlSupplier = urlSupplier;
        this.factory = factory;
        this.serializerProvider = serializerProvider;
        this.requestSerializer = serializerProvider.getDefault();
        this.instrumentation = instrumentation;
    }

    private static String readContent(ResponseBody body) throws IOException {
        if (body == null) return null;
        return readString(body.byteStream());
    }

    @Nonnull
    private static String contentType(ResponseBody body) {
        if (body == null) return "*/*";
        MediaType mediaType = body.contentType();
        if (mediaType == null) return "*/*";
        return mediaType.toString();
    }

    @Override
    public <Request extends PMessage<Request>,
            Response extends PMessage<Response>>
    PServiceCall<Response> handleCall(PServiceCall<Request> call,
                                      PService service) throws IOException {
        if (call.getType() == PServiceCallType.EXCEPTION || call.getType() == PServiceCallType.REPLY) {
            throw new PApplicationException("Request with invalid call type: " + call.getType(),
                                            PApplicationExceptionType.INVALID_MESSAGE_TYPE);
        }

        long startTime = System.nanoTime();
        PServiceCall<Response> reply = null;
        try {
            @Nonnull
            URL url = urlSupplier.get();
            try {
                ProvidenceRequestBody<Request> content = new ProvidenceRequestBody<>(call, requestSerializer);
                okhttp3.Request request = new okhttp3.Request.Builder()
                        .url(url)
                        .post(content)
                        .build();
                try (okhttp3.Response response = factory.newCall(request).execute()) {
                    if (call.getType() == PServiceCallType.CALL) {
                        if (!response.isSuccessful()) {
                            throw new HttpResponseException(
                                    url,
                                    response.code(),
                                    response.message(),
                                    readContent(response.body()),
                                    null);
                        }

                        Serializer   responseSerializer = requestSerializer;
                        ResponseBody responseBody       = response.body();
                        if (responseBody == null) {
                            throw new PApplicationException(
                                    "No response on " + response.code() + " for " + url.toString(),
                                    PApplicationExceptionType.PROTOCOL_ERROR);
                        }
                        if (responseBody.contentType() != null) {
                            try {
                                responseSerializer = serializerProvider.getSerializer(contentType(responseBody));
                            } catch (IllegalArgumentException e) {
                                throw new PApplicationException(
                                        "Unknown content-type in response: " + responseBody.contentType(),
                                        PApplicationExceptionType.INVALID_PROTOCOL).initCause(e);
                            }
                        }

                        // non 200 responses should have triggered a HttpResponseException,
                        // so this is safe.
                        reply = responseSerializer.deserialize(responseBody.byteStream(), service);
                        responseSerializer.verifyEndOfContent(responseBody.byteStream());

                        if (reply.getType() == PServiceCallType.CALL || reply.getType() == PServiceCallType.ONEWAY) {
                            throw new PApplicationException("Reply with invalid call type: " + reply.getType(),
                                                            PApplicationExceptionType.INVALID_MESSAGE_TYPE);
                        }
                        if (reply.getSequence() != call.getSequence()) {
                            throw new PApplicationException(
                                    "Reply sequence out of order: call = " + call.getSequence() + ", reply = " +
                                    reply.getSequence(),
                                    PApplicationExceptionType.BAD_SEQUENCE_ID);
                        }
                    }

                    long   endTime  = System.nanoTime();
                    double duration = ((double) (endTime - startTime)) / NS_IN_MILLIS;
                    try {
                        instrumentation.onComplete(duration, call, reply);
                    } catch (Exception ignore) {
                    }

                    return reply;
                }
                // Ignore whatever is left of the response when returning, in
                // case we did'nt read the whole response.
            } catch (ConnectException e) {
                // Normalize connection refused exceptions to HttpClientConnectException.
                // The native exception is not helpful (for when using NetHttpTransport).
                throw new HttpClientConnectException(e, url, InetAddress.getAllByName(url.getHost()));
            }
        } catch (IOException | RuntimeException e) {
            long endTime = System.nanoTime();
            double duration = ((double) (endTime - startTime)) / NS_IN_MILLIS;
            try {
                instrumentation.onTransportException(e, duration, call, reply);
            } catch (Throwable ie) {
                e.addSuppressed(ie);
            }

            throw e;
        }
    }
}