From 0572eed8aa29879c74794b22e8ae79e414dd5821 Mon Sep 17 00:00:00 2001 From: Vadym Matsishevskyi <25311427+vam-google@users.noreply.github.com> Date: Fri, 28 Jan 2022 10:00:39 -0800 Subject: [PATCH] feat: add REST interceptors infrastructure (#1607) The added interceptors infrastructure mimics the one from gRPC. --- .../ForwardingHttpJsonClientCall.java | 88 +++++++ .../ForwardingHttpJsonClientCallListener.java | 79 +++++++ .../api/gax/httpjson/HttpJsonChannel.java | 7 - .../gax/httpjson/HttpJsonClientCallImpl.java | 16 +- .../httpjson/HttpJsonClientInterceptor.java | 55 +++++ .../httpjson/HttpJsonHeaderInterceptor.java | 69 ++++++ .../httpjson/HttpJsonInterceptorProvider.java | 46 ++++ .../InstantiatingHttpJsonChannelProvider.java | 49 ++-- .../gax/httpjson/ManagedHttpJsonChannel.java | 41 +--- .../ManagedHttpJsonInterceptorChannel.java | 83 +++++++ .../HttpJsonClientInterceptorTest.java | 217 ++++++++++++++++++ .../httpjson/HttpJsonDirectCallableTest.java | 20 +- ...JsonDirectServerStreamingCallableTest.java | 19 +- 13 files changed, 703 insertions(+), 86 deletions(-) create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCall.java create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCallListener.java create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientInterceptor.java create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonHeaderInterceptor.java create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonInterceptorProvider.java create mode 100644 gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonInterceptorChannel.java create mode 100644 gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientInterceptorTest.java diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCall.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCall.java new file mode 100644 index 000000000..8539b2b50 --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCall.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.core.BetaApi; +import javax.annotation.Nullable; + +/** + * A {@link HttpJsonClientCall} which forwards all of its methods to another {@link + * HttpJsonClientCall}. + */ +@BetaApi +public abstract class ForwardingHttpJsonClientCall + extends HttpJsonClientCall { + + protected abstract HttpJsonClientCall delegate(); + + @Override + public void start(Listener responseListener, HttpJsonMetadata requestHeaders) { + delegate().start(responseListener, requestHeaders); + } + + @Override + public void request(int numMessages) { + delegate().request(numMessages); + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + delegate().cancel(message, cause); + } + + @Override + public void halfClose() { + delegate().halfClose(); + } + + @Override + public void sendMessage(RequestT message) { + delegate().sendMessage(message); + } + + /** + * A simplified version of {@link ForwardingHttpJsonClientCall} where subclasses can pass in a + * {@link HttpJsonClientCall} as the delegate. + */ + public abstract static class SimpleForwardingHttpJsonClientCall + extends ForwardingHttpJsonClientCall { + + private final HttpJsonClientCall delegate; + + protected SimpleForwardingHttpJsonClientCall(HttpJsonClientCall delegate) { + this.delegate = delegate; + } + + @Override + protected HttpJsonClientCall delegate() { + return delegate; + } + } +} diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCallListener.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCallListener.java new file mode 100644 index 000000000..8c64b957f --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ForwardingHttpJsonClientCallListener.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.httpjson; + +import com.google.api.core.BetaApi; + +/** + * A {@link HttpJsonClientCall.Listener} which forwards all of its methods to another {@link + * HttpJsonClientCall.Listener}. + */ +@BetaApi +public abstract class ForwardingHttpJsonClientCallListener + extends HttpJsonClientCall.Listener { + + protected abstract HttpJsonClientCall.Listener delegate(); + + @Override + public void onHeaders(HttpJsonMetadata responseHeaders) { + delegate().onHeaders(responseHeaders); + } + + @Override + public void onMessage(ResponseT message) { + delegate().onMessage(message); + } + + @Override + public void onClose(int statusCode, HttpJsonMetadata trailers) { + delegate().onClose(statusCode, trailers); + } + + /** + * A simplified version of {@link ForwardingHttpJsonClientCallListener} where subclasses can pass + * in a {@link HttpJsonClientCall.Listener} as the delegate. + */ + public abstract static class SimpleForwardingHttpJsonClientCallListener + extends ForwardingHttpJsonClientCallListener { + + private final HttpJsonClientCall.Listener delegate; + + protected SimpleForwardingHttpJsonClientCallListener( + HttpJsonClientCall.Listener delegate) { + this.delegate = delegate; + } + + @Override + protected HttpJsonClientCall.Listener delegate() { + return delegate; + } + } +} diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonChannel.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonChannel.java index 558816c4d..96db96b46 100644 --- a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonChannel.java +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonChannel.java @@ -29,7 +29,6 @@ */ package com.google.api.gax.httpjson; -import com.google.api.core.ApiFuture; import com.google.api.core.BetaApi; /** HttpJsonChannel contains the functionality to issue http-json calls. */ @@ -37,10 +36,4 @@ public interface HttpJsonChannel { HttpJsonClientCall newCall( ApiMethodDescriptor methodDescriptor, HttpJsonCallOptions callOptions); - - @Deprecated - ApiFuture issueFutureUnaryCall( - HttpJsonCallOptions callOptions, - RequestT request, - ApiMethodDescriptor methodDescriptor); } diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index 42be4d28c..9f52712b2 100644 --- a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -34,13 +34,11 @@ import com.google.api.gax.httpjson.HttpRequestRunnable.ResultListener; import com.google.api.gax.httpjson.HttpRequestRunnable.RunnableResult; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; -import java.util.Map; import java.util.Queue; import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; @@ -48,7 +46,7 @@ import javax.annotation.concurrent.GuardedBy; /** - * This class servers as main implementation of {@link HttpJsonClientCall} for rest transport and is + * This class serves as main implementation of {@link HttpJsonClientCall} for REST transport and is * expected to be used for every REST call. It currently supports unary and server-streaming * workflows. The overall behavior and surface of the class mimics as close as possible behavior of * the corresponding ClientCall implementation in gRPC transport. @@ -90,7 +88,6 @@ final class HttpJsonClientCallImpl private final ApiMethodDescriptor methodDescriptor; private final HttpTransport httpTransport; private final Executor executor; - private final HttpJsonMetadata defaultHeaders; // // Request-specific data (provided by client code) before we get a response. @@ -124,15 +121,13 @@ final class HttpJsonClientCallImpl String endpoint, HttpJsonCallOptions callOptions, HttpTransport httpTransport, - Executor executor, - HttpJsonMetadata defaultHeaders) { + Executor executor) { this.methodDescriptor = methodDescriptor; this.endpoint = endpoint; this.callOptions = callOptions; this.httpTransport = httpTransport; this.executor = executor; this.closed = false; - this.defaultHeaders = defaultHeaders; } @Override @@ -164,12 +159,7 @@ public void start(Listener responseListener, HttpJsonMetadata request } Preconditions.checkState(this.listener == null, "The call is already started"); this.listener = responseListener; - Map mergedHeaders = - ImmutableMap.builder() - .putAll(defaultHeaders.getHeaders()) - .putAll(requestHeaders.getHeaders()) - .build(); - this.requestHeaders = requestHeaders.toBuilder().setHeaders(mergedHeaders).build(); + this.requestHeaders = requestHeaders; } } diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientInterceptor.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientInterceptor.java new file mode 100644 index 000000000..446fe09f2 --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientInterceptor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.core.BetaApi; + +/** + * Interface for intercepting outgoing calls before they are dispatched by a {@link + * HttpJsonChannel}. + * + *

The interceptor may be called for multiple {@link HttpJsonClientCall calls} by one or more + * threads without completing the previous ones first. The implementations must be thread-safe. + */ +@BetaApi +public interface HttpJsonClientInterceptor { + /** + * Intercept {@link HttpJsonClientCall} creation by the {@code next} {@link HttpJsonChannel}. + * + * @param method the remote method to be called + * @param callOptions the runtime options to be applied to this call + * @param next the channel which is being intercepted + * @return the call object for the remote operation, never {@code null} + */ + HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next); +} diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonHeaderInterceptor.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonHeaderInterceptor.java new file mode 100644 index 000000000..0f58181cb --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonHeaderInterceptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * An interceptor to handle custom headers. + * + *

Package-private for internal usage. + */ +class HttpJsonHeaderInterceptor implements HttpJsonClientInterceptor { + + private final Map staticHeaders; + + public HttpJsonHeaderInterceptor(Map staticHeaders) { + this.staticHeaders = staticHeaders; + } + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + final HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + HttpJsonClientCall call = next.newCall(method, callOptions); + return new SimpleForwardingHttpJsonClientCall(call) { + @Override + public void start( + HttpJsonClientCall.Listener responseListener, HttpJsonMetadata headers) { + Map mergedHeaders = + ImmutableMap.builder() + .putAll(headers.getHeaders()) + .putAll(staticHeaders) + .build(); + + super.start(responseListener, headers.toBuilder().setHeaders(mergedHeaders).build()); + } + }; + } +} diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonInterceptorProvider.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonInterceptorProvider.java new file mode 100644 index 000000000..4c5ddb475 --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonInterceptorProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.core.BetaApi; +import java.util.List; + +/** Provider of custom REST ClientInterceptors. */ +@BetaApi( + "The surface for adding custom interceptors is not stable yet and may change in the future.") +public interface HttpJsonInterceptorProvider { + + /** + * Get the list of client interceptors. + * + * @return interceptors + */ + List getInterceptors(); +} diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java index ca92d0fbe..2d3f73f33 100644 --- a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java @@ -36,7 +36,6 @@ import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.rpc.FixedHeaderProvider; import com.google.api.gax.rpc.HeaderProvider; -import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.mtls.MtlsProvider; import com.google.auth.Credentials; @@ -44,7 +43,6 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; @@ -64,29 +62,24 @@ @BetaApi @InternalExtensionOnly public final class InstantiatingHttpJsonChannelProvider implements TransportChannelProvider { + private final Executor executor; private final HeaderProvider headerProvider; + private final HttpJsonInterceptorProvider interceptorProvider; private final String endpoint; private final HttpTransport httpTransport; private final MtlsProvider mtlsProvider; - private InstantiatingHttpJsonChannelProvider( - Executor executor, HeaderProvider headerProvider, String endpoint) { - this.executor = executor; - this.headerProvider = headerProvider; - this.endpoint = endpoint; - this.httpTransport = null; - this.mtlsProvider = new MtlsProvider(); - } - private InstantiatingHttpJsonChannelProvider( Executor executor, HeaderProvider headerProvider, + HttpJsonInterceptorProvider interceptorProvider, String endpoint, HttpTransport httpTransport, MtlsProvider mtlsProvider) { this.executor = executor; this.headerProvider = headerProvider; + this.interceptorProvider = interceptorProvider; this.endpoint = endpoint; this.httpTransport = httpTransport; this.mtlsProvider = mtlsProvider; @@ -152,7 +145,7 @@ public String getTransportName() { } @Override - public TransportChannel getTransportChannel() throws IOException { + public HttpJsonTransportChannel getTransportChannel() throws IOException { if (needsHeaders()) { throw new IllegalStateException("getTransportChannel() called when needsHeaders() is true"); } else { @@ -185,9 +178,7 @@ HttpTransport createHttpTransport() throws IOException, GeneralSecurityException return null; } - private TransportChannel createChannel() throws IOException, GeneralSecurityException { - Map headers = new HashMap<>(headerProvider.getHeaders()); - + private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecurityException { HttpTransport httpTransportToUse = httpTransport; if (httpTransportToUse == null) { httpTransportToUse = createHttpTransport(); @@ -196,11 +187,20 @@ private TransportChannel createChannel() throws IOException, GeneralSecurityExce ManagedHttpJsonChannel channel = ManagedHttpJsonChannel.newBuilder() .setEndpoint(endpoint) - .setDefaultHeaders(HttpJsonMetadata.newBuilder().setHeaders(headers).build()) .setExecutor(executor) .setHttpTransport(httpTransportToUse) .build(); + HttpJsonClientInterceptor headerInterceptor = + new HttpJsonHeaderInterceptor(headerProvider.getHeaders()); + + channel = new ManagedHttpJsonInterceptorChannel(channel, headerInterceptor); + if (interceptorProvider != null && interceptorProvider.getInterceptors() != null) { + for (HttpJsonClientInterceptor interceptor : interceptorProvider.getInterceptors()) { + channel = new ManagedHttpJsonInterceptorChannel(channel, interceptor); + } + } + return HttpJsonTransportChannel.newBuilder().setManagedChannel(channel).build(); } @@ -223,8 +223,10 @@ public static Builder newBuilder() { } public static final class Builder { + private Executor executor; private HeaderProvider headerProvider; + private HttpJsonInterceptorProvider interceptorProvider; private String endpoint; private HttpTransport httpTransport; private MtlsProvider mtlsProvider = new MtlsProvider(); @@ -237,6 +239,7 @@ private Builder(InstantiatingHttpJsonChannelProvider provider) { this.endpoint = provider.endpoint; this.httpTransport = provider.httpTransport; this.mtlsProvider = provider.mtlsProvider; + this.interceptorProvider = provider.interceptorProvider; } /** @@ -270,6 +273,18 @@ public Builder setHeaderProvider(HeaderProvider headerProvider) { return this; } + /** + * Sets the GrpcInterceptorProvider for this TransportChannelProvider. + * + *

The provider will be called once for each underlying gRPC ManagedChannel that is created. + * It is recommended to return a new list of new interceptors on each call so that interceptors + * are not shared among channels, but this is not required. + */ + public Builder setInterceptorProvider(HttpJsonInterceptorProvider interceptorProvider) { + this.interceptorProvider = interceptorProvider; + return this; + } + /** Sets the endpoint used to reach the service, eg "localhost:8080". */ public Builder setEndpoint(String endpoint) { this.endpoint = endpoint; @@ -294,7 +309,7 @@ Builder setMtlsProvider(MtlsProvider mtlsProvider) { public InstantiatingHttpJsonChannelProvider build() { return new InstantiatingHttpJsonChannelProvider( - executor, headerProvider, endpoint, httpTransport, mtlsProvider); + executor, headerProvider, interceptorProvider, endpoint, httpTransport, mtlsProvider); } } } diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java index d75152230..6fb4200d3 100644 --- a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java @@ -31,7 +31,6 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.core.ApiFuture; import com.google.api.core.BetaApi; import com.google.api.gax.core.BackgroundResource; import com.google.api.gax.core.InstantiatingExecutorProvider; @@ -45,25 +44,25 @@ /** Implementation of HttpJsonChannel which can issue http-json calls. */ @BetaApi public class ManagedHttpJsonChannel implements HttpJsonChannel, BackgroundResource { + private static final ExecutorService DEFAULT_EXECUTOR = InstantiatingExecutorProvider.newBuilder().build().getExecutor(); private final Executor executor; private final String endpoint; - private final HttpJsonMetadata defaultHeaders; private final HttpTransport httpTransport; private boolean isTransportShutdown; + protected ManagedHttpJsonChannel() { + this(null, null, null); + } + private ManagedHttpJsonChannel( - Executor executor, - String endpoint, - @Nullable HttpTransport httpTransport, - HttpJsonMetadata defaultHeaders) { + Executor executor, String endpoint, @Nullable HttpTransport httpTransport) { this.executor = executor; this.endpoint = endpoint; this.httpTransport = httpTransport == null ? new NetHttpTransport() : httpTransport; - this.defaultHeaders = defaultHeaders; } @Override @@ -71,18 +70,7 @@ public HttpJsonClientCall newCall( ApiMethodDescriptor methodDescriptor, HttpJsonCallOptions callOptions) { return new HttpJsonClientCallImpl<>( - methodDescriptor, endpoint, callOptions, httpTransport, executor, defaultHeaders); - } - - @Override - @Deprecated - public ApiFuture issueFutureUnaryCall( - HttpJsonCallOptions callOptions, - RequestT request, - ApiMethodDescriptor methodDescriptor) { - - return HttpJsonClientCalls.eagerFutureUnaryCall( - newCall(methodDescriptor, callOptions), request); + methodDescriptor, endpoint, callOptions, httpTransport, executor); } @Override @@ -123,15 +111,13 @@ public boolean awaitTermination(long duration, TimeUnit unit) throws Interrupted public void close() {} public static Builder newBuilder() { - return new Builder() - .setDefaultHeaders(HttpJsonMetadata.newBuilder().build()) - .setExecutor(DEFAULT_EXECUTOR); + return new Builder().setExecutor(DEFAULT_EXECUTOR); } public static class Builder { + private Executor executor; private String endpoint; - private HttpJsonMetadata defaultHeaders; private HttpTransport httpTransport; private Builder() {} @@ -146,11 +132,6 @@ public Builder setEndpoint(String endpoint) { return this; } - public Builder setDefaultHeaders(HttpJsonMetadata defaultHeaders) { - this.defaultHeaders = defaultHeaders; - return this; - } - public Builder setHttpTransport(HttpTransport httpTransport) { this.httpTransport = httpTransport; return this; @@ -158,7 +139,9 @@ public Builder setHttpTransport(HttpTransport httpTransport) { public ManagedHttpJsonChannel build() { Preconditions.checkNotNull(endpoint); - return new ManagedHttpJsonChannel(executor, endpoint, httpTransport, defaultHeaders); + + return new ManagedHttpJsonChannel( + executor, endpoint, httpTransport == null ? new NetHttpTransport() : httpTransport); } } } diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonInterceptorChannel.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonInterceptorChannel.java new file mode 100644 index 000000000..05321e2fd --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonInterceptorChannel.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.core.BetaApi; +import java.util.concurrent.TimeUnit; + +@BetaApi +class ManagedHttpJsonInterceptorChannel extends ManagedHttpJsonChannel { + + private final ManagedHttpJsonChannel channel; + private final HttpJsonClientInterceptor interceptor; + + ManagedHttpJsonInterceptorChannel( + ManagedHttpJsonChannel channel, HttpJsonClientInterceptor interceptor) { + super(); + this.channel = channel; + this.interceptor = interceptor; + } + + @Override + public HttpJsonClientCall newCall( + ApiMethodDescriptor methodDescriptor, HttpJsonCallOptions callOptions) { + return interceptor.interceptCall(methodDescriptor, callOptions, channel); + } + + @Override + public synchronized void shutdown() { + channel.shutdown(); + } + + @Override + public boolean isShutdown() { + return channel.isShutdown(); + } + + @Override + public boolean isTerminated() { + return channel.isTerminated(); + } + + @Override + public void shutdownNow() { + channel.shutdownNow(); + } + + @Override + public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { + return channel.awaitTermination(duration, unit); + } + + @Override + public void close() { + channel.close(); + } +} diff --git a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientInterceptorTest.java b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientInterceptorTest.java new file mode 100644 index 000000000..52c0460b9 --- /dev/null +++ b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientInterceptorTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2017 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener; +import com.google.api.gax.httpjson.testing.MockHttpService; +import com.google.protobuf.Field; +import com.google.protobuf.Field.Cardinality; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.threeten.bp.Duration; + +@RunWith(JUnit4.class) +public class HttpJsonClientInterceptorTest { + private static class CapturingClientInterceptor implements HttpJsonClientInterceptor { + // Manually capturing arguments instead of using Mockito. This is intentional, as this + // specific test interceptor class represents a typical interceptor implementation. Doing the + // same with mocks will simply make this whole test less readable. + private volatile HttpJsonMetadata capturedResponseHeaders; + private volatile Object capturedMessage; + private volatile int capturedStatusCode; + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + HttpJsonClientCall call = next.newCall(method, callOptions); + return new SimpleForwardingHttpJsonClientCall(call) { + @Override + public void start(Listener responseListener, HttpJsonMetadata requestHeaders) { + Listener forwardingResponseListener = + new SimpleForwardingHttpJsonClientCallListener(responseListener) { + @Override + public void onHeaders(HttpJsonMetadata responseHeaders) { + capturedResponseHeaders = responseHeaders; + super.onHeaders(responseHeaders); + } + + @Override + public void onMessage(ResponseT message) { + capturedMessage = message; + super.onMessage(message); + } + + @Override + public void onClose(int statusCode, HttpJsonMetadata trailers) { + capturedStatusCode = statusCode; + super.onClose(statusCode, trailers); + } + }; + + super.start(forwardingResponseListener, requestHeaders); + } + }; + } + } + + private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.v1.Fake/FakeMethod") + .setHttpMethod("POST") + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/fake/v1/name/{name}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = ProtoRestSerializer.create(); + serializer.putPathParam(fields, "name", request.getName()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = ProtoRestSerializer.create(); + serializer.putQueryParam(fields, "number", request.getNumber()); + return fields; + }) + .setRequestBodyExtractor( + request -> + ProtoRestSerializer.create() + .toBody("*", request.toBuilder().clearName().build())) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Field.getDefaultInstance()) + .build()) + .build(); + + private static final MockHttpService MOCK_SERVICE = + new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); + + private static ExecutorService executorService; + + private CapturingClientInterceptor interceptor; + private ManagedHttpJsonChannel channel; + + @BeforeClass + public static void initialize() { + executorService = + Executors.newFixedThreadPool( + 2, + r -> { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + }); + } + + @AfterClass + public static void destroy() { + executorService.shutdownNow(); + } + + @Before + public void setUp() throws IOException { + interceptor = new CapturingClientInterceptor(); + channel = + InstantiatingHttpJsonChannelProvider.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .setHeaderProvider(() -> Collections.singletonMap("header-key", "headerValue")) + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) + .build() + .getTransportChannel() + .getManagedChannel(); + } + + @After + public void tearDown() { + MOCK_SERVICE.reset(); + } + + @Test + public void testCustomInterceptor() throws ExecutionException, InterruptedException { + HttpJsonDirectCallable callable = + new HttpJsonDirectCallable<>(FAKE_METHOD_DESCRIPTOR); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(channel) + .withTimeout(Duration.ofSeconds(30)); + + Field request; + Field expectedResponse; + request = + expectedResponse = + Field.newBuilder() // "echo" service + .setName("imTheBestField") + .setNumber(2) + .setCardinality(Cardinality.CARDINALITY_OPTIONAL) + .setDefaultValue("blah") + .build(); + + MOCK_SERVICE.addResponse(expectedResponse); + + Field actualResponse = callable.futureCall(request, callContext).get(); + + // Test that the interceptors did not affect normal execution + assertThat(actualResponse).isEqualTo(expectedResponse); + assertThat(MOCK_SERVICE.getRequestPaths().size()).isEqualTo(1); + String headerValue = MOCK_SERVICE.getRequestHeaders().get("header-key").iterator().next(); + + // Test that internal interceptor worked (the one which inserts headers) + assertThat(headerValue).isEqualTo("headerValue"); + + // Test that the custom interceptor was called + assertThat(interceptor.capturedStatusCode).isEqualTo(200); + assertThat(interceptor.capturedResponseHeaders).isNotNull(); + assertThat(interceptor.capturedMessage).isEqualTo(request); + } +} diff --git a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectCallableTest.java b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectCallableTest.java index 394f7df32..d7a88e7e9 100644 --- a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectCallableTest.java +++ b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectCallableTest.java @@ -51,9 +51,13 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; import org.threeten.bp.Duration; +@RunWith(JUnit4.class) public class HttpJsonDirectCallableTest { + private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = ApiMethodDescriptor.newBuilder() .setFullMethodName("google.cloud.v1.Fake/FakeMethod") @@ -90,15 +94,13 @@ public class HttpJsonDirectCallableTest { new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); private final ManagedHttpJsonChannel channel = - ManagedHttpJsonChannel.newBuilder() - .setEndpoint("google.com:443") - .setDefaultHeaders( - HttpJsonMetadata.newBuilder() - .setHeaders(Collections.singletonMap("header-key", "headerValue")) - .build()) - .setExecutor(executorService) - .setHttpTransport(MOCK_SERVICE) - .build(); + new ManagedHttpJsonInterceptorChannel( + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .build(), + new HttpJsonHeaderInterceptor(Collections.singletonMap("header-key", "headerValue"))); private static ExecutorService executorService; diff --git a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectServerStreamingCallableTest.java b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectServerStreamingCallableTest.java index 094b09e49..b929e4dce 100644 --- a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectServerStreamingCallableTest.java +++ b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonDirectServerStreamingCallableTest.java @@ -48,7 +48,6 @@ import com.google.protobuf.Field; import com.google.type.Color; import com.google.type.Money; -import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -131,17 +130,15 @@ public static void destroy() { } @Before - public void setUp() throws InstantiationException, IllegalAccessException, IOException { + public void setUp() { ManagedHttpJsonChannel channel = - ManagedHttpJsonChannel.newBuilder() - .setEndpoint("google.com:443") - .setDefaultHeaders( - HttpJsonMetadata.newBuilder() - .setHeaders(Collections.singletonMap("header-key", "headerValue")) - .build()) - .setExecutor(executorService) - .setHttpTransport(MOCK_SERVICE) - .build(); + new ManagedHttpJsonInterceptorChannel( + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .build(), + new HttpJsonHeaderInterceptor(Collections.singletonMap("header-key", "headerValue"))); clientContext = ClientContext.newBuilder()