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;
}
}
}