From fd9425d27d83d14be7109e72dfc4f386608f7e4c Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 13 Aug 2024 09:39:59 -0400 Subject: [PATCH] Implements the gRPC MP Client API (#9026) Implements the gRPC MP Client API. This API uses interfaces to create client proxies. It introduces two new annotations, GrpcProxy and GrpcChannel (also defined as part of the Grpc namespace). Client proxies can be injected into CDI beans to access remote gRPC services. Signed-off-by: Santiago Pericas-Geertsen --- all/pom.xml | 4 + bom/pom.xml | 5 + .../main/java/io/helidon/grpc/api/Grpc.java | 55 +- .../io/helidon/grpc/core/package-info.java | 2 +- microprofile/grpc/client/pom.xml | 179 +++++ .../grpc/client/ChannelProducer.java | 97 +++ .../grpc/client/ClientMethodDescriptor.java | 414 +++++++++++ .../microprofile/grpc/client/ClientProxy.java | 70 ++ .../grpc/client/ClientRequestAttribute.java | 58 ++ .../grpc/client/ClientServiceDescriptor.java | 655 ++++++++++++++++++ .../grpc/client/DelegatingBeanAttributes.java | 98 +++ .../GrpcChannelDescriptorBlueprint.java | 70 ++ .../GrpcChannelsDescriptorBlueprint.java | 35 + .../grpc/client/GrpcChannelsProvider.java | 178 +++++ .../grpc/client/GrpcClientBuilder.java | 195 ++++++ .../grpc/client/GrpcClientCdiExtension.java | 107 +++ .../grpc/client/GrpcConfigurablePort.java | 35 + .../grpc/client/GrpcProxyBuilder.java | 79 +++ .../grpc/client/GrpcProxyProducer.java | 69 ++ .../grpc/client/GrpcServiceClient.java | 483 +++++++++++++ .../grpc/client/package-info.java | 20 + .../client/src/main/java/module-info.java | 40 ++ .../client/ClientMethodDescriptorTest.java | 139 ++++ .../client/ClientServiceDescriptorTest.java | 292 ++++++++ .../grpc/client/EchoServiceTest.java | 147 ++++ .../grpc/client/JavaMarshaller.java | 71 ++ .../src/test/java/services/EchoService.java | 53 ++ .../test/java/services/TreeMapService.java | 271 ++++++++ .../grpc/client/src/test/proto/echo.proto | 30 + .../grpc/client/src/test/proto/strings.proto | 31 + .../src/test/resources/application.yaml | 48 ++ .../grpc/client/src/test/resources/client.p12 | Bin 0 -> 4181 bytes .../src/test/resources/logging.properties | 33 + .../grpc/client/src/test/resources/server.p12 | Bin 0 -> 4133 bytes microprofile/grpc/pom.xml | 1 + .../helidon/webclient/grpc/GrpcChannel.java | 11 +- .../io/helidon/webclient/grpc/GrpcClient.java | 7 + .../webclient/grpc/GrpcClientImpl.java | 5 + 38 files changed, 4067 insertions(+), 20 deletions(-) create mode 100644 microprofile/grpc/client/pom.xml create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java create mode 100644 microprofile/grpc/client/src/main/java/module-info.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java create mode 100644 microprofile/grpc/client/src/test/java/services/EchoService.java create mode 100644 microprofile/grpc/client/src/test/java/services/TreeMapService.java create mode 100644 microprofile/grpc/client/src/test/proto/echo.proto create mode 100644 microprofile/grpc/client/src/test/proto/strings.proto create mode 100644 microprofile/grpc/client/src/test/resources/application.yaml create mode 100644 microprofile/grpc/client/src/test/resources/client.p12 create mode 100644 microprofile/grpc/client/src/test/resources/logging.properties create mode 100644 microprofile/grpc/client/src/test/resources/server.p12 diff --git a/all/pom.xml b/all/pom.xml index 7f710b8ad82..0abd9ea96f9 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -299,6 +299,10 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-server + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + io.helidon.microprofile.grpc helidon-microprofile-grpc-tracing diff --git a/bom/pom.xml b/bom/pom.xml index b5532308d1a..d6b6d76825a 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -115,6 +115,11 @@ helidon-microprofile-grpc-server ${helidon.version} + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + ${helidon.version} + io.helidon.microprofile.grpc helidon-microprofile-grpc-tracing diff --git a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java index 4b763f4ec22..4f5a50c2e70 100644 --- a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java +++ b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java @@ -25,10 +25,6 @@ import io.grpc.MethodDescriptor; -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - /** * The Helidon gRPC API. */ @@ -163,8 +159,8 @@ public interface Grpc { * It is required when a {@linkplain io.helidon.grpc.api.Grpc.GrpcInterceptorBinding interceptor binding} * is used.

*/ - @Retention(RUNTIME) - @Target(TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) @Documented @interface GrpcInterceptor { } @@ -173,8 +169,8 @@ public interface Grpc { * Specifies that an annotation type is a gRPC interceptor binding type. A gRPC Interceptor binding is * used to specify the binding of a gRPC client or server interceptor to target gRPC service and methods. */ - @Target(ANNOTATION_TYPE) - @Retention(RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @Retention(RetentionPolicy.RUNTIME) @Documented @interface GrpcInterceptorBinding { } @@ -198,8 +194,8 @@ public interface Grpc { * public void updateOrder(Order order) { ... } * */ - @Target({TYPE, ElementType.METHOD}) - @Retention(RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) @interface GrpcInterceptors { /** * An ordered list of interceptors. @@ -213,8 +209,8 @@ public interface Grpc { * An annotation used to annotate a type or method to specify the * named marshaller supplier to use for rpc method calls. */ - @Target({TYPE, ElementType.METHOD}) - @Retention(RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface GrpcMarshaller { @@ -240,8 +236,8 @@ public interface Grpc { * An annotation to mark a class as representing a gRPC service * or a method as a gRPC service method. */ - @Target({ElementType.METHOD, ANNOTATION_TYPE}) - @Retention(RUNTIME) + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface GrpcMethod { @@ -265,8 +261,8 @@ public interface Grpc { /** * An annotation to indicate the request type of gRPC method. */ - @Target({ElementType.METHOD}) - @Retention(RUNTIME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface RequestType { @@ -281,8 +277,8 @@ public interface Grpc { /** * An annotation to indicate the response type of a gRPC method. */ - @Target({ElementType.METHOD}) - @Retention(RUNTIME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface ResponseType { @@ -293,4 +289,27 @@ public interface Grpc { */ Class value(); } + + /** + * An annotation that can be used to specify the name of a configured gRPC channel. + */ + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @interface GrpcChannel { + + /** + * The name of the configured channel. + * + * @return name of the channel + */ + String value(); + } + + /** + * An annotation used to mark an injection point for a gRPC service client proxy. + */ + @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @interface GrpcProxy { + } } diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java index a10d014bfed..4c5124c1aa9 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java @@ -15,6 +15,6 @@ */ /** - * Core classes used by both the gRPC server API and gRPC client API. + * Core classes used by both the gRPC server and gRPC client. */ package io.helidon.grpc.core; diff --git a/microprofile/grpc/client/pom.xml b/microprofile/grpc/client/pom.xml new file mode 100644 index 00000000000..f2b2c481fba --- /dev/null +++ b/microprofile/grpc/client/pom.xml @@ -0,0 +1,179 @@ + + + + + 4.0.0 + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-project + 4.1.0-SNAPSHOT + + + helidon-microprofile-grpc-client + Helidon Microprofile gRPC Client + + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.grpc + helidon-grpc-api + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + + + io.helidon.config + helidon-config + + + io.helidon.webclient + helidon-webclient-grpc + + + io.helidon.common + helidon-common-tls + + + jakarta.inject + jakarta.inject-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + io.helidon.webserver + helidon-webserver-grpc + test + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-server + test + + + io.helidon.config + helidon-config-yaml + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + javax.annotation + javax.annotation-api + true + + + org.mockito + mockito-core + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + \ No newline at end of file diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java new file mode 100644 index 00000000000..9a1cb1f3c5f --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import io.helidon.config.Config; +import io.helidon.grpc.api.Grpc; + +import io.grpc.Channel; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; + +/** + * A producer of gRPC {@link io.grpc.Channel Channels}. + */ +@ApplicationScoped +public class ChannelProducer { + + private final GrpcChannelsProvider provider; + private final ReentrantLock lock = new ReentrantLock(); + private final Map channelMap = new HashMap<>(); + + /** + * Create a {@link ChannelProducer}. + * + * @param config the {@link io.helidon.config.Config} to use to configure + * the provided {@link io.grpc.Channel}s + */ + @Inject + ChannelProducer(Config config) { + provider = GrpcChannelsProvider.create(config.get("grpc.client")); + } + + /** + * Produces a gRPC {@link io.grpc.Channel}. + * + * @param injectionPoint the injection point + * @return a gRPC {@link io.grpc.Channel} + */ + @Produces + @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + public Channel get(InjectionPoint injectionPoint) { + Grpc.GrpcChannel qualifier = injectionPoint.getQualifiers() + .stream() + .filter(q -> q.annotationType().equals(Grpc.GrpcChannel.class)) + .map(q -> (Grpc.GrpcChannel) q) + .findFirst() + .orElse(null); + + String name = (qualifier == null) ? GrpcChannelsProvider.DEFAULT_CHANNEL_NAME : qualifier.value(); + return findChannel(name); + } + + /** + * Produces the default gRPC {@link io.grpc.Channel}. + * + * @return the default gRPC {@link io.grpc.Channel} + */ + @Produces + public Channel getDefaultChannel() { + return findChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME); + } + + /** + * Obtain the named {@link io.grpc.Channel}. + * + * @param name the channel name + * @return the named {@link io.grpc.Channel} + */ + public Channel findChannel(String name) { + try { + lock.lock(); + return channelMap.computeIfAbsent(name, provider::channel); + } finally { + lock.unlock(); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java new file mode 100644 index 00000000000..01a1260ac71 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.Arrays; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.WeightedBag; + +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; + +/** + * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping a {@link MethodDescriptor}, + * this class also holds the request and response types of the gRPC method. A + * {@link ClientServiceDescriptor} can contain zero or more {@link MethodDescriptor}. + *

+ * An instance of ClientMethodDescriptor can be created either from an existing {@link MethodDescriptor} or + * from one of the factory methods {@link #bidirectional(String, String)}, {@link #clientStreaming(String, String)}, + * {@link #serverStreaming(String, String)} or {@link #unary(String, String)}. + */ +public final class ClientMethodDescriptor { + + /** + * The simple name of the method. + */ + private final String name; + + /** + * The {@link MethodDescriptor} for this method. This is usually obtained from protocol buffer + * method getDescriptor (from service getDescriptor). + */ + private final MethodDescriptor descriptor; + + /** + * The list of client interceptors for this method. + */ + private final WeightedBag interceptors; + + /** + * The {@link CallCredentials} for this method. + */ + private final CallCredentials callCredentials; + + /** + * The method handler for this method. + */ + private final MethodHandler methodHandler; + + private ClientMethodDescriptor(String name, + MethodDescriptor descriptor, + WeightedBag interceptors, + CallCredentials callCredentials, + MethodHandler methodHandler) { + this.name = name; + this.descriptor = descriptor; + this.interceptors = interceptors; + this.callCredentials = callCredentials; + this.methodHandler = methodHandler; + } + + /** + * Creates a new {@link Builder} with the specified name and {@link MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder} + * @return A new instance of a {@link Builder} + */ + static Builder builder(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return new Builder(serviceName, name, descriptor); + } + + /** + * Creates a new {@link Builder} with the specified + * name and {@link MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder} + * @return a new instance of a {@link Builder} + */ + static ClientMethodDescriptor create(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return builder(serviceName, name, descriptor).build(); + } + + /** + * Creates a new unary {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder unary(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.UNARY); + } + + /** + * Creates a new client Streaming {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder clientStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.CLIENT_STREAMING); + } + + /** + * Creates a new server streaming {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder serverStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.SERVER_STREAMING); + } + + /** + * Creates a new bidirectional {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder bidirectional(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING); + } + + /** + * Return the {@link CallCredentials} set on this service. + * + * @return the {@link CallCredentials} set on this service + */ + public CallCredentials callCredentials() { + return this.callCredentials; + } + + /** + * Creates a new {@link Builder} with the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @param methodType the gRPC method type + * @return a new instance of a {@link Builder} + */ + static Builder builder(String serviceName, + String name, + MethodDescriptor.MethodType methodType) { + + MethodDescriptor.Builder builder = MethodDescriptor.newBuilder() + .setFullMethodName(serviceName + "/" + name) + .setType(methodType); + + return new Builder(serviceName, name, builder) + .requestType(Object.class) + .responseType(Object.class); + } + + /** + * Returns the simple name of the method. + * + * @return The simple name of the method. + */ + public String name() { + return name; + } + + /** + * Returns the {@link MethodDescriptor} of this method. + * + * @param the request type + * @param the response type + * @return The {@link MethodDescriptor} of this method. + */ + @SuppressWarnings("unchecked") + public MethodDescriptor descriptor() { + return (MethodDescriptor) descriptor; + } + + /** + * Obtain the {@link ClientInterceptor}s to use for this method. + * + * @return the {@link ClientInterceptor}s to use for this method + */ + WeightedBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Obtain the {@link MethodHandler} to use to make client calls. + * + * @return the {@link MethodHandler} to use to make client calls + */ + public MethodHandler methodHandler() { + return methodHandler; + } + + /** + * ClientMethod configuration API. + */ + public interface Rules { + + /** + * Sets the type of parameter of this method. + * + * @param type The type of parameter of this method. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules requestType(Class type); + + /** + * Sets the type of parameter of this method. + * + * @param type The type of parameter of this method. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules responseType(Class type); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the method. + * + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + */ + Rules intercept(ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the method. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ClientInterceptor}s to register + * @return this {@link Rules} to allow fluent method chaining + */ + Rules intercept(int priority, ClientInterceptor... interceptors); + + /** + * Register the {@link MarshallerSupplier} for the method. + *

+ * If not set the default {@link MarshallerSupplier} from the service will be used. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Register the specified {@link CallCredentials} to be used for this method. This overrides + * any {@link CallCredentials} set on the {@link ClientServiceDescriptor}. + * + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(CallCredentials callCredentials); + + /** + * Set the {@link MethodHandler} that can be used to invoke the method. + * + * @param methodHandler the {@link MethodHandler} to use + * @return this {@link Rules} instance for fluent call chaining + */ + Rules methodHandler(MethodHandler methodHandler); + } + + /** + * {@link MethodDescriptor} builder implementation. + */ + public static class Builder + implements Rules, io.helidon.common.Builder { + + private String name; + private final MethodDescriptor.Builder descriptor; + private Class requestType; + private Class responseType; + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.create(); + private MarshallerSupplier marshallerSupplier; + private CallCredentials callCredentials; + private MethodHandler methodHandler; + + /** + * Constructs a new Builder instance. + * + * @param serviceName The name of the service ths method belongs to + * @param name the name of this method + * @param descriptor The gRPC method descriptor builder + */ + Builder(String serviceName, String name, MethodDescriptor.Builder descriptor) { + this.name = name; + this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name); + } + + @Override + public Builder requestType(Class type) { + this.requestType = type; + return this; + } + + @Override + public Builder responseType(Class type) { + this.responseType = type; + return this; + } + + @Override + public Builder intercept(ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Builder intercept(int priority, ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier supplier) { + this.marshallerSupplier = supplier; + return this; + } + + Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { + if (supplier == null) { + this.defaultMarshallerSupplier = MarshallerSupplier.create(); + } else { + this.defaultMarshallerSupplier = supplier; + } + return this; + } + + @Override + public Builder methodHandler(MethodHandler methodHandler) { + this.methodHandler = methodHandler; + return this; + } + + /** + * Sets the full name of this Method. + * + * @param fullName the full name of the method + * @return this builder instance for fluent API + */ + Builder fullName(String fullName) { + descriptor.setFullMethodName(fullName); + this.name = fullName.substring(fullName.lastIndexOf('/') + 1); + return this; + } + + @Override + public Rules callCredentials(CallCredentials callCredentials) { + this.callCredentials = callCredentials; + return this; + } + + /** + * Builds and returns a new instance of {@link ClientMethodDescriptor}. + * + * @return a new instance of {@link ClientMethodDescriptor} + */ + @Override + @SuppressWarnings("unchecked") + public ClientMethodDescriptor build() { + MarshallerSupplier supplier = this.marshallerSupplier; + + if (supplier == null) { + supplier = defaultMarshallerSupplier; + } + + if (requestType != null) { + descriptor.setRequestMarshaller((MethodDescriptor.Marshaller) supplier.get(requestType)); + } + + if (responseType != null) { + descriptor.setResponseMarshaller((MethodDescriptor.Marshaller) supplier.get(responseType)); + } + + return new ClientMethodDescriptor(name, + descriptor.build(), + interceptors, + callCredentials, + methodHandler); + } + + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java new file mode 100644 index 00000000000..18883921d95 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * A dynamic proxy that forwards methods to gRPC call handlers. + */ +class ClientProxy implements InvocationHandler { + + private final GrpcServiceClient client; + + /** + * A map of Java method name to gRPR method name. + */ + private final Map names; + + /** + * Create a {@link ClientProxy}. + * + * @param client the {@link GrpcServiceClient} to use + * @param names a map of Java method names to gRPC method names + */ + private ClientProxy(GrpcServiceClient client, Map names) { + this.client = client; + this.names = names; + } + + /** + * Create a {@link ClientProxy} instance. + * + * @param client the {@link GrpcServiceClient} to use + * @param names a map of Java method names to gRPC method names + * @return a {@link ClientProxy} instance for the specified service client + */ + static ClientProxy create(GrpcServiceClient client, Map names) { + return new ClientProxy(client, names); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + return client.invoke(names.get(method.getName()), args); + } + + /** + * Obtain the underlying {@link GrpcServiceClient}. + * + * @return the underlying {@link GrpcServiceClient} + */ + public GrpcServiceClient getClient() { + return client; + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java new file mode 100644 index 00000000000..4e47dfd6b34 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +/** + * An enum of possible gRPC client call attributes to attach to + * call tracing spans. + */ +public enum ClientRequestAttribute { + /** + * Add the method type to the tracing span. + */ + METHOD_TYPE, + + /** + * Add the method name to the tracing span. + */ + METHOD_NAME, + + /** + * Add the call deadline to the tracing span. + */ + DEADLINE, + + /** + * Add the compressor type to the tracing span. + */ + COMPRESSOR, + + /** + * Add the security authority to the tracing span. + */ + AUTHORITY, + + /** + * Add the method call options to the tracing span. + */ + ALL_CALL_OPTIONS, + + /** + * Add the method call headers to the tracing span. + */ + HEADERS +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java new file mode 100644 index 00000000000..f31ed4a9754 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java @@ -0,0 +1,655 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.WeightedBag; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import io.grpc.BindableService; +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.ServiceDescriptor; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * Encapsulates all details about a client side gRPC service. + */ +public class ClientServiceDescriptor { + + private final String serviceName; + private final Map methods; + private final WeightedBag interceptors; + private final CallCredentials callCredentials; + + private ClientServiceDescriptor(String serviceName, + Map methods, + WeightedBag interceptors, + CallCredentials callCredentials) { + this.serviceName = serviceName; + this.methods = methods; + this.interceptors = interceptors; + this.callCredentials = callCredentials; + } + + /** + * Create a {@link ClientServiceDescriptor} from a {@link ServiceDescriptor}. + * + * @param descriptor the {@link ServiceDescriptor} + * @return a {@link ClientServiceDescriptor} + */ + public static ClientServiceDescriptor create(ServiceDescriptor descriptor) { + return builder(descriptor).build(); + } + + /** + * Create a {@link ClientServiceDescriptor} from a {@link BindableService}. + * + * @param service the BindableService + * @return a {@link ClientServiceDescriptor} + */ + public static ClientServiceDescriptor create(BindableService service) { + return builder(service).build(); + } + + /** + * Create a {@link Builder} from a {@link ServiceDescriptor}. + * + * @param service the {@link ServiceDescriptor} + * @return a {@link Builder} + */ + public static Builder builder(ServiceDescriptor service) { + return new Builder(service); + } + + /** + * Create a {@link Builder} from a {@link BindableService}. + * + * @param service the {@link BindableService} + * @return a {@link Builder} + */ + public static Builder builder(BindableService service) { + return new Builder(service); + } + + /** + * Create a {@link Builder} form a name and type. + *

+ * The {@link Class#getSimpleName() class simple name} will be used for the service name. + * + * @param serviceClass the service class + * @return a {@link Builder} + */ + public static Builder builder(Class serviceClass) { + try { + Method method = serviceClass.getMethod("getServiceDescriptor"); + if (method.getReturnType() == ServiceDescriptor.class) { + ServiceDescriptor svcDesc = (ServiceDescriptor) method.invoke(null); + return builder(svcDesc); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException itEx) { + // Ignored. + } + return builder(serviceClass.getSimpleName(), serviceClass); + } + + /** + * Create a {@link Builder} form a name and type. + * + * @param serviceName the getName of the service to use to initialise the builder + * @param serviceClass the service class + * @return a {@link Builder} + */ + public static Builder builder(String serviceName, Class serviceClass) { + return new Builder(serviceName, serviceClass); + } + + /** + * Obtain the service name. + * + * @return the service name + */ + public String name() { + return serviceName; + } + + /** + * Return {@link ClientMethodDescriptor} for a specified method getName. + * + * @param name method getName + * @return method getDescriptor for the specified getName + */ + public ClientMethodDescriptor method(String name) { + return methods.get(name); + } + + /** + * Return the collections of methods that make up this service. + * + * @return service methods + */ + public Collection methods() { + return Collections.unmodifiableCollection(methods.values()); + } + + /** + * Return service interceptors. + * + * @return service interceptors + */ + public WeightedBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Return the {@link CallCredentials} set on this service. + * + * @return the {@link CallCredentials} set on this service + */ + public CallCredentials callCredentials() { + return this.callCredentials; + } + + @Override + public String toString() { + return "ClientServiceDescriptor(name='" + serviceName + "')"; + } + + // ---- inner interface: Rules ----------------------------------------- + + /** + * Fluent configuration interface for the {@link ClientServiceDescriptor}. + */ + public interface Rules { + /** + * Obtain the name fo the service this configuration configures. + * + * @return the name fo the service this configuration configures + */ + String name(); + + /** + * Set the name for the service. + * + * @param name the name of service + * @return this {@link Rules} instance for fluent call chaining + * @throws NullPointerException if the getName is null + * @throws IllegalArgumentException if the getName is a blank String + */ + Rules name(String name); + + /** + * Register the proto file for the service. + * + * @param proto the service proto + * @return this {@link Rules} instance for fluent call chaining + */ + Rules proto(Descriptors.FileDescriptor proto); + + /** + * Register the {@link MarshallerSupplier} for the service. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the service. + * + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + */ + Rules intercept(ClientInterceptor... interceptors); + + /** + * Add one or more {@link ClientInterceptor} instances that will intercept calls + * to this service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ClientInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for a named method of the service. + * + * @param methodName the name of the method to intercept + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + * @throws IllegalArgumentException if no method exists for the specified getName + */ + Rules intercept(String methodName, ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for a named method of the service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param methodName the name of the method to intercept + * @param priority the priority to assign to the interceptors + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + * + * @throws IllegalArgumentException if no method exists for the specified name + */ + Rules intercept(String methodName, int priority, ClientInterceptor... interceptors); + + /** + * Register unary method for the service. + * + * @param name The getName of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name, Consumer configurer); + + /** + * Register server streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, Consumer configurer); + + /** + * Register client streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, Consumer configurer); + + /** + * Register bi-directional streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, Consumer configurer); + + /** + * Register the {@link CallCredentials} to be used for this service. + * + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(CallCredentials callCredentials); + + /** + * Register the {@link CallCredentials} to be used for the specified method in this service. This overrides + * any {@link CallCredentials} set on this {@link ClientServiceDescriptor} + * + * @param name the method name + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(String name, CallCredentials callCredentials); + + } + + // ---- inner class: BaseBuilder -------------------------------------------- + + /** + * A {@link ClientServiceDescriptor} builder. + */ + public static final class Builder implements Rules, io.helidon.common.Builder { + + private String name; + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private final Class serviceClass; + private Descriptors.FileDescriptor proto; + private MarshallerSupplier marshallerSupplier = MarshallerSupplier.create(); + private CallCredentials callCredentials; + + private final Map methodBuilders = new HashMap<>(); + + /** + * Builds the ClientService from a {@link BindableService}. + * + * @param service the {@link BindableService} to use to initialize the builder + */ + private Builder(BindableService service) { + this(service.bindService().getServiceDescriptor()); + } + + /** + * Builds the ClientService from a {@link BindableService}. + * + * @param serviceDescriptor the {@link ServiceDescriptor} to use to initialize the builder + */ + private Builder(ServiceDescriptor serviceDescriptor) { + this.name = serviceDescriptor.getName(); + this.serviceClass = serviceDescriptor.getClass(); + + for (io.grpc.MethodDescriptor md : serviceDescriptor.getMethods()) { + String methodName = extractMethodName(md.getFullMethodName()); + + methodBuilders.put(methodName, ClientMethodDescriptor.builder(this.name, methodName, md.toBuilder())); + } + } + + /** + * Create a new {@link Builder}. + * + * @param name the service name + * @param serviceClass the service class + */ + private Builder(String name, Class serviceClass) { + this.name = name; + this.serviceClass = serviceClass; + } + + @Override + public String name() { + return name; + } + + @Override + public Builder name(String serviceName) { + if (serviceName == null) { + throw new NullPointerException("Service getName cannot be null"); + } + + if (serviceName.trim().isEmpty()) { + throw new IllegalArgumentException("Service getName cannot be blank"); + } + + this.name = serviceName.trim(); + for (Map.Entry e : methodBuilders.entrySet()) { + e.getValue().fullName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, e.getKey())); + } + return this; + } + + @Override + public Builder proto(Descriptors.FileDescriptor proto) { + this.proto = proto; + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier marshallerSupplier) { + this.marshallerSupplier = marshallerSupplier; + return this; + } + + @Override + public Builder unary(String name) { + return unary(name, null); + } + + @Override + public Builder unary(String name, Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.UNARY, configurer)); + return this; + } + + @Override + public Builder serverStreaming(String name) { + return serverStreaming(name, null); + } + + @Override + public Builder serverStreaming(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.SERVER_STREAMING, configurer)); + return this; + } + + @Override + public Builder clientStreaming(String name) { + return clientStreaming(name, null); + } + + @Override + public Builder clientStreaming(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.CLIENT_STREAMING, configurer)); + return this; + } + + @Override + public Builder bidirectional(String name) { + return bidirectional(name, null); + } + + @Override + public Builder bidirectional(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.BIDI_STREAMING, configurer)); + return this; + } + + @Override + public Builder intercept(ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Rules intercept(int priority, ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder intercept(String methodName, ClientInterceptor... interceptors) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.intercept(interceptors); + + return this; + } + + @Override + public Builder intercept(String methodName, int priority, ClientInterceptor... interceptors) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.intercept(priority, interceptors); + + return this; + } + + @Override + public Builder callCredentials(CallCredentials callCredentials) { + this.callCredentials = callCredentials; + return this; + } + + @Override + public Builder callCredentials(String methodName, CallCredentials callCredentials) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.callCredentials(callCredentials); + return this; + } + + @Override + public ClientServiceDescriptor build() { + Map methods = new LinkedHashMap<>(); + for (Map.Entry entry : methodBuilders.entrySet()) { + methods.put(entry.getKey(), entry.getValue().build()); + } + + return new ClientServiceDescriptor(name, methods, interceptors, callCredentials); + } + + // ---- helpers ----------------------------------------------------- + + private ClientMethodDescriptor.Builder createMethodDescriptor( + String methodName, + MethodType methodType, + Consumer configurer) { + + io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() + .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, methodName)) + .setType(methodType) + .setSampledToLocalTracing(true); + + Class requestType = getTypeFromMethodDescriptor(methodName, true); + Class responseType = getTypeFromMethodDescriptor(methodName, false); + + ClientMethodDescriptor.Builder builder = ClientMethodDescriptor.builder(this.name, methodName, grpcDesc) + .defaultMarshallerSupplier(this.marshallerSupplier) + .requestType(requestType) + .responseType(responseType); + + if (configurer != null) { + configurer.accept(builder); + } + + return builder; + } + + private Class getTypeFromMethodDescriptor(String methodName, boolean fInput) { + + // if the proto is not present, assume that we are not using + // protobuf for marshalling and that whichever marshaller is used + // doesn't need type information (basically, that the serialized + // stream is self-describing) + if (proto == null) { + return Object.class; + } + + // todo: add error handling here, and fail fast with a more + // todo: meaningful exception (and message) than a NPE + // todo: if the service or the method cannot be found + Descriptors.ServiceDescriptor svc = proto.findServiceByName(name); + Descriptors.MethodDescriptor mtd = svc.findMethodByName(methodName); + Descriptors.Descriptor type = fInput ? mtd.getInputType() : mtd.getOutputType(); + + String pkg = getPackageName(); + String outerClass = getOuterClassName(); + + // make sure that any nested protobuf class names are converted + // into a proper Java binary class getName + String className = pkg + "." + outerClass + type.getFullName().replace('.', '$'); + + // the assumption here is that the protobuf generated classes can always + // be loaded by the same class loader that loaded the service class, + // as the service implementation is bound to depend on them + try { + return serviceClass != null + ? serviceClass.getClassLoader().loadClass(className) + : this.getClass().getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String getPackageName() { + String pkg = proto.getOptions().getJavaPackage(); + return "".equals(pkg) ? proto.getPackage() : pkg; + } + + private String getOuterClassName() { + DescriptorProtos.FileOptions options = proto.getOptions(); + if (options.getJavaMultipleFiles()) { + // there is no outer class -- each message will have its own top-level class + return ""; + } + + String outerClass = options.getJavaOuterClassname(); + if ("".equals(outerClass)) { + outerClass = getOuterClassFromFileName(proto.getName()); + } + + // append $ in order to timed a proper binary getName for the nested message class + return outerClass + "$"; + } + + private String getOuterClassFromFileName(String name) { + // strip .proto extension + name = name.substring(0, name.lastIndexOf(".proto")); + + String[] words = name.split("_"); + StringBuilder sb = new StringBuilder(name.length()); + + for (String word : words) { + sb.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)); + } + + return sb.toString(); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java new file mode 100644 index 00000000000..04333d9fd81 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import jakarta.enterprise.inject.spi.BeanAttributes; + +/** + * A {@link jakarta.enterprise.inject.spi.BeanAttributes} implementation. + * + * @param the class of the bean instance + */ +class DelegatingBeanAttributes implements BeanAttributes { + + private final BeanAttributes delegate; + private final Set types; + + /** + * Create a {@link DelegatingBeanAttributes}. + * + * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to + * @param types the {@link java.lang.reflect.Type}s for this bean + */ + private DelegatingBeanAttributes(BeanAttributes delegate, Set types) { + super(); + Objects.requireNonNull(delegate); + this.delegate = delegate; + this.types = Collections.unmodifiableSet(types); + } + + /** + * Create a {@link DelegatingBeanAttributes}. + * + * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to + * @param types the {@link java.lang.reflect.Type}s for this bean + */ + static DelegatingBeanAttributes create(BeanAttributes delegate, Set types) { + return new DelegatingBeanAttributes<>(delegate, types); + } + + @Override + public String getName() { + return this.delegate.getName(); + } + + @Override + public Set getQualifiers() { + return this.delegate.getQualifiers(); + } + + @Override + public Class getScope() { + return this.delegate.getScope(); + } + + @Override + public Set> getStereotypes() { + return this.delegate.getStereotypes(); + } + + @Override + public Set getTypes() { + if (types == null || types.isEmpty()) { + return this.delegate.getTypes(); + } else { + return types; + } + } + + @Override + public boolean isAlternative() { + return this.delegate.isAlternative(); + } + + @Override + public String toString() { + return this.delegate.toString(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java new file mode 100644 index 00000000000..54e7b8da1a9 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.tls.Tls; + +@Prototype.Blueprint +@Prototype.Configured +interface GrpcChannelDescriptorBlueprint { + + /** + * The name of this channel. + * + * @return channel name + */ + @Option.Configured + String name(); + + /** + * The host to connect to. + * + * @return the host + */ + @Option.Configured + @Option.Default(GrpcChannelsProvider.DEFAULT_HOST) + String host(); + + /** + * The port to connect to. + * + * @return the port + */ + @Option.Configured + @Option.DefaultInt(GrpcChannelsProvider.DEFAULT_PORT) + int port(); + + /** + * The target URI. + * + * @return the URI + */ + @Option.Configured + Optional target(); + + /** + * TLS configuration for the connection. + * + * @return the TLS config + */ + @Option.Configured + Optional tls(); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java new file mode 100644 index 00000000000..c6932921197 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.List; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +@Prototype.Configured +interface GrpcChannelsDescriptorBlueprint { + + /** + * List of channel descriptors. + * + * @return channel descriptors + */ + @Option.Configured + List channels(); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java new file mode 100644 index 00000000000..aacbc1f25d6 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.common.tls.Tls; +import io.helidon.config.Config; +import io.helidon.webclient.grpc.GrpcClient; + +import io.grpc.Channel; + +/** + * GrpcChannelsProvider is a factory for pre-configured gRPC Channel instances. + */ +public class GrpcChannelsProvider { + + /** + * A constant for holding the default channel configuration name (which is "default"). + */ + public static final String DEFAULT_CHANNEL_NAME = "default"; + + /** + * A constant for holding the default host name (which is "localhost"). + */ + public static final String DEFAULT_HOST = "localhost"; + + /** + * A constant for holding the default port (which is "1408"). + */ + public static final int DEFAULT_PORT = 1408; + + private final Map channelConfigs; + + private GrpcChannelsProvider(Map channelDescriptors) { + this.channelConfigs = new HashMap<>(channelDescriptors); + } + + /** + * Builds a new instance of {@link GrpcChannelsProvider} using default configuration. The + * default configuration connects to "localhost:1408" without TLS. + * + * @return a new instance of {@link GrpcChannelsProvider} + */ + public static GrpcChannelsProvider create() { + return GrpcChannelsProvider.builder().build(); + } + + /** + * Creates a {@link GrpcChannelsProvider} using the specified configuration. + * + * @param config The externalized configuration. + * @return a new instance of {@link GrpcChannelsProvider} + */ + public static GrpcChannelsProvider create(Config config) { + return new Builder(config).build(); + } + + /** + * Create a new {@link Builder}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return builder(null); + } + + /** + * Create a new {@link Builder}. + * + * @param config the {@link Config} to bootstrap from + * @return a new {@link Builder} + */ + public static Builder builder(Config config) { + return new Builder(config); + } + + /** + * Returns a {@link Channel} for the specified channel or host name. + *

+ * If the specified channel name does not exist in the configuration, we will assume + * that it represents the name of the gRPC host to connect to and will create a plain text + * channel to the host with the specified {@code name}, on a default port (1408). + * + * @param name the name of the channel configuration as specified in the configuration file, + * or the name of the host to connect to + * @return a new instance of {@link Channel} + * @throws NullPointerException if name is null + * @throws IllegalArgumentException if name is empty + */ + public Channel channel(String name) { + if (name == null) { + throw new NullPointerException("name cannot be null."); + } + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("name cannot be empty or blank."); + } + GrpcChannelDescriptor chCfg = channelConfigs.computeIfAbsent(name, hostName -> + GrpcChannelDescriptor.builder().name(name).host(name).build()); + return createChannel(name, chCfg); + } + + /** + * Creates a channel from a channel descriptor. + * + * @param name the channel name + * @param descriptor the channel descriptor + * @return the channel + */ + private Channel createChannel(String name, GrpcChannelDescriptor descriptor) { + Tls clientTls = descriptor.tls().orElse(null); + if (clientTls == null) { + throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client"); + } + int port = descriptor.port(); + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://" + descriptor.host() + ":" + port) + .build(); + return grpcClient.channel(); + } + + /** + * Builder builds an instance of {@link GrpcChannelsProvider}. + */ + public static class Builder implements io.helidon.common.Builder { + + private final Map channelConfigs = new HashMap<>(); + + private Builder(Config config) { + // Add the default channel (which can be overridden in the config) + channel(DEFAULT_CHANNEL_NAME, GrpcChannelDescriptor.builder().name(DEFAULT_CHANNEL_NAME).build()); + + if (config == null) { + return; + } + + GrpcChannelsDescriptor channelsDescriptor = GrpcChannelsDescriptor.builder().config(config).build(); + channelsDescriptor.channels().forEach(ch -> channelConfigs.put(ch.name(), ch)); + } + + /** + * Add or replace the specified {@link GrpcChannelDescriptor}. + * + * @param name the name of the configuration + * @param descriptor the {@link GrpcChannelDescriptor} to be added + * @return this Builder instance + */ + public Builder channel(String name, GrpcChannelDescriptor descriptor) { + channelConfigs.put(name, descriptor); + return this; + } + + /** + * Create a new instance of {@link GrpcChannelsProvider} from this Builder. + * + * @return a new instance of {@link GrpcChannelsProvider} + */ + public GrpcChannelsProvider build() { + return new GrpcChannelsProvider(channelConfigs); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java new file mode 100644 index 00000000000..b391de0116a --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.Builder; +import io.helidon.grpc.api.Grpc; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.microprofile.grpc.core.AbstractServiceBuilder; +import io.helidon.microprofile.grpc.core.AnnotatedMethod; +import io.helidon.microprofile.grpc.core.AnnotatedMethodList; +import io.helidon.microprofile.grpc.core.InstanceSupplier; +import io.helidon.microprofile.grpc.core.ModelHelper; + +import static java.lang.System.Logger.Level; + +/** + * A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor} instances + * from an annotated POJO. + */ +class GrpcClientBuilder extends AbstractServiceBuilder + implements Builder { + + private static final System.Logger LOGGER = System.getLogger(GrpcClientBuilder.class.getName()); + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @param instance the target instance to call gRPC handler methods on + * @throws NullPointerException if the service or instance parameters are null + */ + private GrpcClientBuilder(Class serviceClass, Supplier instance) { + super(serviceClass, instance); + } + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service. + * + * @param service the service to call gRPC handler methods on + * @throws NullPointerException if the service is null + * @return a {@link GrpcClientBuilder} + */ + static GrpcClientBuilder create(Object service) { + return new GrpcClientBuilder(service.getClass(), InstanceSupplier.singleton(service)); + } + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @throws NullPointerException if the service class is null + * @return a {@link GrpcClientBuilder} + */ + static GrpcClientBuilder create(Class serviceClass) { + return new GrpcClientBuilder(Objects.requireNonNull(serviceClass), createInstanceSupplier(serviceClass)); + } + + /** + * Create a new resource model builder for the introspected class. + *

+ * The model returned is filled with the introspected data. + *

+ * + * @return new resource model builder for the introspected class. + */ + @Override + public ClientServiceDescriptor.Builder build() { + checkForNonPublicMethodIssues(); + + Class annotatedServiceClass = annotatedServiceClass(); + AnnotatedMethodList methodList = AnnotatedMethodList.create(annotatedServiceClass); + String name = determineServiceName(annotatedServiceClass); + + ClientServiceDescriptor.Builder builder = ClientServiceDescriptor.builder(serviceClass()) + .name(name) + .marshallerSupplier(getMarshallerSupplier()); + + addServiceMethods(builder, methodList); + + LOGGER.log(Level.DEBUG, () -> String.format("A new gRPC service was created by ServiceModeller: %s", builder)); + + return builder; + } + + /** + * Add methods to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}. + * + * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to + * @param methodList the list of methods to add + */ + private void addServiceMethods(ClientServiceDescriptor.Builder builder, AnnotatedMethodList methodList) { + for (AnnotatedMethod am : methodList.withAnnotation(Grpc.GrpcMethod.class)) { + addServiceMethod(builder, am); + } + for (AnnotatedMethod am : methodList.withMetaAnnotation(Grpc.GrpcMethod.class)) { + addServiceMethod(builder, am); + } + } + + /** + * Add a method to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}. + *

+ * The method configuration will be determined by the annotations present on the + * method and the method signature. + * + * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to + * @param method the {@link io.helidon.microprofile.grpc.core.AnnotatedMethod} representing the method to add + */ + private void addServiceMethod(ClientServiceDescriptor.Builder builder, AnnotatedMethod method) { + Grpc.GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(Grpc.GrpcMethod.class); + String name = determineMethodName(method, annotation); + + MethodHandler handler = handlerSuppliers().stream() + .filter(supplier -> supplier.supplies(method)) + .findFirst() + .map(supplier -> supplier.get(name, method, instanceSupplier())) + .orElseThrow(() -> new IllegalArgumentException("Cannot locate a method handler supplier for method " + method)); + + Class requestType = handler.getRequestType(); + Class responseType = handler.getResponseType(); + AnnotatedMethodConfigurer configurer = new AnnotatedMethodConfigurer(method, requestType, responseType, handler); + + switch (annotation.value()) { + case UNARY: + builder.unary(name, configurer); + break; + case CLIENT_STREAMING: + builder.clientStreaming(name, configurer); + break; + case SERVER_STREAMING: + builder.serverStreaming(name, configurer); + break; + case BIDI_STREAMING: + builder.bidirectional(name, configurer); + break; + case UNKNOWN: + default: + LOGGER.log(Level.ERROR, () -> "Unrecognized method type " + annotation.value()); + } + } + + /** + * A {@link java.util.function.Consumer} of {@link io.helidon.microprofile.grpc.client.ClientMethodDescriptor.Rules} + * that applies configuration changes based on annotations present on the gRPC + * method. + */ + private static class AnnotatedMethodConfigurer + implements Consumer { + + private final AnnotatedMethod method; + private final Class requestType; + private final Class responseType; + private final MethodHandler methodHandler; + + private AnnotatedMethodConfigurer(AnnotatedMethod method, + Class requestType, + Class responseType, + MethodHandler methodHandler) { + this.method = method; + this.requestType = requestType; + this.responseType = responseType; + this.methodHandler = methodHandler; + } + + @Override + public void accept(ClientMethodDescriptor.Rules config) { + config.requestType(requestType) + .responseType(responseType) + .methodHandler(methodHandler); + + if (method.isAnnotationPresent(Grpc.GrpcMarshaller.class)) { + config.marshallerSupplier(ModelHelper.getMarshallerSupplier( + method.getAnnotation(Grpc.GrpcMarshaller.class))); + } + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java new file mode 100644 index 00000000000..d0ec1033a71 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import io.helidon.grpc.api.Grpc; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedMethod; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.BeanAttributes; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessInjectionPoint; +import jakarta.enterprise.inject.spi.ProducerFactory; + +/** + * A CDI extension to add gRPC client functionality. + */ +public class GrpcClientCdiExtension implements Extension { + + private final Set proxyTypes = new HashSet<>(); + + /** + * Adds beans to the bean manager. + * + * @param event before bean discovery event + */ + public void addBeans(@Observes BeforeBeanDiscovery event) { + event.addAnnotatedType(ChannelProducer.class, ChannelProducer.class.getName()); + } + + /** + * Process injection points. + *

+ * In this method injection points that have the {@link io.helidon.grpc.api.Grpc.GrpcProxy} are processed + * and their types are stored so that in the {@link #afterBean( + *jakarta.enterprise.inject.spi.AfterBeanDiscovery, jakarta.enterprise.inject.spi.BeanManager)} + * we can manually create a producer for the correct service proxy type. + * + * @param pip the injection point + * @param the declared type of the injection point. + * @param the bean class of the bean that declares the injection point + */ + public void gatherApplications(@Observes ProcessInjectionPoint pip) { + Annotated annotated = pip.getInjectionPoint().getAnnotated(); + if (annotated.isAnnotationPresent(Grpc.GrpcProxy.class)) { + Type type = pip.getInjectionPoint().getType(); + proxyTypes.add(type); + } + } + + /** + * Process the previously captured {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection points. + *

+ * For each {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection point we create a producer bean + * for the required type. + * + * @param event the {@link jakarta.enterprise.inject.spi.AfterBeanDiscovery} event + * @param beanManager the CDI bean manager + */ + public void afterBean(@Observes AfterBeanDiscovery event, BeanManager beanManager) { + AnnotatedType producerType = beanManager.createAnnotatedType(GrpcProxyProducer.class); + AnnotatedMethod producerMethod = producerType.getMethods() + .stream() + .filter(m -> m.isAnnotationPresent(Grpc.GrpcProxy.class)) + .filter(m -> m.isAnnotationPresent(Grpc.GrpcChannel.class)) + .findFirst() + .orElse(null); + if (producerMethod != null) { + for (Type type : proxyTypes) { + addProducerBean(event, beanManager, producerMethod, type); + } + } + } + + private void addProducerBean(AfterBeanDiscovery event, + BeanManager beanManager, + AnnotatedMethod producerMethod, + Type type) { + BeanAttributes producerAttributes = beanManager.createBeanAttributes(producerMethod); + ProducerFactory factory = beanManager.getProducerFactory(producerMethod, null); + Set types = Set.of(Object.class, type); + BeanAttributes beanAttributes = DelegatingBeanAttributes.create(producerAttributes, types); + event.addBean(beanManager.createBean(beanAttributes, GrpcProxyProducer.class, factory)); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java new file mode 100644 index 00000000000..9742daf7bac --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +/** + * Interface implemented by all gRPC client proxies. The method {@link #channelPort} can be + * called at runtime to override the client URI port from config. Typically used for testing. + */ +public interface GrpcConfigurablePort { + + /** + * Name of single setter method on this interface. + */ + String CHANNEL_PORT = "channelPort"; + + /** + * Overrides client URI port. + * + * @param value the new port value + */ + void channelPort(int value); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java new file mode 100644 index 00000000000..82d9bea7f00 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.common.Builder; + +import io.grpc.Channel; + +/** + * A builder for gRPC clients dynamic proxies. + * + * @param the type of the interface to be proxied + */ +public class GrpcProxyBuilder implements Builder, T> { + + private static final Map, ClientServiceDescriptor> DESCRIPTORS = new ConcurrentHashMap<>(); + + private final GrpcServiceClient client; + + private final Class type; + + private GrpcProxyBuilder(GrpcServiceClient client, Class type) { + this.client = client; + this.type = type; + } + + /** + * Create a {@code GrpcProxyBuilder} that can build gRPC dynamic proxies + * for a given gRPC service interface. + *

+ * The class passed to this method should be properly annotated with + * {@link io.helidon.grpc.api.Grpc.GrpcService} and + * {@link io.helidon.grpc.api.Grpc.GrpcMethod} annotations + * so that the proxy can properly route calls to the server. + * + * @param channel the {@link io.grpc.Channel} to connect to the server + * @param type the service type + * @param the service type + * @return a {@link io.helidon.microprofile.grpc.client.GrpcProxyBuilder} that can build dynamic proxies + * for the gRPC service + */ + public static GrpcProxyBuilder create(Channel channel, Class type) { + ClientServiceDescriptor descriptor = DESCRIPTORS.computeIfAbsent(type, GrpcProxyBuilder::createDescriptor); + return new GrpcProxyBuilder<>(GrpcServiceClient.builder(channel, descriptor).build(), type); + } + + /** + * Build a gRPC client dynamic proxy of the required type. + * + * @return a gRPC client dynamic proxy + */ + @Override + public T build() { + return client.proxy(type, GrpcConfigurablePort.class); + } + + private static ClientServiceDescriptor createDescriptor(Class type) { + GrpcClientBuilder builder = GrpcClientBuilder.create(type); + ClientServiceDescriptor.Builder descriptorBuilder = builder.build(); + return descriptorBuilder.build(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java new file mode 100644 index 00000000000..20fc3f9d5fa --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.core.ModelHelper; + +import io.grpc.Channel; +import jakarta.enterprise.inject.spi.InjectionPoint; + +/** + * A utility class of gRPC CDI producer stubs. + *

+ * The methods in this class are not real CDI producer methods, + * they act as templates that the {@link io.helidon.microprofile.grpc.client.GrpcClientCdiExtension} + * will use to create producers on the fly as injection points + * are observed. + */ +class GrpcProxyProducer { + + private GrpcProxyProducer() { + } + + /** + * A CDI producer method that produces a client proxy for a gRPC service that + * will connect to the server using the channel specified via + * {@link io.helidon.grpc.api.Grpc.GrpcChannel} annotation on the proxy interface + * or injection point, or the default {@link io.grpc.Channel}. + *

+ * This is not a real producer method but is used as a stub by the gRPC client + * CDI extension to create real producers as injection points are discovered. + * + * @param injectionPoint the injection point where the client proxy is to be injected + * @return a gRPC client proxy + */ + @Grpc.GrpcProxy + @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + static Object proxyUsingNamedChannel(InjectionPoint injectionPoint, ChannelProducer producer) { + Class type = ModelHelper.getGenericType(injectionPoint.getType()); + + String channelName; + if (injectionPoint.getAnnotated().isAnnotationPresent(Grpc.GrpcChannel.class)) { + channelName = injectionPoint.getAnnotated().getAnnotation(Grpc.GrpcChannel.class).value(); + } else { + channelName = type.isAnnotationPresent(Grpc.GrpcChannel.class) + ? type.getAnnotation(Grpc.GrpcChannel.class).value() + : GrpcChannelsProvider.DEFAULT_CHANNEL_NAME; + } + + Channel channel = producer.findChannel(channelName); + GrpcProxyBuilder builder = GrpcProxyBuilder.create(channel, type); + + return builder.build(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java new file mode 100644 index 00000000000..022a2293a36 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.webclient.api.ClientUri; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.Status; +import io.grpc.stub.AbstractStub; +import io.grpc.stub.ClientCalls; +import io.grpc.stub.StreamObserver; + +/** + * A gRPC Client for a specific gRPC service. + */ +public class GrpcServiceClient { + + private final Channel channel; + + private final HashMap> methodStubs; + + private final ClientServiceDescriptor clientServiceDescriptor; + + /** + * Creates a {@link Builder}. + * + * @param channel the {@link Channel} to use to connect to the server + * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service + * + * @return a new instance of {@link Builder} + */ + public static Builder builder(Channel channel, ClientServiceDescriptor descriptor) { + return new Builder(channel, descriptor); + } + + /** + * Creates a {@link GrpcServiceClient}. + * + * @param channel the {@link Channel} to use to connect to the server + * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service + * + * @return a new instance of {@link Builder} + */ + public static GrpcServiceClient create(Channel channel, ClientServiceDescriptor descriptor) { + return builder(channel, descriptor).build(); + } + + private GrpcServiceClient(Channel channel, + CallOptions callOptions, + ClientServiceDescriptor clientServiceDescriptor) { + this.channel = channel; + this.clientServiceDescriptor = clientServiceDescriptor; + this.methodStubs = new HashMap<>(); + + // Merge Interceptors specified in Channel, ClientServiceDescriptor and ClientMethodDescriptor. + // Add the merged interceptor list to the AbstractStub which will be be used for the invocation + // of the method. + for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) { + GrpcMethodStub methodStub = new GrpcMethodStub<>(channel, callOptions, methodDescriptor); + + WeightedBag priorityInterceptors = WeightedBag.create(InterceptorWeights.USER); + priorityInterceptors.addAll(clientServiceDescriptor.interceptors()); + priorityInterceptors.addAll(methodDescriptor.interceptors()); + List interceptors = priorityInterceptors.stream().toList(); + + if (!interceptors.isEmpty()) { + LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); + + // iterate the interceptors in reverse order so that the interceptor chain is in the correct order + for (int i = interceptors.size() - 1; i >= 0; i--) { + ClientInterceptor interceptor = interceptors.get(i); + if (!uniqueInterceptors.contains(interceptor)) { + uniqueInterceptors.add(interceptor); + } + } + + for (ClientInterceptor interceptor : uniqueInterceptors) { + methodStub = methodStub.withInterceptors(interceptor); + } + } + + if (methodDescriptor.callCredentials() != null) { + // Method level CallCredentials take precedence over service level CallCredentials. + methodStub = methodStub.withCallCredentials(methodDescriptor.callCredentials()); + } else if (clientServiceDescriptor.callCredentials() != null) { + methodStub = methodStub.withCallCredentials(clientServiceDescriptor.callCredentials()); + } + + methodStubs.put(methodDescriptor.name(), methodStub); + } + } + + /** + * Obtain the service name. + * + * @return The name of the service + */ + public String serviceName() { + return clientServiceDescriptor.name(); + } + + /** + * Invoke the specified method using the method's + * {@link MethodHandler}. + * + * @param name the name of the method to invoke + * @param args the method arguments + * @return the method response + */ + Object invoke(String name, Object[] args) { + GrpcMethodStub stub = methodStubs.get(name); + if (stub == null) { + // may be used during testing to override a channel's port + if (name.equals(GrpcConfigurablePort.CHANNEL_PORT)) { + io.helidon.webclient.grpc.GrpcChannel grpcChannel = (io.helidon.webclient.grpc.GrpcChannel) channel; + ClientUri uri = grpcChannel.grpcClient().clientConfig().baseUri().orElseThrow(); + uri.port((int) args[0]); + return null; + } + throw Status.INTERNAL.withDescription("gRPC method '" + name + "' does not exist").asRuntimeException(); + } + ClientMethodDescriptor descriptor = stub.descriptor(); + MethodHandler methodHandler = descriptor.methodHandler(); + + return switch (descriptor.descriptor().getType()) { + case UNARY -> methodHandler.unary(args, this::unary); + case CLIENT_STREAMING -> methodHandler.clientStreaming(args, this::clientStreaming); + case SERVER_STREAMING -> methodHandler.serverStreaming(args, this::serverStreaming); + case BIDI_STREAMING -> methodHandler.bidirectional(args, this::bidiStreaming); + default -> throw Status.INTERNAL.withDescription("Unknown or unsupported method type for method " + name) + .asRuntimeException(); + }; + } + + /** + * Create a dynamic proxy for the specified interface that proxies + * calls to the wrapped gRPC service. + * + * @param type the interface to create a proxy for + * @param extraTypes extra types for the proxy to implement + * @param the type of the returned proxy + * @return a dynamic proxy that calls methods on this gRPC service + */ + @SuppressWarnings("unchecked") + public T proxy(Class type, Class... extraTypes) { + Map names = new HashMap<>(); + names.put(GrpcConfigurablePort.CHANNEL_PORT, GrpcConfigurablePort.CHANNEL_PORT); // for testing + for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) { + MethodHandler methodHandler = methodDescriptor.methodHandler(); + if (methodHandler != null) { + names.put(methodHandler.javaMethodName(), methodDescriptor.name()); + } + } + + Class[] proxyTypes; + if (extraTypes == null || extraTypes.length == 0) { + proxyTypes = new Class[] {type}; + } else { + proxyTypes = new Class[extraTypes.length + 1]; + proxyTypes[0] = type; + System.arraycopy(extraTypes, 0, proxyTypes, 1, extraTypes.length); + } + return (T) Proxy.newProxyInstance(type.getClassLoader(), proxyTypes, ClientProxy.create(this, names)); + } + + /** + * Invoke the specified unary method with the specified request object. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return The result of this invocation + */ + public RespT blockingUnary(String methodName, ReqT request) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + return ClientCalls.blockingUnaryCall( + stub.getChannel(), stub.descriptor().descriptor(), stub.getCallOptions(), request); + } + + /** + * Asynchronously invoke the specified unary method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return A {@link CompletionStage} that will complete with the result of the unary method call + */ + public CompletionStage unary(String methodName, ReqT request) { + SingleValueStreamObserver observer = new SingleValueStreamObserver<>(); + + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + ClientCalls.asyncUnaryCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + + return observer.completionStage(); + } + + /** + * Invoke the specified unary method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + */ + public void unary(String methodName, ReqT request, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + ClientCalls.asyncUnaryCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + } + + /** + * Invoke the specified server streaming method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return an {@link Iterator} to obtain the streamed results + */ + public Iterator blockingServerStreaming(String methodName, ReqT request) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING); + return ClientCalls.blockingServerStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request); + } + + /** + * Invoke the specified server streaming method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param observer a {@link StreamObserver} to receive the results + * @param the request type + * @param the response type + */ + public void serverStreaming(String methodName, ReqT request, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING); + ClientCalls.asyncServerStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param items an {@link Iterable} of items to be streamed to the server + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to retrieve the method call result + */ + public CompletionStage clientStreaming(String methodName, Iterable items) { + return clientStreaming(methodName, StreamSupport.stream(items.spliterator(), false)); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param items a {@link Stream} of items to be streamed to the server + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to retrieve the method call result + */ + public CompletionStage clientStreaming(String methodName, Stream items) { + SingleValueStreamObserver obsv = new SingleValueStreamObserver<>(); + GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING); + StreamObserver reqStream = ClientCalls.asyncClientStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + obsv); + + items.forEach(reqStream::onNext); + reqStream.onCompleted(); + + return obsv.completionStage(); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + * @return a {@link StreamObserver} to use to stream requests to the server + */ + public StreamObserver clientStreaming(String methodName, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING); + return ClientCalls.asyncClientStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + observer); + } + + /** + * Invoke the specified bidirectional streaming method. + * + * @param methodName the method name to be invoked. + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to use to stream requests to the server + */ + public StreamObserver bidiStreaming(String methodName, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.BIDI_STREAMING); + return ClientCalls.asyncBidiStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + observer); + } + + @SuppressWarnings("unchecked") + private GrpcMethodStub ensureMethod(String methodName, MethodType methodType) { + GrpcMethodStub stub = (GrpcMethodStub) methodStubs.get(methodName); + if (stub == null) { + throw new IllegalArgumentException("No method named " + methodName + " registered with this service"); + } + ClientMethodDescriptor cmd = stub.descriptor(); + if (cmd.descriptor().getType() != methodType) { + throw new IllegalArgumentException("Method (" + methodName + ") already registered with a different method type."); + } + + return stub; + } + + /** + * GrpcMethodStub can be used to configure method specific Interceptors, Metrics, Tracing, Deadlines, etc. + */ + private static class GrpcMethodStub + extends AbstractStub> { + + private final ClientMethodDescriptor cmd; + + GrpcMethodStub(Channel channel, CallOptions callOptions, ClientMethodDescriptor cmd) { + super(channel, callOptions); + this.cmd = cmd; + } + + @Override + protected GrpcMethodStub build(Channel channel, CallOptions callOptions) { + return new GrpcMethodStub<>(channel, callOptions, cmd); + } + + public ClientMethodDescriptor descriptor() { + return cmd; + } + } + + /** + * Builder to build an instance of {@link GrpcServiceClient}. + */ + public static class Builder { + + private final Channel channel; + + private CallOptions callOptions = CallOptions.DEFAULT; + + private final ClientServiceDescriptor clientServiceDescriptor; + + private Builder(Channel channel, ClientServiceDescriptor descriptor) { + this.channel = channel; + this.clientServiceDescriptor = descriptor; + } + + /** + * Set the {@link CallOptions} to use. + * + * @param callOptions the {@link CallOptions} to use + * @return This {@link Builder} for fluent method chaining + */ + public Builder callOptions(CallOptions callOptions) { + this.callOptions = callOptions; + return this; + } + + /** + * Build an instance of {@link GrpcServiceClient}. + * + * @return an new instance of a {@link GrpcServiceClient} + */ + public GrpcServiceClient build() { + return new GrpcServiceClient(channel, callOptions, clientServiceDescriptor); + } + } + + /** + * A simple {@link StreamObserver} adapter class that completes + * a {@link CompletableFuture} when the observer is completed. + *

+ * This observer uses the value passed to its {@link #onNext(Object)} method to complete + * the {@link CompletableFuture}. + *

+ * This observer should only be used in cases where a single result is expected. If more + * that one call is made to {@link #onNext(Object)} then future will be completed with + * an exception. + * + * @param The type of objects received in this stream. + */ + public static class SingleValueStreamObserver implements StreamObserver { + + private int count; + + private T result; + + private final CompletableFuture resultFuture = new CompletableFuture<>(); + + /** + * Create a SingleValueStreamObserver. + */ + public SingleValueStreamObserver() { + } + + /** + * Obtain the {@link CompletableFuture} that will be completed + * when the {@link StreamObserver} completes. + * + * @return The CompletableFuture + */ + public CompletionStage completionStage() { + return resultFuture; + } + + @Override + public void onNext(T value) { + if (count++ == 0) { + result = value; + } else { + resultFuture.completeExceptionally(new IllegalStateException("More than one result received.")); + } + } + + @Override + public void onError(Throwable t) { + resultFuture.completeExceptionally(t); + } + + @Override + public void onCompleted() { + resultFuture.complete(result); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java new file mode 100644 index 00000000000..a3232b59001 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Client gRPC microprofile classes. + */ +package io.helidon.microprofile.grpc.client; diff --git a/microprofile/grpc/client/src/main/java/module-info.java b/microprofile/grpc/client/src/main/java/module-info.java new file mode 100644 index 00000000000..b4fa484d1e3 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/module-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * gRPC microprofile client module. + */ +module io.helidon.microprofile.grpc.client { + + requires io.helidon.common; + requires io.helidon.common.tls; + requires io.helidon.config; + requires io.helidon.webclient.grpc; + requires io.helidon.grpc.api; + requires io.helidon.microprofile.grpc.core; + + requires io.grpc; + requires jakarta.cdi; + requires jakarta.inject; + + requires transitive io.helidon.grpc.core; + + exports io.helidon.microprofile.grpc.client; + + provides jakarta.enterprise.inject.spi.Extension with + io.helidon.microprofile.grpc.client.GrpcClientCdiExtension; + +} \ No newline at end of file diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java new file mode 100644 index 00000000000..fc4b49190d8 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import io.helidon.microprofile.grpc.client.test.Echo; +import io.helidon.microprofile.grpc.client.test.EchoServiceGrpc; + +import io.grpc.MethodDescriptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; + +public class ClientMethodDescriptorTest { + + private MethodDescriptor.Builder grpcDescriptor; + + @BeforeEach + public void setup() { + grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")) + .toBuilder(); + } + + @Test + public void shouldCreateMethodDescriptorFromGrpcDescriptor() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.create("FooService", + "foo", + grpcDescriptor); + + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + MethodDescriptor expected = grpcDescriptor.build(); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(expected.getType())); + assertThat(methodDescriptor.getRequestMarshaller(), is(expected.getRequestMarshaller())); + assertThat(methodDescriptor.getResponseMarshaller(), is(expected.getResponseMarshaller())); + } + + @Test + public void shouldCreateBidirectionalMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.bidirectional("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + } + + @Test + public void shouldCreateClientStreamingMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.clientStreaming("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + } + + @Test + public void shouldCreateServerStreamingMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.serverStreaming("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + } + + @Test + public void shouldCreateUnaryMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.unary("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + } + + @Test + public void shouldSetName() { + ClientMethodDescriptor.Builder builder = ClientMethodDescriptor + .unary("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()); + + builder.fullName("Foo/Bar"); + + ClientMethodDescriptor descriptor = builder.build(); + + assertThat(descriptor.name(), is("Bar")); + assertThat(descriptor.descriptor().getFullMethodName(), is("Foo/Bar")); + } + + @Test + public void testMarshallerTypesForProtoBuilder() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor + .unary("EchoService", "Echo") + .requestType(Echo.EchoRequest.class) + .responseType(Echo.EchoResponse.class) + .build(); + + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getRequestMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class)); + assertThat(methodDescriptor.getResponseMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class)); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java new file mode 100644 index 00000000000..7d945d45fd3 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.Collection; + +import io.helidon.microprofile.grpc.client.test.StringServiceGrpc; + +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServiceDescriptor; +import org.junit.jupiter.api.Test; +import services.TreeMapService; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +public class ClientServiceDescriptorTest { + + @Test + public void shouldCreateDescriptorFromGrpcServiceDescriptor() { + ServiceDescriptor grpcDescriptor = StringServiceGrpc.getServiceDescriptor(); + ClientServiceDescriptor descriptor = ClientServiceDescriptor.create(grpcDescriptor); + String serviceName = "StringService"; + assertThat(descriptor.name(), is(serviceName)); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + Collection> expectedMethods = grpcDescriptor.getMethods(); + Collection actualMethods = descriptor.methods(); + assertThat(actualMethods.size(), is(expectedMethods.size())); + + for (MethodDescriptor methodDescriptor : expectedMethods) { + String name = methodDescriptor.getFullMethodName().substring(serviceName.length() + 1); + ClientMethodDescriptor method = descriptor.method(name); + assertThat(method.name(), is(name)); + assertThat(method.interceptors(), is(emptyIterable())); + MethodDescriptor actualDescriptor = method.descriptor(); + assertThat(actualDescriptor.getType(), is(methodDescriptor.getType())); + } + } + + @Test + public void shouldCreateDescriptorFromBindableService() { + StringServiceBindableService bindableService = new StringServiceBindableService(); + ClientServiceDescriptor descriptor = ClientServiceDescriptor.create(bindableService); + String serviceName = "StringService"; + assertThat(descriptor.name(), is(serviceName)); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + Collection> expectedMethods = bindableService.bindService().getMethods(); + Collection actualMethods = descriptor.methods(); + assertThat(actualMethods.size(), is(expectedMethods.size())); + + for (ServerMethodDefinition expectedMethod : expectedMethods) { + MethodDescriptor methodDescriptor = expectedMethod.getMethodDescriptor(); + String name = methodDescriptor.getFullMethodName().substring(serviceName.length() + 1); + ClientMethodDescriptor method = descriptor.method(name); + assertThat(method.name(), is(name)); + assertThat(method.interceptors(), is(emptyIterable())); + MethodDescriptor actualDescriptor = method.descriptor(); + assertThat(actualDescriptor.getType(), is(methodDescriptor.getType())); + } + } + + @Test + public void testServiceName() { + ClientServiceDescriptor.Builder builder = ClientServiceDescriptor.builder("TreeMapService", + TreeMapService.class); + assertThat(builder.name(), is("TreeMapService")); + + ClientServiceDescriptor descriptor = builder.build(); + assertThat(descriptor.name(), is("TreeMapService")); + } + + @Test + public void testDefaultMethodCount() { + ClientServiceDescriptor svcDesc = newClientServiceDescriptorBuilder(TreeMapService.class).build(); + assertThat(svcDesc.methods().size(), equalTo(0)); + } + + @Test + public void shouldNotAllowNullName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(NullPointerException.class, () -> builder.name(null)); + } + + @Test + public void shouldNotAllowEmptyStringName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(IllegalArgumentException.class, () -> builder.name("")); + } + + @Test + public void shouldNotAllowBlankName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(IllegalArgumentException.class, () -> builder.name(" \t ")); + } + + @Test + public void shouldAddBidirectionalMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .bidirectional("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddBidirectionalMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .bidirectional("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddClientStreamingMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .clientStreaming("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddClientStreamingMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .clientStreaming("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddServerStreamingMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .serverStreaming("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddServerStreamingMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .serverStreaming("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddUnaryMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddUnaryMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddInterceptor() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .intercept(interceptor) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor)); + } + + @Test + public void shouldAddInterceptors() { + ClientInterceptor interceptorOne = mock(ClientInterceptor.class); + ClientInterceptor interceptorTwo = mock(ClientInterceptor.class); + ClientInterceptor interceptorThree = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .intercept(interceptorOne) + .intercept(interceptorTwo, interceptorThree) + .build(); + + assertThat(descriptor.interceptors(), containsInAnyOrder(interceptorOne, interceptorTwo, interceptorThree)); + } + + @Test + public void shouldAddInterceptorToMethod() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo") + .intercept("foo", interceptor) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + } + + @Test + public void shouldSetNameOnMethods() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + ClientServiceDescriptor descriptor = builder.unary("bar") + .name("Foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("bar"); + assertThat(method.descriptor().getFullMethodName(), is("Foo/bar")); + } + + public static class StringServiceBindableService + extends StringServiceGrpc.StringServiceImplBase { + } + + private ClientServiceDescriptor.Builder newClientServiceDescriptorBuilder(Class service) { + return ClientServiceDescriptor.builder(service) + .marshallerSupplier(new JavaMarshaller.Supplier()); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java new file mode 100644 index 00000000000..68db09e9b91 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.server.GrpcMpCdiExtension; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.webclient.grpc.GrpcClient; + +import io.grpc.stub.StreamObserver; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.grpc.core.ResponseHelper.complete; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(EchoServiceTest.EchoService.class) +@AddBean(JavaMarshaller.Supplier.class) +@AddExtension(GrpcMpCdiExtension.class) +@AddExtension(GrpcClientCdiExtension.class) +class EchoServiceTest { + + @Inject + private WebTarget webTarget; + + @Inject + @Grpc.GrpcProxy + private EchoServiceClient proxyClient; + + @BeforeEach + void updatePort() { + if (proxyClient instanceof GrpcConfigurablePort client) { + client.channelPort(webTarget.getUri().getPort()); + } + } + + @Test + void testEcho() throws InterruptedException, ExecutionException, TimeoutException { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + webTarget.getUri().getPort()) + .build(); + + ClientServiceDescriptor descriptor = ClientServiceDescriptor.builder(EchoService.class) + .name("EchoService") + .marshallerSupplier(new JavaMarshaller.Supplier()) + .unary("Echo") + .build(); + + CompletableFuture future = new CompletableFuture<>(); + GrpcServiceClient client = GrpcServiceClient.create(grpcClient.channel(), descriptor); + StreamObserver observer = new StreamObserver<>() { + @Override + public void onNext(String value) { + future.complete(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + client.unary("Echo", "Howdy", observer); + assertThat(future.get(5, TimeUnit.SECONDS), is("Howdy")); + } + + @Test + void testEchoInject() throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver observer = new StreamObserver<>() { + @Override + public void onNext(String value) { + future.complete(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + proxyClient.echo("Howdy", observer); + assertThat(future.get(5, TimeUnit.SECONDS), is("Howdy")); + } + + @Grpc.GrpcService + @Grpc.GrpcMarshaller("java") + public static class EchoService { + + @Grpc.Unary("Echo") + public void echo(String request, StreamObserver observer) { + try { + complete(observer, request); + } catch (IllegalStateException e) { + observer.onError(e); + } + } + } + + @Grpc.GrpcService("EchoService") + @Grpc.GrpcMarshaller("java") + @Grpc.GrpcChannel("echo-channel") + public interface EchoServiceClient { + + @Grpc.Unary("Echo") + void echo(String request, StreamObserver observer); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java new file mode 100644 index 00000000000..de585accde5 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.grpc.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import io.helidon.grpc.core.MarshallerSupplier; + +import io.grpc.MethodDescriptor; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Named; + +/** + * An implementation of a gRPC {@link io.grpc.MethodDescriptor.Marshaller} that + * uses Java serialization for testing. + */ +public class JavaMarshaller implements MethodDescriptor.Marshaller { + + @Override + public InputStream stream(T obj) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(obj); + return new ByteArrayInputStream(out.toByteArray()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public T parse(InputStream in) { + try (ObjectInputStream ois = new ObjectInputStream(in)) { + return (T) ois.readObject(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * A {@link io.helidon.grpc.core.MarshallerSupplier} implementation that supplies + * instances of {@link io.helidon.microprofile.grpc.client.JavaMarshaller}. + */ + @Dependent + @Named("java") + public static class Supplier implements MarshallerSupplier { + + @Override + public MethodDescriptor.Marshaller get(Class clazz) { + return new JavaMarshaller<>(); + } + } +} diff --git a/microprofile/grpc/client/src/test/java/services/EchoService.java b/microprofile/grpc/client/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..a930b24555a --- /dev/null +++ b/microprofile/grpc/client/src/test/java/services/EchoService.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 services; + +import io.helidon.microprofile.grpc.client.test.Echo; +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +/** + * A simple test gRPC echo service. + */ +public class EchoService implements GrpcService { + + @Override + public Descriptors.FileDescriptor proto() { + return Echo.getDescriptor(); + } + + @Override + public void update(Routing routing) { + routing.unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/microprofile/grpc/client/src/test/java/services/TreeMapService.java b/microprofile/grpc/client/src/test/java/services/TreeMapService.java new file mode 100644 index 00000000000..973b87f8953 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/services/TreeMapService.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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 services; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; +import java.util.TreeMap; + +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +/** + * A simple class that implements gRPC service. Used for testing. + */ +public class TreeMapService implements GrpcService { + + /** + * A reference to a {@link services.TreeMapService.Person} named "Bilbo". + */ + public static Person BILBO = new Person(1, "Bilbo", 111, "Male", + new String[] {"Burglaring", "Pipe smoking"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Frodo". + */ + public static Person FRODO = new Person(2, "Frodo", 33, "Male", + new String[] {"Long hikes"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Aragon". + */ + public static Person ARAGON = new Person(3, "Aragon", 87, "Male", + new String[] {"Pipe smoking", "Hitting on elvish women"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Galadriel". + */ + public static Person GALARDRIEL = new Person(4, "Galadriel", 8372, "Female", + new String[] {"Dwarves"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Gandalf". + */ + public static Person GANDALF = new Person(5, "Gandalf", 32767, "Male", + new String[] {"Wizardry"}); + + private final TreeMap lorMap = new TreeMap<>(); + + public TreeMapService() { + lorMap.put(1, BILBO); + lorMap.put(2, FRODO); + lorMap.put(3, ARAGON); + lorMap.put(4, GALARDRIEL); + lorMap.put(5, GANDALF); + } + + @Override + public Descriptors.FileDescriptor proto() { + return null; + } + + @Override + public void update(Routing routing) { + // TODO routing.marshallerSupplier(new JavaMarshaller.Supplier()); + routing.unary("get", this::get); + routing.serverStream("greaterOrEqualTo", this::greaterOrEqualTo); + routing.clientStream("sumOfAges", this::sumOfAges); + routing.bidi("persons", this::persons); + } + + /** + * Retrieve the person from the TreeMap. This is a UNARY call. + * + * @param id The id of the person. + * @param observer the call response + */ + public void get(Integer id, StreamObserver observer) { + complete(observer, lorMap.get(id)); + } + + /** + * Return the Persons whose Ids are greater than or equal to the specified key. This is a ServerStreaming call. + * + * @param id The id to use. + * @param observer A {@link io.grpc.stub.StreamObserver} into which {@link services.TreeMapService.Person}s whose ids + * are greater than or equal to the specified id will be emitted. + */ + public void greaterOrEqualTo(Integer id, StreamObserver observer) { + for (Person p : lorMap.tailMap(id).values()) { + observer.onNext(p); + } + observer.onCompleted(); + } + + /** + * Return the sum of ages of all Persons whose Ids are streamed from the client. This is a Client streaming call. + * + * @param observer A {@link io.grpc.stub.StreamObserver} into which the sum of ages of + * all {@link services.TreeMapService.Person}s will be emitted. + * @return A {@link io.grpc.stub.StreamObserver} into which the ids of {@link services.TreeMapService.Person}s + * should be emitted into. + */ + public StreamObserver sumOfAges(StreamObserver observer) { + return new StreamObserver() { + private int sum = 0; + + public void onNext(Integer id) { + System.out.println("Received id: ==> " + id); + Person p = lorMap.get(id); + sum += p != null ? p.age : 0; + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onNext(sum); + observer.onCompleted(); + } + }; + } + + /** + * Streams the {@link services.TreeMapService.Person} into the specified observer for each of the id that is + * streamed (from the client). This is a bi-directional streaming call. + * + * @param observer A {@link io.grpc.stub.StreamObserver} into which the sum of ages of + * all {@link services.TreeMapService.Person}s will be emitted. + * @return A {@link io.grpc.stub.StreamObserver} into which the ids of {@link services.TreeMapService.Person}s + * should be emitted into. + */ + public StreamObserver persons(StreamObserver observer) { + return new StreamObserver() { + public void onNext(Integer id) { + Person p = lorMap.get(id); + if (p != null) { + observer.onNext(p); + } + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + /** + * A person class used in the test code. + */ + public static class Person implements Serializable { + + private final int id; + private final String name; + private final int age; + private final String gender; + private final String[] hobbies; + + /** + * Creates a new Person. + * @param id The id of the person. + * @param name The name of the person. + * @param age The age of the person. + * @param gender The gender of the person. + * @param hobbies The hobbies of the person. + */ + public Person(int id, String name, int age, String gender, String[] hobbies) { + this.id = id; + this.name = name; + this.age = age; + this.gender = gender; + this.hobbies = hobbies; + } + + /** + * Returns the Id of the person. + * @return The id of the person. + */ + public int getId() { + return id; + } + + /** + * Returns the name of the person. + * @return The name of the person. + */ + public String getName() { + return name; + } + + /** + * Returns the gender of the person. + * @return The gender of the person. + */ + public String getGender() { + return gender; + } + + /** + * Returns the hobbies of the person. + * @return The hobbies of the person. + */ + public String[] getHobbies() { + return hobbies; + } + + /** + * Returns the age of the person. + * @return The age of the person. + */ + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", gender='" + gender + '\'' + + ", hobbies=" + Arrays.toString(hobbies) + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Person person)) { + return false; + } + return getId() == person.getId() && + getAge() == person.getAge() && + Objects.equals(getName(), person.getName()) && + Objects.equals(getGender(), person.getGender()) && + Arrays.equals(getHobbies(), person.getHobbies()); + } + + @Override + public int hashCode() { + int result = Objects.hash(getId(), getName(), getAge(), getGender()); + result = 31 * result + Arrays.hashCode(getHobbies()); + return result; + } + } +} diff --git a/microprofile/grpc/client/src/test/proto/echo.proto b/microprofile/grpc/client/src/test/proto/echo.proto new file mode 100644 index 00000000000..68911ff5c12 --- /dev/null +++ b/microprofile/grpc/client/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +syntax = "proto3"; +option java_package = "io.helidon.microprofile.grpc.client.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/microprofile/grpc/client/src/test/proto/strings.proto b/microprofile/grpc/client/src/test/proto/strings.proto new file mode 100644 index 00000000000..8e01019e310 --- /dev/null +++ b/microprofile/grpc/client/src/test/proto/strings.proto @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. + * + * Licensed 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. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.microprofile.grpc.client.test"; + +service StringService { + rpc Upper (StringMessage) returns (StringMessage) {} + rpc Lower (StringMessage) returns (StringMessage) {} + rpc Split (StringMessage) returns (stream StringMessage) {} + rpc Join (stream StringMessage) returns (StringMessage) {} + rpc Echo (stream StringMessage) returns (stream StringMessage) {} +} + +message StringMessage { + string text = 1; +} diff --git a/microprofile/grpc/client/src/test/resources/application.yaml b/microprofile/grpc/client/src/test/resources/application.yaml new file mode 100644 index 00000000000..525f7570c65 --- /dev/null +++ b/microprofile/grpc/client/src/test/resources/application.yaml @@ -0,0 +1,48 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed 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. +# + +server: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "server.p12" + +grpc: + client: + channels: + - name: "echo-channel" + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "client.p12" \ No newline at end of file diff --git a/microprofile/grpc/client/src/test/resources/client.p12 b/microprofile/grpc/client/src/test/resources/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4eb3b8325cd0190163032aca86e1d4fc94856822 GIT binary patch literal 4181 zcmY+EWmFW5wuWIGKw^;Y8d`FO4r!R7kra{cRN$kVp>ybzA%_qK=@29&1VMz6MnqaE zk#3Inu6xf}=f_@qt>@kQ{rNx;WcmQy2M`1q5k4Vbta|J@2$ul21o7w^hgIE5dsUMBr)v#p-`Y6`%P3zDS600VN3FH3R`Xhdjn7`hWbloDoRH)IP6+WcmE9 z`35_>AJFE&UKlM8cz`qTw+lF;|4Y44{>C>)$b58Pwv%K0R(%IXj}~QHjD4NBVYZjc zF@t^Op?!4hS2W1WsUs*fH4rKzE`)r~ui`YVNmNGREmp`fmhZ-l6^5sfB`zPY40}#g zH)^o?@o9hBqDnY3fTdikVzhQYjJ@QFySfUF2zyQ%MJkSx&6yp$d%0u5zcM8tP1i^6 z%}M{O^-J(q-l}? zG;sYG<`hB67OAJ{Kn7v!iM%17ZGOW*_+6?mj_PPjWm!Ul8pMIbf!M^ zWgBVP1D^1uoR+3$()AEJwCJQVGFXYpP?{E!vJd6qcaPa-_wk2fi>CtS#t_Q=YzDhg z_T{E?`*O_~!8+lV;#JIN%WYb`Smp6z>%jU|E0Q|UwSakO!YAE{b)<{PF*8og zVze-4xbFhS3hi4{gv+TZ5{{zgoi6OkC#C!n|C=?7gif&4$VS7{f>r$*glAp@OguSO zga@4c?5V%spuxJq+7~S4uO0~Qtx@pLpVA#omKA$K=0qs_or+yj(CH>JtF!DYfsUdj#Y;j*a+4940>!Es- zM}7u;^}?V7TD&gvN+Kp6EAKaV;pn^2wV@?@vl^3I+gH5IuR|iG=L7^PT$x(L9Pe@2 z?=%-f?!NUux8M^q@js$G+ipgHOQVf`dy{^6RCxNjjhLfHs9_L9h593q(M55&T#~hF z@ZHR#^3^uOTItFm*F)8$)0x`}Zp|%D6NEgD^>WL7HNjNpku0LT6DS&tU+qTrHbUu9 z&)^rxWz@YiTBES@?NpTL!5omgXlbo6pb6g`>>=vt1vu!E<3vBcdqHR_Ba4|Eb_xGg z)pJ##He7jY95Ya1>hZEv-!3&R>CaoXf5J1TZ)6O9K~lf--8>#_9ORnq@zOo*O|s_- zH>^L{5fkgqYi9TBfU#YG%Akqk_WHE*j6&Si&2D2c`4YK3)g}>K$KvCQ7`x9(WDsEy zuQH5qIeN|NDn3hDL$9r!IAXNK`>ba0F{990Ft$^8<0CG8rrA~9aBG;US|w!VjlFOnx;asOlp6HSq&uB> zPp2hwGj;nLOA8~DC4JrPmu=91AuurQp^IZMg8;( zNk?BgR!9GmWnhg;q@`&luP<%zDo;OcX`dMoFJwV0VQyK&8YX2%_YF9}(b}Ac#W$qCfyH z&fn$zp9J^6yo&!nUaeG&iy}_w@w&JB$t=SwN~qqp*!;g?+nl!qbt-3Srz zdUU=~v7Sgb0VsS}2&|z~|3MrAAtr}=3)n1hxrwEjX74xY&WumZJgpiDImLH>0HGEj zE@Pz6+gd485YSPSd-~MF_ofwqC9vZ}8J_A_xnnt@2jF{VD_){puWF0@1B$Q~N$0Vs z-?9BDZRR}ZLhMq|y5-J;bH5!%c&I1&i~bmo_W>~SP^HW#(~yyFh0IX*un{<(Z*H5* zG3|bt;nH9dulFr4zGP97g%w$luP$lQCw&7zUrnj|ct{dHQaAqrM}sXWo* zIwbhHK4sYTi}c}+H=ECU_CW)XPNsksw*O(5%WXUzxJ;CW4wY-#fhx7#RxUv{vGW)O zD08=SPkJ@3D3mcw@tOyy;4L~ar`xu7adVYdzfGQS4)LplDy z?4mahFGu`fqOEB?-}?;U;~_d$fZX81i-)m3$WLiGrYR{Vb(+BxFB{bgmoIBI=R^T$ z2jb^~oKDwEmh%hg@%Nu7LXSBz15KET`V3br4R4cQx~+zqc5kP!I37*ccaqajWobMK zZi-eDvt^V0Wx5yXv-L$NFi$%&6yL^6h{)OAOJ(=Ff06E!^>9J;{ZS0mp#M!8;!a)0 ziI!n_qQZSi)bXC}Av&X{1=>a_R`JQ(6a=y?=Xs3hk0yi^%Qtn{d`)D4<(!F3-FsJ5 zfY|6y$E7qMM(%l|mdCYd7K?3kA#e(=E7g{U4qi2#Z$_ndvFhE0Oh3#!n`A1vNw<3W z2uInJ(>ciz+w6ik()9M}hRO6sepzQ5EC$ppZo^+t8|M>ZX4rADe+_-TVMhM%sw z9eW4be3ydc95|A1=ww9x$mFh{%P|`7Y<{Ga>VGJRJ%>eS&vgD{K%AZsPcYh zR}~?4Sn|7?H;%kfyI~CMyewJu8rWrh+pR0NYq-Iyq&?|pGN4B-TYzWrbyp>!A<)38 z4%1Lg&0jg5u$U@}6z(^3tDDy`9}Qvpeg^x)O5ZI8it`!f*`5Kvgjs4Q##u&}*=DiI zn!gK5ddr7~KTeHsx+V>5JIb!U`qpgU)F>16$TyDG#KhQ%jGy|+YU~O=2kCWLwf7N0 zX%Sbo>AZn%B0h?8<4)6dI~>r`P6LmtinE+;OU)O|Wex9p(ws2hCJo+9_pFiB z7fAm7$^ThSDziqaaV4p!zZcH?1IeFP&zt$b9G{haDnBg97{BIqH&x|~TRr{mNtgF1 z=KD*IKX7d`AHJ*>xJ?0B3;EYPe-a3nrJ&`HtY_0RBG!7dK>bgh3!%qzVTg$*y+MY) z&$Z8|T}^|k`2EED{c_d(304<g-Im@iW9sI{w z4KPkbki~?=Wtp>|bG;b~`LW$2ht1I_SHKB9W3!PrEptc|R1Yy; zL@g}P*5dF&^1AI!S0rw>;(g?>MWY)#lX^iM*AL>3H>kVuE63I@@!OM<{gtNgl29Y= z$^`X5{qhOWJ2HWP{AE~d%@UaqJ_L+dy2JO@AXYtb^96==Ut?GO`W&^~Le97!;K3a` z=?l=}772&XuzBVWt*y=8MfJjUx6iG4BNV{th8^B9N^x2wm{R^G=X52#O9B1MWoAC@ zp1xqHET!2Hy`ewqcZvU%1Frz-;E~+EAuqN@lnWGN)N|X2^itS8+MkaTv9oe9ldamr zpS2{JVzL@2Bvi}=Nii~+uY+@+OR18n)q#s5o=uczm)Acyy)PdGb)79|j7*YoAE07> zR`N%M{!UWAahsr!##wZg{a)X~h#T|vN!gy2`{vaiqcq`c)c|6%UCUNfJ1>%-T5l-b zp=s4{-N=IPAX0nM)8>w{-~|ginZhrAq)NOaoJC7sHpFg9jZ>KDYsR~;OtGzR=ai>k zg2qili0Iyw1YyE!f{P65K)rcr%JX_h-u#w=ZyG}lmZ{`U9e*Yg_e>8T-4(kX%B^`0 z9PO7HKCu0@&@-2{$|71S(wKBumamE%t#&9d$rL>_dPFn^H(cc8$D0dwetT0u~E zav=YHx$|t?RjAix^BY5*cG-M6dW?aGScPcvZ1WaFRz*emZ-6;-XsdYu;M#$K_??%U%)qP1NrdiQm%Bp9X&p zGZHi1mxeT_v*>(tPAI0(&%1qh5mXmP^7eAOwT`x=6+ZQ$d5AG{+wcW$S6i*&3H$Wv z`5{Ce0)`Ock(HgCm*wo&uHc@KVUNx#p)sG-brDlzk#;QGPN23U_sZnaC zR#j1}wMX*web4(o&w2j1=brQV-h2PM9~gp(p8`k;Lom)ksBa_nkjL~uC@>$vI0Qm4 z_Wi}(Fa+4~zap?H2m!wT7wi2kEeP%ZZPC#HDe@7Za~J}247&xP`G5TPI2VXTfN24L z92dsPc*Zj?PJcd1XKPtOK?&&odkfTG#4NWoXi#X?tUX=j%hxaKOs%{99O_IITApR-b2_+u8z$_({pQY9E^_?lJ%Lxw)um4Y z-}4W>TyaKDjqM}}SAe&dg!VeC))bT0_r9A1KjddPnUuKfB_uzsCr>@`W|jzm8wBi2 zg=(C()RGx{{q}{Q*NNO`NfbhXNhfvNgjG)=UuV6*rfXjdScE%>O$|(JLRo~IFRRW% zr}yaEYS^Tuo`*AtuhWGtJ#HnT)zEYf^}pIGyRIgjErR?Qfu^(1j~q`k#9&0P1J9@@ z+hmK$nBT1C7pq7p0U(d2`Hy$5*ci2TO??+zigDpIlV(MW*HSbDr(BCjwn^iy=3YG%{5?~1 z^mA6vDK5$6&1ZM_34*mMsFP65k~#!S6(tAfkBZePTezHVEr=amkhsr5i)<11Gae3Bk^T$|$j+p(xtwof(Xr?DzJhNeGCK{Hl z1;xDO3K9fydzRy!0vKMWfQzli*-XXh7!?(~db&$SCNz7Ysi~}Qj>6u>BRGBE>hk>o zA>Jeq>JuvwDWlVb9h`TLxyGaxC1jpc*-a$F+u$wXlPRVh_fcdip7d$I-q~^ z?fYu@fv%z5#p2d!aotq_y`$v^(6ArP zx-9SL&%-b}Ql1h~x=oQw%-#G`i&4h+`%Xd3!zCj-x1a|9^prRoHXtI#m=xW(OERBL zPv=BdZqLjFI*yH_`x?H@HNwpT#K%fevou_WFK>Pj?dlMLK!hm&>@$W4iaf^kdd3ln z?~(?y6qsAWGL3ZOUFMY7e|57l6z|<0bDrk2YA*2%RRS1jTRdRBQy6KvKJ{y9>L#>k zVe_R@KT!{ryp3h5xQ>lIdY#8u9#k!MVfN=?UW`dCQiT`e^eqL1Ly=0~E&(%-jkf`* zUXcv0b@fFxl|xAUNCvd*Wyio#b%{!vFXYU1>%`BTJbW=#*C`yJ%E6JaE`_==&c81! zAq%_mZ&gyWNPu`L01`0%{~%Tg7SL5oFF!Xv2^j?md3k9WSxH%WSr~%G?%zXDNIrtb z=r7Wz00RCtjsGOT|MDv2f4qtj_c{!@r!sE|pJP>kj#x?dQuh9@SECR#ROR4)eD!xU zmF_n!<@W?u(uurVa|N;uO#2=|H+w7BytQ>HQ|Mn<(PdtX=Jo9<$0|6?qa0t^Ef+Jv?(Axl<-+C456ved zR14F?S`FFst?(9SG0(4*s|4aeA0jKukSBwW*0|8l)UfI!+RNx&_J zz2Rb1E<0TAmx|C@^AwfqZOo=Ow|tLZlYCH(Ocy@07fV=?AX1A}sM@So2n-ejNh{za zNdeN7VkVA2ca!ndk`Nr)fR*pgHp!TEt=18Ra$N`q_!d6t(!Q;-S+@@qXrBcC2tP?I zcY78bkD5Y@aZ1tLOoOFg-2?z!&6as`)H5}FfQNWrff#?8`c6OXBkQtzsrFbxc-u{- z%U+3{K=xegE17<_a`svc_Br0Q(KMUyFw++~>qr~xdrcfON?KX5x-cb6)3L#_umt``m+@GX3x#xk76gtdGdFU@;+FUCW*8N4U|VRut3 zA#=yPdh=+=7pIitM}mgP?uxw7ewn7|6JDSl!$h?+v>F>AwbFSj@tS#X>b0uvS&i`> zRpqQ1^oAmfL)E4L73r+7qcU7ZAyF^eu{3YtpzQ}?P0hSy(nYM5bv|d zeoZ{s{UUAEwI7b9?@KIaaU|3O6Vua-BOI@k`u(oWI=vV^_gyq;eTP&q=f+3M<=1>) z<9+}S?FkPM$UL!}Cr=b%*f^6DX=n;l{kcm46ETz;kNr49ySXy)KFaNWo|Z8RT6iL|fI z7`o{Dxz^=Sa&4|ubn)RcGwn_YvMkf^L=<7&BhwDD-e~*xnXSP&1=wZUN_E@ci=C}iX+uXZheqa; zrwmARHL0=K%8NS=#f9TIi)~Lv51*%d^L=Jf>=GesJIa&_E2Q9eBk<_PP1Y~ySWT8I z{wR{5Cya~H{jSg;2CZ3YrxWV!V9n!;K+IVS@^A(OaqCk_=VgCOpTZ*{+$0M4c)d*R z17QO=_jXw$^$k@Dg>iilz)Edw)<*ksVmjSDA2V`XzciGfm>$i$GizT{zNDr)J=`I5 z#%>GrHxH%9-Bfy~U*f9^ojKA_W`lF`m zT2u5w7e3aU^rp||+QhMau98H14r5+6*Osvvh4DLWDgOQ5pK9);@O|+8H5oP5y)pVD zzRAPucT{}O9n#W6MlQLf+^J8b;Uboj#x8!3>AitD!nA{4Du_;@X2;Vb6*Rm z!)>z%a9Jt8`d=pZ!qw>vk=f>*R$nWtiQhBE3q1C6pTsk7q~SLcDXMz#S zCu|Q=1c6!Lf+=@2BLh2p`S_U6-8{}i)<-G}Le|LP7EJN(M1qF&j~M2EtZf(<`pl^T z0_lAsz4NN&03~9IKSaS;4CboG(4!m!uBpn)OL^TI!xOe#=E@!XKtS)j#Z#_chPypbCeM1p|8}V{N)?udQ~g4u2_y-OtCbBlEm@ z=B;BQ+MZi03LuiTU^n4fx8UVJgNEJq#)AdR;# zD=Yn+s18l|A;ir*P@;xz)FW-Gdl^sZ22MO-{>mhE_L-O>4+rNC-f$eHCyQd-c_2E8 z56)%*A|^cJWLli}5?N^X8T`8uw&}pDC9eDaI<%jCPM4S>{iUUk0ko-xJ2Z)Pmu_;p zDDVG>csN>oSMEWhNN58}CFopuPuu+ZK)g*N6rTN%m_mgdGU=Y5?7$hZ6S4&Vh^6ij zKZ;Te%gjZdvA2~6Oesu`GqY^i2fQY~0y6b569&#KZcq);iCGs_V5I{m011&}CL>Zt zN54kE>8k|OvjU3oxNBqD(6ivJ%H|W6OsvIP*MtcheTyTG3RDmtjPN@k9+R<92h1Rz zNp7nYRyVxyT`f_o!13 zROazdppozUl0TfnRtGJb8jqP!CA%c{d7Lx%wJt1$Wt6(`*O6Uw z_nExHrAOA#ou8}BLOi9h953j3I}f>3qvRgmMsij4`!FIr2(?hnL#MT#aD}l()@Wn! zIwrc8$%!QS>Q$&Dz)wKXW#RH&;ku6d;0EE$O!32LHt(}9(#subeacy90=p%`om%M# zPP3|&r?iqDXM@vUA8^OpE_`UQ99#&UG_X1cG@ksC!23|we$FP^QMU0&O-M_XjY|3N z$0!;s)c!te%hfR7Aft2m5GJ$5Bj~y>j6iarDu!pGb7O`+;=N&JtE1I7ot?2ah@z=h zW2K$ZPl4%!zuON?jp_ux)7X81p9@7<=7o)muQ!`VU*(U_GMZtu2O?fDpn4;&s;Q(e zRGDC(^{49^Z4ZPC^eY6YNJjucoEOc70koBr&K)(r+s?@g^Se%SH`l9uWoVU9k8~!7 zSm(Da`qf`K>*l$GR3fX#>*?3KSLGipLS>5eZQT02fellR^9>>##$oNHE}<85p%~Q7 z%_(}1jZoEc^nl$(=_+s4AiZQK^f5uJuh zyx{G`iKeIyY%fRaL#e4KJl~qBe?H-g4xGk|wmuFpt&b6U)UaK7xbX7Trn9x!O|I0d zM0C&$Crky#52J>FMHwh5IKcoA3);!)%GXyMs9+@{1qF^WmEsdjJ%}afv*WKBoMu}7 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn literal 0 HcmV?d00001 diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml index 286d044c437..89cb26d3b3f 100644 --- a/microprofile/grpc/pom.xml +++ b/microprofile/grpc/pom.xml @@ -34,6 +34,7 @@ core server + client tracing tests diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java index 3efb29e47f7..f4d5e006cb7 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java @@ -26,7 +26,7 @@ /** * Helidon's implementation of a gRPC {@link Channel}. */ -class GrpcChannel extends Channel { +public class GrpcChannel extends Channel { private final GrpcClientImpl grpcClient; @@ -39,6 +39,15 @@ class GrpcChannel extends Channel { this.grpcClient = (GrpcClientImpl) grpcClient; } + /** + * Underlying gRPC Client for this channel. + * + * @return the gRPC client + */ + public GrpcClient grpcClient() { + return grpcClient; + } + @Override public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java index d786f7840f0..b4ffdc582cf 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java @@ -114,4 +114,11 @@ static GrpcClient create() { default Channel channel(Collection interceptors) { return channel(interceptors.toArray(new ClientInterceptor[]{})); } + + /** + * Configuration for this gRPC client. + * + * @return the configuration + */ + GrpcClientConfig clientConfig(); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java index 7a8ce6dfa91..077b8e9a83f 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java @@ -61,4 +61,9 @@ public Channel channel() { public Channel channel(ClientInterceptor... interceptors) { return ClientInterceptors.intercept(channel(), interceptors); } + + @Override + public GrpcClientConfig clientConfig() { + return clientConfig; + } }