GoogleHttpClientHandler.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.google;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.javanet.NetHttpTransport;
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.descriptor.PService;
import net.morimekta.providence.serializer.DefaultSerializerProvider;
import net.morimekta.providence.serializer.Serializer;
import net.morimekta.providence.serializer.SerializerProvider;
import net.morimekta.util.io.IOUtils;

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

import static com.google.common.base.Strings.emptyToNull;
import static net.morimekta.providence.PServiceCallInstrumentation.NS_IN_MILLIS;

/**
 * HTTP client handler using the google HTTP client interface.
 */
public class GoogleHttpClientHandler implements PServiceCallHandler {
    private final HttpRequestFactory          factory;
    private final SerializerProvider          serializerProvider;
    private final Serializer                  requestSerializer;
    private final Supplier<GenericUrl>        urlSupplier;
    private final PServiceCallInstrumentation instrumentation;

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

    /**
     * Create a HTTP client with default serialization and no instrumentation.
     *
     * @param urlSupplier The HTTP url supplier.
     * @param factory The HTTP request factory.
     */
    public GoogleHttpClientHandler(@Nonnull Supplier<GenericUrl> urlSupplier,
                                   @Nonnull HttpRequestFactory 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 GoogleHttpClientHandler(@Nonnull Supplier<GenericUrl> urlSupplier,
                                   @Nonnull HttpRequestFactory 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 GoogleHttpClientHandler(@Nonnull Supplier<GenericUrl> urlSupplier,
                                   @Nonnull HttpRequestFactory factory,
                                   @Nonnull SerializerProvider serializerProvider,
                                   @Nonnull PServiceCallInstrumentation instrumentation) {
        this.urlSupplier = urlSupplier;
        this.factory = factory;
        this.serializerProvider = serializerProvider;
        this.requestSerializer = serializerProvider.getDefault();
        this.instrumentation = instrumentation;
    }

    @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 {
            ProvidenceHttpContent content = new ProvidenceHttpContent(call, requestSerializer);

            @Nonnull
            GenericUrl url = urlSupplier.get();
            try {
                HttpRequest request = factory.buildPostRequest(url, content);
                request.setThrowExceptionOnExecuteError(false)
                       .getHeaders()
                       .setAccept(requestSerializer.mediaType());
                HttpResponse response = request.execute();
                try {
                    InputStream in = response.getContent();
                    if (response.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
                        throw new net.morimekta.providence.client.HttpResponseException(
                                url.toURL(),
                                response.getStatusCode(),
                                response.getStatusMessage(),
                                in != null ? emptyToNull(IOUtils.readString(in)) : null,
                                null);
                    }
                    // should be impossible?
                    if (in == null) {
                        throw new net.morimekta.providence.client.HttpResponseException(
                                url.toURL(),
                                response.getStatusCode(),
                                response.getStatusMessage(),
                                null,
                                null);
                    }

                    if (call.getType() == PServiceCallType.CALL) {
                        Serializer responseSerializer = requestSerializer;
                        if (response.getContentType() != null) {
                            try {
                                responseSerializer = serializerProvider.getSerializer(
                                        response.getContentType());
                            } catch (IllegalArgumentException e) {
                                throw new PApplicationException(
                                        "Unknown content-type in response: " + response.getContentType(),
                                        PApplicationExceptionType.INVALID_PROTOCOL).initCause(e);
                            }
                        }

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

                        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;
                } finally {
                    // Ignore whatever is left of the response when returning, in
                    // case we did'nt read the whole response.
                    response.ignore();
                }
            } catch (HttpResponseException e) {
                // Normalize connection refused exceptions to HttpClientConnectException.
                // The native exception is not helpful (for when using NetHttpTransport).
                throw new net.morimekta.providence.client.HttpResponseException(url.toURL(), e.getStatusCode(), e.getStatusMessage(), e.getContent(), e);
            } catch (ConnectException e) {
                // Normalize connection refused exceptions to HttpClientConnectException.
                // The native exception is not helpful (for when using NetHttpTransport).
                throw new HttpClientConnectException(e, url.toURL(), 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;
        }
    }
}