From 46f35e72cf3c58d7fb79484d60f9fbee97016821 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 30 May 2024 11:53:49 -0400 Subject: [PATCH 01/30] Ports helidon-microprofile-core module from 3.x and moves additional classes to helidon-grpc module. Signed-off-by: Santiago Pericas-Geertsen --- grpc/core/pom.xml | 4 + .../java/io/helidon/grpc/core/GrpcHelper.java | 157 ++++ .../helidon/grpc/core/MarshallerSupplier.java | 10 + .../io/helidon/grpc/core/ResponseHelper.java | 455 +++++++++ .../helidon/grpc/core/SafeStreamObserver.java | 169 ++++ grpc/core/src/main/java/module-info.java | 4 + microprofile/grpc/core/pom.xml | 70 ++ .../core/AbstractMethodHandlerSupplier.java | 294 ++++++ .../grpc/core/AbstractServiceBuilder.java | 248 +++++ .../grpc/core/AnnotatedMethod.java | 488 ++++++++++ .../grpc/core/AnnotatedMethodList.java | 261 ++++++ .../microprofile/grpc/core/Bidirectional.java | 46 + .../BidirectionalMethodHandlerSupplier.java | 175 ++++ .../grpc/core/ClientStreaming.java | 46 + .../ClientStreamingMethodHandlerSupplier.java | 399 ++++++++ .../helidon/microprofile/grpc/core/Grpc.java | 75 ++ .../grpc/core/GrpcCdiExtension.java | 52 ++ .../grpc/core/GrpcInterceptor.java | 76 ++ .../grpc/core/GrpcInterceptorBinding.java | 42 + .../grpc/core/GrpcInterceptors.java | 51 + .../grpc/core/GrpcMarshaller.java | 41 + .../microprofile/grpc/core/GrpcMethod.java | 52 ++ .../grpc/core/InProcessGrpcChannel.java | 57 ++ .../microprofile/grpc/core/Instance.java | 158 ++++ .../grpc/core/MethodHandlerSupplier.java | 53 ++ .../microprofile/grpc/core/ModelHelper.java | 322 +++++++ .../microprofile/grpc/core/RequestType.java | 40 + .../microprofile/grpc/core/ResponseType.java | 40 + .../grpc/core/ServerStreaming.java | 46 + .../ServerStreamingMethodHandlerSupplier.java | 385 ++++++++ .../helidon/microprofile/grpc/core/Unary.java | 46 + .../grpc/core/UnaryMethodHandlerSupplier.java | 873 ++++++++++++++++++ .../microprofile/grpc/core/package-info.java | 20 + .../grpc/core/src/main/java/module-info.java | 44 + .../AbstractMethodHandlerSupplierTest.java | 225 +++++ .../grpc/core/AnnotatedMethodListTest.java | 178 ++++ .../grpc/core/AnnotatedMethodTest.java | 286 ++++++ ...idirectionalMethodHandlerSupplierTest.java | 223 +++++ ...entStreamingMethodHandlerSupplierTest.java | 462 +++++++++ .../microprofile/grpc/core/InstanceTest.java | 87 ++ .../grpc/core/MethodHandlerTest.java | 95 ++ .../grpc/core/ModelHelperTest.java | 119 +++ ...verStreamingMethodHandlerSupplierTest.java | 434 +++++++++ .../core/UnaryMethodHandlerSupplierTest.java | 829 +++++++++++++++++ microprofile/grpc/pom.xml | 72 ++ microprofile/pom.xml | 1 + 46 files changed, 8310 insertions(+) create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java create mode 100644 microprofile/grpc/core/pom.xml create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethodList.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Bidirectional.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcCdiExtension.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptorBinding.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMethod.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java create mode 100644 microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/package-info.java create mode 100644 microprofile/grpc/core/src/main/java/module-info.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplierTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodListTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplierTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplierTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/InstanceTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/MethodHandlerTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ModelHelperTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplierTest.java create mode 100644 microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplierTest.java create mode 100644 microprofile/grpc/pom.xml diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index ab16615f330..652b107f9f3 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -33,6 +33,10 @@ io.helidon.common helidon-common + + io.helidon.http + helidon-http + io.grpc grpc-api diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java new file mode 100644 index 00000000000..dbba7cc24cf --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java @@ -0,0 +1,157 @@ +/* + * 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.grpc.core; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; + +/** + * Helper methods for common gRPC tasks. + */ +public final class GrpcHelper { + + /** + * Private constructor for utility class. + */ + private GrpcHelper() { + } + + /** + * Extract the gRPC service name from a method full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the service name extracted from the full name + */ + public static String extractServiceName(String fullMethodName) { + int index = fullMethodName.indexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(0, index); + } + + /** + * Extract the name prefix from a method full name. + *

+ * The prefix is everything upto the but not including the last + * '/' character in the full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the name prefix extracted from the full name + */ + public static String extractNamePrefix(String fullMethodName) { + int index = fullMethodName.lastIndexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(0, index); + } + + /** + * Extract the gRPC method name from a method full name. + * + * @param fullMethodName the gRPC method full name + * + * @return the method name extracted from the full name + */ + public static String extractMethodName(String fullMethodName) { + int index = fullMethodName.lastIndexOf('/'); + return index == -1 ? fullMethodName : fullMethodName.substring(index + 1); + } + + /** + * Convert a gRPC {@link StatusException} to a {@link io.helidon.http.Status}. + * + * @param ex the gRPC {@link StatusException} to convert + * + * @return the gRPC {@link StatusException} converted to a {@link io.helidon.http.Status} + */ + public static io.helidon.http.Status toHttpResponseStatus(StatusException ex) { + return toHttpResponseStatus(ex.getStatus()); + } + + /** + * Convert a gRPC {@link StatusRuntimeException} to a {@link io.helidon.http.Status}. + * + * @param ex the gRPC {@link StatusRuntimeException} to convert + * + * @return the gRPC {@link StatusRuntimeException} converted to a {@link io.helidon.http.Status} + */ + public static io.helidon.http.Status toHttpResponseStatus(StatusRuntimeException ex) { + return toHttpResponseStatus(ex.getStatus()); + } + + /** + * Convert a gRPC {@link Status} to a {@link io.helidon.http.Status}. + * + * @param status the gRPC {@link Status} to convert + * + * @return the gRPC {@link Status} converted to a {@link io.helidon.http.Status} + */ + public static io.helidon.http.Status toHttpResponseStatus(Status status) { + io.helidon.http.Status httpStatus = switch (status.getCode()) { + case OK -> io.helidon.http.Status.create(200, status.getDescription()); + case INVALID_ARGUMENT -> io.helidon.http.Status.create(400, status.getDescription()); + case DEADLINE_EXCEEDED -> io.helidon.http.Status.create(408, status.getDescription()); + case NOT_FOUND -> io.helidon.http.Status.create(404, status.getDescription()); + case ALREADY_EXISTS -> io.helidon.http.Status.create(412, status.getDescription()); + case PERMISSION_DENIED -> io.helidon.http.Status.create(403, status.getDescription()); + case FAILED_PRECONDITION -> io.helidon.http.Status.create(412, status.getDescription()); + case OUT_OF_RANGE -> io.helidon.http.Status.create(400, status.getDescription()); + case UNIMPLEMENTED -> io.helidon.http.Status.create(501, status.getDescription()); + case UNAVAILABLE -> io.helidon.http.Status.create(503, status.getDescription()); + case UNAUTHENTICATED -> io.helidon.http.Status.create(401, status.getDescription()); + default -> io.helidon.http.Status.create(500, status.getDescription()); + }; + return httpStatus; + } + + /** + * Ensure that a {@link Throwable} is either a {@link StatusRuntimeException} or + * a {@link StatusException}. + * + * @param thrown the {@link Throwable} to test + * @param status the {@link Status} to use if the {@link Throwable} has to be converted + * @return the {@link Throwable} if it is a {@link StatusRuntimeException} or a + * {@link StatusException}, or a new {@link StatusException} created from the + * specified {@link Status} with the {@link Throwable} as the cause. + */ + public static Throwable ensureStatusException(Throwable thrown, Status status) { + if (thrown instanceof StatusRuntimeException || thrown instanceof StatusException) { + return thrown; + } else { + return status.withCause(thrown).asException(); + } + } + + /** + * Ensure that a {@link Throwable} is a {@link StatusRuntimeException}. + * + * @param thrown the {@link Throwable} to test + * @param status the {@link Status} to use if the {@link Throwable} has to be converted + * @return the {@link Throwable} if it is a {@link StatusRuntimeException} or a new + * {@link StatusRuntimeException} created from the specified {@link Status} + * with the {@link Throwable} as the cause. + */ + public static StatusRuntimeException ensureStatusRuntimeException(Throwable thrown, Status status) { + if (thrown instanceof StatusRuntimeException) { + return (StatusRuntimeException) thrown; + } else if (thrown instanceof StatusException) { + StatusException ex = (StatusException) thrown; + return new StatusRuntimeException(ex.getStatus(), ex.getTrailers()); + } else { + return status.withCause(thrown).asRuntimeException(); + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java index 7a2d3ba2bcb..ab2e8c5453c 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -25,6 +25,16 @@ @FunctionalInterface public interface MarshallerSupplier { + /** + * The name of the Protocol Buffer marshaller supplier. + */ + String PROTO = "proto"; + + /** + * The name to use to specify the default marshaller supplier. + */ + String DEFAULT = "default"; + /** * Obtain a {@link MethodDescriptor.Marshaller} for a type. * diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java new file mode 100644 index 00000000000..e95afb2a83a --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java @@ -0,0 +1,455 @@ +/* + * 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.grpc.core; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.grpc.stub.StreamObserver; + +/** + * A number of helper methods to handle sending responses to a {@link StreamObserver}. + */ +public final class ResponseHelper { + private ResponseHelper() { + } + + /** + * Complete a gRPC request. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * specified value then calling {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to complete + * @param value the value to use when calling {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, T value) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + safe.onNext(value); + safe.onCompleted(); + } + + /** + * Complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, CompletionStage future) { + future.whenComplete(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, CompletionStage future) { + future.whenCompleteAsync(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, CompletionStage future, Executor executor) { + future.whenCompleteAsync(completeWithResult(observer), executor); + } + + /** + * Complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, Callable callable) { + try { + observer.onNext(callable.call()); + observer.onCompleted(); + } catch (Throwable t) { + observer.onError(t); + } + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Callable callable) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable))); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Callable callable, Executor executor) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable), executor)); + } + + /** + * Execute a {@link Runnable} task and on completion of the task complete the gRPC request by + * calling {@link StreamObserver#onNext(Object)} using the specified result and then call + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, Runnable task, T result) { + complete(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The task and and request completion will be executed on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Runnable task, T result) { + completeAsync(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Runnable task, T result, Executor executor) { + completeAsync(observer, Executors.callable(task, result), executor); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void stream(StreamObserver observer, Stream stream) { + stream(observer, () -> stream); + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until + * the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void streamAsync(StreamObserver observer, Stream stream, Executor executor) { + executor.execute(() -> stream(observer, () -> stream)); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void stream(StreamObserver observer, Supplier> supplier) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + Throwable thrown = null; + + try { + supplier.get().forEach(safe::onNext); + } catch (Throwable t) { + thrown = t; + } + + if (thrown == null) { + safe.onCompleted(); + } else { + safe.onError(thrown); + } + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method + * until the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void streamAsync(StreamObserver observer, Supplier> supplier, Executor executor) { + executor.execute(() -> stream(observer, supplier)); + } + + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + // todo: a bit of a chicken or egg when used with Coherence streaming methods, isn't it? + public static Consumer stream(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenComplete(completeWithoutResult(safe)); + return safe::onNext; + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the + * fork-join thread pool. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static Consumer streamAsync(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe)); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value)); + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the executor + * thread. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static Consumer streamAsync(StreamObserver observer, CompletionStage stage, Executor executor) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe), executor); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value), executor); + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method. + * @param observer the {@link StreamObserver} to complete + * @param the type of the result + * @param the type of the response + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static BiConsumer completeWithResult(StreamObserver observer) { + return new CompletionAction<>(observer, true); + } + + /** + * Obtain a {@link Consumer} that can be used to complete a {@link StreamObserver}. + * @param observer the {@link StreamObserver} to complete + * @param the type of the response + * @return a {@link Consumer} that can be used to complete a {@link StreamObserver} + */ + public static BiConsumer completeWithoutResult(StreamObserver observer) { + return new CompletionAction<>(observer, false); + } + + /** + * Convert a {@link Callable} to a {@link Supplier}. + * @param callable the {@link Callable} to convert + * @param the result returned by the {@link Callable} + * @return a {@link Supplier} that wraps the {@link Callable} + */ + public static Supplier createSupplier(Callable callable) { + return new CallableSupplier<>(callable); + } + + /** + * A {@link BiConsumer} that is used to handle completion of a + * {@link CompletionStage} by forwarding + * the result to a {@link StreamObserver}. + * + * @param the type of the {@link CompletionStage}'s result + * @param the type of result expected by the {@link StreamObserver} + */ + private static class CompletionAction implements BiConsumer { + private StreamObserver observer; + private boolean sendResult; + + CompletionAction(StreamObserver observer, boolean sendResult) { + this.observer = observer; + this.sendResult = sendResult; + } + + @Override + @SuppressWarnings("unchecked") + public void accept(T result, Throwable error) { + if (error != null) { + observer.onError(error); + } else { + if (sendResult) { + observer.onNext((U) result); + } + observer.onCompleted(); + } + } + } + + /** + * A class that converts a {@link Callable} to a {@link Supplier}. + * @param the type of result returned from the callable + */ + private static class CallableSupplier implements Supplier { + private Callable callable; + + CallableSupplier(Callable callable) { + this.callable = callable; + } + + @Override + public T get() { + try { + return callable.call(); + } catch (Exception e) { + throw new CompletionException(e.getMessage(), e); + } + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java new file mode 100644 index 00000000000..31cfb1db7ef --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java @@ -0,0 +1,169 @@ +/* + * 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.grpc.core; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A {@link StreamObserver} that handles exceptions correctly. + * + * @param the type of response expected + */ +public class SafeStreamObserver + implements StreamObserver { + + /** + * Create a {@link SafeStreamObserver} that wraps + * another {@link StreamObserver}. + * + * @param streamObserver the {@link StreamObserver} to wrap + */ + private SafeStreamObserver(StreamObserver streamObserver) { + delegate = streamObserver; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (t == null) { + onError(Status.INVALID_ARGUMENT + .withDescription("onNext called with null. Null values are generally not allowed.") + .asRuntimeException()); + } else { + try { + delegate.onNext(t); + } catch (Throwable thrown) { + throwIfFatal(thrown); + onError(thrown); + } + } + } + + @Override + public void onError(Throwable thrown) { + try { + if (done) { + LOGGER.log(Level.SEVERE, checkNotNull(thrown), () -> "OnError called after StreamObserver was closed"); + } else { + done = true; + delegate.onError(checkNotNull(thrown)); + } + } catch (Throwable t) { + throwIfFatal(t); + LOGGER.log(Level.SEVERE, t, () -> "Caught exception handling onError"); + } + } + + @Override + public void onCompleted() { + if (done) { + LOGGER.log(Level.WARNING, "onComplete called after StreamObserver was closed"); + } else { + try { + done = true; + delegate.onCompleted(); + } catch (Throwable thrown) { + throwIfFatal(thrown); + LOGGER.log(Level.SEVERE, thrown, () -> "Caught exception handling onComplete"); + } + } + } + + /** + * Obtain the wrapped {@link StreamObserver}. + * @return the wrapped {@link StreamObserver} + */ + public StreamObserver delegate() { + return delegate; + } + + private Throwable checkNotNull(Throwable thrown) { + if (thrown == null) { + thrown = Status.INVALID_ARGUMENT + .withDescription("onError called with null Throwable. Null exceptions are generally not allowed.") + .asRuntimeException(); + } + + return thrown; + } + + /** + * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These varieties are + * as follows: + *

    + *
  • {@code VirtualMachineError}
  • + *
  • {@code ThreadDeath}
  • + *
  • {@code LinkageError}
  • + *
+ * + * @param thrown the {@code Throwable} to test and perhaps throw + */ + private static void throwIfFatal(Throwable thrown) { + if (thrown instanceof VirtualMachineError) { + throw (VirtualMachineError) thrown; + } else if (thrown instanceof ThreadDeath) { + throw (ThreadDeath) thrown; + } else if (thrown instanceof LinkageError) { + throw (LinkageError) thrown; + } + } + + /** + * Ensure that the specified {@link StreamObserver} is a safe observer. + *

+ * If the specified observer is not an instance of {@link SafeStreamObserver} then wrap + * it in a {@link SafeStreamObserver}. + * + * @param observer the {@link StreamObserver} to test + * @param the response type expected by the observer + * + * @return a safe {@link StreamObserver} + */ + public static StreamObserver ensureSafeObserver(StreamObserver observer) { + if (observer instanceof SafeStreamObserver) { + return observer; + } + + return new SafeStreamObserver<>(observer); + } + + // ----- constants ------------------------------------------------------ + + /** + * The {2link Logger} to use. + */ + private static final Logger LOGGER = Logger.getLogger(SafeStreamObserver.class.getName()); + + // ----- data members --------------------------------------------------- + + /** + * The actual StreamObserver. + */ + private StreamObserver delegate; + + /** + * Indicates a terminal state. + */ + private boolean done; +} diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java index 695cf325613..2bb58f7a62e 100644 --- a/grpc/core/src/main/java/module-info.java +++ b/grpc/core/src/main/java/module-info.java @@ -19,7 +19,11 @@ */ module io.helidon.grpc.core { + requires java.logging; + requires io.helidon.common; + requires io.helidon.http; + requires transitive io.grpc; requires transitive io.grpc.stub; requires transitive com.google.protobuf; diff --git a/microprofile/grpc/core/pom.xml b/microprofile/grpc/core/pom.xml new file mode 100644 index 00000000000..d8ee2f6be43 --- /dev/null +++ b/microprofile/grpc/core/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + io.helidon.microprofile.grpc + helidon-microprofile-grpc + 4.0.0-SNAPSHOT + + + helidon-microprofile-grpc-core + Helidon Microprofile gRPC Core + Core microprofile gRPC + + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.webclient + helidon-webclient-grpc + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + io.helidon.common + helidon-common + + + io.helidon.microprofile.config + helidon-microprofile-config + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.mockito + mockito-core + test + + + diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java new file mode 100644 index 00000000000..8d43437b9c0 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java @@ -0,0 +1,294 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.SafeStreamObserver; + +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A base class for {@link MethodHandlerSupplier} implementations. + */ +abstract class AbstractMethodHandlerSupplier + implements MethodHandlerSupplier { + + private final MethodDescriptor.MethodType methodType; + + /** + * Create an {@link AbstractMethodHandlerSupplier}. + * + * @param methodType the {@link MethodDescriptor.MethodType} to handle + * @throws java.lang.NullPointerException if the method type parameter is {@code null} + */ + AbstractMethodHandlerSupplier(MethodDescriptor.MethodType methodType) { + this.methodType = Objects.requireNonNull(methodType, "The method type parameter cannot be null"); + } + + @Override + public boolean supplies(AnnotatedMethod method) { + return isRequiredMethodType(method); + } + + /** + * Determine whether the specified method is annotated with {@link GrpcMethod} + * or another annotation that is itself annotated with {@link GrpcMethod} + * with a type matching this handler's {@link #methodType}. + * + * @param method the method to test + * @return {@code true} if the method is annotated with the correct type + */ + boolean isRequiredMethodType(AnnotatedMethod method) { + if (method == null) { + return false; + } + + GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(GrpcMethod.class); + return annotation != null && methodType.equals(annotation.type()); + } + + /** + * A base class for method handlers. + * + * @param the request type + * @param the response type + */ + public abstract static class AbstractHandler + implements MethodHandler { + + private final String methodName; + private final AnnotatedMethod method; + private final Supplier instance; + private final MethodDescriptor.MethodType methodType; + private Class requestType = Empty.class; + private Class responseType = Empty.class; + + /** + * Create a handler. + * + * @param methodName the name of the gRPC method + * @param method the underlying handler method this handler should call + * @param instance the supplier to use to obtain the object to call the method on + * @param methodType the type of method handled by this handler + */ + protected AbstractHandler(String methodName, + AnnotatedMethod method, + Supplier instance, + MethodDescriptor.MethodType methodType) { + this.methodName = methodName; + this.method = method; + this.instance = instance; + this.methodType = methodType; + } + + @Override + public final MethodDescriptor.MethodType type() { + return methodType; + } + + @Override + public void invoke(ReqT request, StreamObserver observer) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + + if (Empty.class.equals(requestType)) { + safe = new NullHandlingResponseObserver<>(observer); + } + + try { + invoke(method.declaredMethod(), instance.get(), request, safe); + } catch (Throwable thrown) { + safe.onError(GrpcHelper.ensureStatusException(thrown, Status.INTERNAL)); + } + } + + /** + * Invoke the actual unary or server streaming gRPC method handler. + * + * @param method the {@link Method} to invoke + * @param instance the service instance to invoke the method on + * @param request the method request + * @param observer the method response observer + * @throws InvocationTargetException if an error occurs invoking the method + * @throws IllegalAccessException if the method cannot be accessed + */ + protected abstract void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException; + + @Override + public StreamObserver invoke(StreamObserver observer) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + try { + return invoke(method.declaredMethod(), instance.get(), safe); + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + + /** + * Invoke the actual client streaming or bi-directional gRPC method handler. + * + * @param method the {@link Method} to invoke + * @param instance the service instance to invoke the method on + * @param observer the method response observer + * @return the {@link StreamObserver} to receive requests from the client + * @throws InvocationTargetException if an error occurs invoking the method + * @throws IllegalAccessException if the method cannot be accessed + */ + protected abstract StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException; + + @Override + public Class getRequestType() { + RequestType annotation = method.getAnnotation(RequestType.class); + if (annotation != null) { + return annotation.value(); + } + return requestType; + } + + /** + * Set the request type to use if no {@link RequestType} annotation + * is present on the annotated method. + * + * @param requestType the request type + */ + protected void setRequestType(Class requestType) { + this.requestType = requestType; + } + + @Override + public Class getResponseType() { + ResponseType annotation = method.getAnnotation(ResponseType.class); + if (annotation != null) { + return annotation.value(); + } + return responseType; + } + + @Override + public String javaMethodName() { + return method.declaredMethod().getName(); + } + + /** + * Set the response type to use if no {@link ResponseType} annotation + * is present on the annotated method. + * @param responseType the response type + */ + protected void setResponseType(Class responseType) { + this.responseType = responseType; + } + + /** + * Obtain the gRPC method name. + * + * @return the gRPC method name + */ + protected String methodName() { + return methodName; + } + + /** + * Complete a {@link io.grpc.stub.StreamObserver}. + * + * @param response the response value + * @param thrown an error that may have occurred + * @param observer the {@link io.grpc.stub.StreamObserver} to complete + * @return always returns {@link Void} (i.e. {@code null}) + */ + protected Void handleFuture(RespT response, Throwable thrown, StreamObserver observer) { + if (thrown == null) { + if (response != null) { + observer.onNext(response); + } + observer.onCompleted(); + } else { + observer.onError(GrpcHelper.ensureStatusException(thrown, Status.INTERNAL)); + } + return null; + } + + /** + * Obtain the generic type of a {@link java.lang.reflect.Type} + *

+ * Typically used to obtain the generic type of a + * {@link io.grpc.stub.StreamObserver} but could + * be used to obtain the generic type of other + * classes. + *

+ * If the type passed in is a {@link Class} then it has no generic + * component so the Object Class will be returned. Typically this + * would be due to a declaration such as + *

StreamObserver observer
instead of a generic declaration + * such as
StreamObserver<String> observer
. + * + * @param type the type to obtain the generic type from + * @return the generic type of a {@link java.lang.reflect.Type} + */ + protected Class getGenericResponseType(Type type) { + if (type instanceof Class) { + return Object.class; + } else { + return ModelHelper.getGenericType(type); + } + } + } + + /** + * A response that handles null values. + * + * @param the type of the response + */ + private static class NullHandlingResponseObserver + implements StreamObserver { + + private final StreamObserver delegate; + + private NullHandlingResponseObserver(StreamObserver delegate) { + this.delegate = delegate; + } + + @Override + @SuppressWarnings("unchecked") + public void onNext(V value) { + if (value == null) { + delegate.onNext(Empty.getDefaultInstance()); + } + delegate.onNext(value); + } + + @Override + public void onError(Throwable throwable) { + delegate.onError(throwable); + } + + @Override + public void onCompleted() { + delegate.onCompleted(); + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java new file mode 100644 index 00000000000..a5bdab576d3 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java @@ -0,0 +1,248 @@ +/* + * 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.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.grpc.core.MarshallerSupplier; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +/** + * A base class for gRPC service and client descriptor builders. + */ +public abstract class AbstractServiceBuilder { + + private static final Logger LOGGER = Logger.getLogger(AbstractServiceBuilder.class.getName()); + + private final Class serviceClass; + private final Class annotatedServiceClass; + private final Supplier instance; + private final List handlerSuppliers; + + /** + * Create a new introspection modeller 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 + */ + protected AbstractServiceBuilder(Class serviceClass, Supplier instance) { + this.serviceClass = Objects.requireNonNull(serviceClass); + this.annotatedServiceClass = ModelHelper.getAnnotatedResourceClass(serviceClass, Grpc.class); + this.instance = Objects.requireNonNull(instance); + this.handlerSuppliers = loadHandlerSuppliers(); + } + + /** + * Determine whether this modeller contains an annotated service. + * + * @return {@code true} if this modeller contains an annotated service + */ + public boolean isAnnotatedService() { + return annotatedServiceClass.isAnnotationPresent(Grpc.class); + } + + /** + * Obtain the service class. + * @return the service class + */ + protected Class serviceClass() { + return serviceClass; + } + + /** + * Obtain the actual annotated class. + * @return the actual annotated class + */ + protected Class annotatedServiceClass() { + return annotatedServiceClass; + } + + /** + * Obtain the {@link MarshallerSupplier} to use. + *

+ * The {@link MarshallerSupplier} will be determined by the {@link GrpcMarshaller} + * annotation if it is present otherwise the default supplier will be returned. + * + * @return the {@link MarshallerSupplier} to use + */ + protected MarshallerSupplier getMarshallerSupplier() { + GrpcMarshaller annotation = annotatedServiceClass.getAnnotation(GrpcMarshaller.class); + return annotation == null ? MarshallerSupplier.create() : ModelHelper.getMarshallerSupplier(annotation); + } + + /** + * Create the service instance supplier. + * + * @param cls the service class + * @return the service instance supplier + */ + protected static Supplier createInstanceSupplier(Class cls) { + if (cls.isAnnotationPresent(Singleton.class)) { + return Instance.singleton(cls); + } else { + return Instance.create(cls); + } + } + + /** + * Verify that there are no non-public annotated methods. + */ + protected void checkForNonPublicMethodIssues() { + AnnotatedMethodList allDeclaredMethods = AnnotatedMethodList.create(getAllDeclaredMethods(serviceClass)); + + // log warnings for all non-public annotated methods + allDeclaredMethods.withMetaAnnotation(GrpcMethod.class).isNotPublic() + .forEach(method -> LOGGER.log(Level.WARNING, () -> String.format("The gRPC method, %s, MUST be " + + "public scoped otherwise the method is ignored", method))); + } + + /** + * Obtain the list of method handler suppliers. + * + * @return the list of method handler suppliers + */ + protected List handlerSuppliers() { + return handlerSuppliers; + } + + /** + * Obtain the service instance supplier. + * + * @return the service instance supplier + */ + protected Supplier instanceSupplier() { + return instance; + } + + /** + * Obtain a list of all of the methods declared on the service class. + * + * @param clazz the service class + * @return a list of all of the methods declared on the service class + */ + protected List getAllDeclaredMethods(Class clazz) { + List result = new LinkedList<>(); + Class current = clazz; + while (current != Object.class && current != null) { + result.addAll(Arrays.asList(current.getDeclaredMethods())); + current = current.getSuperclass(); + } + return result; + } + + /** + * Determine the name of the gRPC service. + *

+ * If the class is annotated with {@link Grpc} + * then the name value from the annotation is used as the service name. If the annotation + * has no name value or the annotation is not present the simple name of the class is used. + * + * @param annotatedClass the annotated class + * @return the name of the gRPC service + */ + protected String determineServiceName(Class annotatedClass) { + Grpc serviceAnnotation = annotatedClass.getAnnotation(Grpc.class); + String name = null; + + if (serviceAnnotation != null) { + name = serviceAnnotation.name().trim(); + } + + if (name == null || name.trim().isEmpty()) { + name = annotatedClass.getSimpleName(); + } + + return name; + } + + /** + * Determine the name to use from the method. + *

+ * If the method is annotated with {@link GrpcMethod} then use the value of {@link GrpcMethod#name()} + * unless {@link GrpcMethod#name()} returns empty string, in which case use the actual method name. + *

+ * If the method is annotated with an annotation that has the meta-annotation {@link GrpcMethod} then use + * the value of that annotation's {@code name()} method. If that annotation does not have a {@code name()} + * method or the {@code name()} method return empty string then use the actual method name. + * + * @param method the annotated method + * @param annotation the method type annotation + * @return the value to use for the method name + */ + public static String determineMethodName(AnnotatedMethod method, GrpcMethod annotation) { + Annotation actualAnnotation = method.annotationsWithMetaAnnotation(GrpcMethod.class) + .findFirst() + .orElse(annotation); + + String name = null; + try { + Method m = actualAnnotation.annotationType().getMethod("name"); + name = (String) m.invoke(actualAnnotation); + } catch (NoSuchMethodException e) { + LOGGER.log(Level.WARNING, () -> String.format("Annotation %s has no name() method", actualAnnotation)); + } catch (IllegalAccessException | InvocationTargetException e) { + LOGGER.log(Level.WARNING, e, () -> String.format("Error calling name() method on annotation %s", actualAnnotation)); + } + + if (name == null || name.trim().isEmpty()) { + name = method.method().getName(); + } + + return name; + } + + /** + * Load the {@link io.helidon.microprofile.grpc.core.MethodHandlerSupplier} instances using the {@link java.util.ServiceLoader} + * and return them in priority order. + *

+ * Priority is determined by the value obtained from the {@link jakarta.annotation.Priority} annotation on + * any implementation classes. Classes not annotated with {@link jakarta.annotation.Priority} have a + * priority of zero. + * + * @return a priority ordered list of {@link io.helidon.microprofile.grpc.core.MethodHandlerSupplier} instances + */ + private List loadHandlerSuppliers() { + List list = new ArrayList<>(); + + HelidonServiceLoader.create(ServiceLoader.load(MethodHandlerSupplier.class)).forEach(list::add); + + list.sort((left, right) -> { + Priority leftPriority = left.getClass().getAnnotation(Priority.class); + Priority rightPriority = right.getClass().getAnnotation(Priority.class); + int leftValue = leftPriority == null ? 0 : leftPriority.value(); + int rightValue = rightPriority == null ? 0 : rightPriority.value(); + return leftValue - rightValue; + }); + + return list; + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java new file mode 100644 index 00000000000..53bc1f61375 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java @@ -0,0 +1,488 @@ +/* + * 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.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +/** + * A model of an annotated gRPC method. + */ +public class AnnotatedMethod implements AnnotatedElement { + + /** + * The set of meta-annotations that are used to identify an annotated gRPC method. + */ + private static final Set> METHOD_META_ANNOTATIONS + = Set.of(GrpcMethod.class); + + /** + * The set of annotations that are used to identify an annotated gRPC method. + */ + private static final Set> METHOD_ANNOTATIONS + = Set.of(GrpcMethod.class, + Bidirectional.class, + ClientStreaming.class, + ServerStreaming.class, + Unary.class); + + /** + * The set of method parameter annotations that are used to identify an annotated gRPC method. + */ + private static final Set> PARAMETER_ANNOTATIONS = Set.of(); + + /** + * The declared method this {@link AnnotatedMethod} represents. + */ + private final Method declaredMethod; + + /** + * The actual annotated gRPC method. + *

+ * This may be the same as or overridden by the {@link #declaredMethod}. + */ + private final Method actualMethod; + + /** + * The annotations present on the method. + *

+ * This is a merged set of annotations from both the declared and actual methods. + */ + private final Annotation[] methodAnnotations; + + /** + * The annotations present on the method's parameters. + *

+ * This is a merged set of annotations from both the declared and actual methods. + */ + private final Annotation[][] parameterAnnotations; + + /** + * Create annotated method instance from a {@link Method Java method}. + * + * @param method the Java method + */ + private AnnotatedMethod(Method method) { + this.declaredMethod = method; + this.actualMethod = findAnnotatedMethod(method); + + if (method.equals(actualMethod)) { + methodAnnotations = method.getAnnotations(); + parameterAnnotations = method.getParameterAnnotations(); + } else { + methodAnnotations = mergeMethodAnnotations(method, actualMethod); + parameterAnnotations = mergeParameterAnnotations(method, actualMethod); + } + } + + /** + * Create an {@link AnnotatedMethod} instance from a {@link Method Java method}. + * + * @param method the Java method + * @throws java.lang.NullPointerException if the method parameter is null + * @return an {@link AnnotatedMethod} instance representing the Java method + */ + public static AnnotatedMethod create(Method method) { + return new AnnotatedMethod(Objects.requireNonNull(method)); + } + + /** + * Get the underlying Java method. + *

+ * This will be the method that is actually annotated with {@link GrpcMethod}, + * which may be the same as or overridden by the method returned by {@link #declaredMethod()}. + * + * @return the actual annotated Java method. + */ + public Method method() { + return actualMethod; + } + + /** + * Get the declared Java method. + *

+ * This method overrides, or is the same as, the one retrieved by {@link #method()}. + * + * @return the declared Java method. + */ + public Method declaredMethod() { + return declaredMethod; + } + + /** + * Get method parameter annotations. + * + * @return method parameter annotations. + */ + public Annotation[][] parameterAnnotations() { + return parameterAnnotations.clone(); + } + + /** + * Get method parameter types. + * + * See also {@link Method#getParameterTypes()}. + * + * @return method parameter types. + */ + public Class[] parameterTypes() { + return actualMethod.getParameterTypes(); + } + + /** + * Get method type parameters. + * + * See also {@link Method#getTypeParameters()}. + * + * @return method type parameters. + */ + public TypeVariable[] typeParameters() { + return actualMethod.getTypeParameters(); + } + + /** + * Get generic method parameter types. + * + * See also {@link Method#getGenericParameterTypes()}. + * + * @return generic method parameter types. + */ + public Type[] genericParameterTypes() { + return actualMethod.getGenericParameterTypes(); + } + + /** + * Get generic method return type. + * + * See also {@link Method#getGenericReturnType()} ()}. + * + * @return generic method return types. + */ + public Type genericReturnType() { + return actualMethod.getGenericReturnType(); + } + + /** + * Get method return type. + * + * See also {@link Method#getReturnType()} ()} ()}. + * + * @return method return types. + */ + public Class returnType() { + return actualMethod.getReturnType(); + } + + /** + * Get all instances of the specified meta-annotation type found on the method + * annotations (a meta-annotation is an annotation that annotates other + * annotations). + * + * @param annotation meta-annotation class to be searched for. + * @param meta-annotation type. + * + * @return meta-annotation instances of a given type annotating the method + * annotations. + */ + public Stream metaMethodAnnotations(Class annotation) { + return Arrays.stream(methodAnnotations) + .map(ann -> ann.annotationType().getAnnotation(annotation)) + .filter(Objects::nonNull); + } + + /** + * Get the first of the specified meta-annotation type found on the method + * annotations or on the method itself (a meta-annotation is an annotation that + * annotates other annotations). + * + * @param type meta-annotation class to be searched for. + * @param meta-annotation type. + * + * @return meta-annotation instances of a given type annotating the method + * annotations + */ + public T firstAnnotationOrMetaAnnotation(Class type) { + return annotationOrMetaAnnotation(type).findFirst().orElse(null); + } + + /** + * Get all instances of the specified meta-annotation type found on the method + * annotations or on the method itself (a meta-annotation is an annotation that + * annotates other annotations). + * + * @param type meta-annotation class to be searched for. + * @param meta-annotation type. + * + * @return meta-annotation instances of a given type annotating the method + * annotations + */ + public Stream annotationOrMetaAnnotation(Class type) { + return Arrays.stream(methodAnnotations) + .map(ann -> annotationOrMetaAnnotation(type, ann)) + .filter(Objects::nonNull); + } + + /** + * Get all instances of annotations annotated with the specified meta-annotation. + * + * @param type meta-annotation class to be searched for. + * + * @return all instances of annotations annotated with the specified meta-annotation + */ + public Stream annotationsWithMetaAnnotation(Class type) { + return Arrays.stream(methodAnnotations) + .filter(ann -> ann.annotationType().isAnnotationPresent(type)); + } + + @SuppressWarnings("unchecked") + private T annotationOrMetaAnnotation(Class type, Annotation annotation) { + if (annotation.annotationType().equals(type)) { + return (T) annotation; + } else { + return annotation.annotationType().getAnnotation(type); + } + } + + @Override + public T getAnnotation(Class annotationType) { + return Arrays.stream(methodAnnotations) + .filter(ma -> ma.annotationType() == annotationType) + .map(annotationType::cast) + .findFirst() + .orElse(actualMethod.getAnnotation(annotationType)); + } + + @Override + public Annotation[] getAnnotations() { + return methodAnnotations.clone(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return getAnnotations(); + } + + @Override + public String toString() { + return declaredMethod.toString(); + } + + /** + * Merge the annotations from two methods. + *

+ * Where both methods have the same annotation then the annotation from the + * declared method will be used. + * + * @param declaredMethod the declared method + * @param actualMethod the method that the declared method overrides + * @return an array of merged annotations + */ + private static Annotation[] mergeMethodAnnotations(Method declaredMethod, Method actualMethod) { + List list = new ArrayList<>(Arrays.asList(declaredMethod.getAnnotations())); + + Arrays.stream(actualMethod.getAnnotations()) + .filter(a -> !declaredMethod.isAnnotationPresent(a.getClass())) + .forEach(list::add); + + return list.toArray(new Annotation[0]); + } + + /** + * Merge the parameter annotations from two methods. + *

+ * Where a parameter has the same annotation in both methods + * then the annotation from the declared method will be used. + * + * @param declaredMethod the declared method + * @param actualMethod the method that the declared method overrides + * @return an array of merged annotations + */ + private static Annotation[][] mergeParameterAnnotations(Method declaredMethod, Method actualMethod) { + Annotation[][] methodParamAnnotations = declaredMethod.getParameterAnnotations(); + Annotation[][] annotatedMethodParamAnnotations = actualMethod.getParameterAnnotations(); + + List> methodParamAnnotationsList = new ArrayList<>(); + + for (int i = 0; i < methodParamAnnotations.length; i++) { + List al = Arrays.asList(methodParamAnnotations[i]); + for (Annotation a : annotatedMethodParamAnnotations[i]) { + if (annotationNotInList(a.getClass(), al)) { + al.add(a); + } + } + methodParamAnnotationsList.add(al); + } + + Annotation[][] mergedAnnotations = new Annotation[methodParamAnnotations.length][]; + for (int i = 0; i < methodParamAnnotations.length; i++) { + List paramAnnotations = methodParamAnnotationsList.get(i); + mergedAnnotations[i] = paramAnnotations.toArray(new Annotation[0]); + } + + return mergedAnnotations; + } + + private static boolean annotationNotInList(Class type, List annotations) { + return annotations.stream().noneMatch(annotation -> type == annotation.getClass()); + } + + /** + * Find the actual annotated gRPC method given a declared method. + *

+ * A declared method may itself be annotated with gRPC method annotations + * or it may override a method annotated with gRPC method annotations. + * If the declared method is an annotated gRPC method then it will be returned, + * if not then the first overridden annotated gRPC method in the class hierarchy + * will be returned or if no method in the class hierarchy is annotated then the + * declared method will be returned. + *

+ * The search order for finding an overridden annotated method is to search the + * class hierarchy before searching the implemented interfaces. + * + * @param declaredMethod the declared method + * @return the actual annotated gRPC method or the declared method if no + * method in the class hierarchy is annotated + */ + private static Method findAnnotatedMethod(Method declaredMethod) { + Method am = findAnnotatedMethod(declaredMethod.getDeclaringClass(), declaredMethod); + return (am != null) ? am : declaredMethod; + } + + /** + * Find the actual annotated gRPC method given a declared method. + *

+ * A declared method may itself be annotated with gRPC method annotations + * or it may override a method annotated with gRPC method annotations. + * If the declared method is an annotated gRPC method then it will be returned, + * if not then the first overridden annotated gRPC method in the class hierarchy + * will be returned or if no method in the class hierarchy is annotated then the + * declared method will be returned. + *

+ * The search order for finding an overridden annotated method is to search the + * class hierarchy before searching the implemented interfaces. + * + * @param declaringClass the Class declaring the method + * @param declaredMethod the declared method + * @return the actual annotated gRPC method or the declared method if no + * method in the class hierarchy is annotated + */ + private static Method findAnnotatedMethod(Class declaringClass, Method declaredMethod) { + if (declaringClass == Object.class) { + return null; + } + + declaredMethod = ModelHelper.findMethodOnClass(declaringClass, declaredMethod); + if (declaredMethod == null) { + return null; + } + + if (hasAnnotations(declaredMethod)) { + return declaredMethod; + } + + // Super classes take precedence over interfaces + Class sc = declaringClass.getSuperclass(); + if (sc != null && sc != Object.class) { + Method sm = findAnnotatedMethod(sc, declaredMethod); + if (sm != null) { + return sm; + } + } + + for (Class ic : declaringClass.getInterfaces()) { + Method im = findAnnotatedMethod(ic, declaredMethod); + if (im != null) { + return im; + } + } + + return null; + } + + /** + * Determine whether a method is annotated with any of the annotations, + * meta-annotations or parameter annotations that would make it a recognised + * gRPC method. + * + * @param method the {@link Method} to test + * @return {@code true} if the method is an annotated gRPC method + */ + private static boolean hasAnnotations(Method method) { + return hasMetaMethodAnnotations(method) + || hasMethodAnnotations(method) + || hasParameterAnnotations(method); + } + + /** + * Determine whether a method is annotated with any of the meta-annotations + * that would make it a recognised gRPC method. + * + * @param method the {@link Method} to test + * @return {@code true} if the method is an annotated gRPC method + */ + private static boolean hasMetaMethodAnnotations(Method method) { + return METHOD_META_ANNOTATIONS.stream() + .anyMatch(a -> hasMetaAnnotation(method, a)); + } + + /** + * Determine whether any of a method's annotations are themselves annotated + * with a specific annotation. + * + * @param method the method to test + * @param type the type of the meta-annotation to search for + * @return {@link true} if any of the method's annotations have the + * specified meta-annotation + */ + private static boolean hasMetaAnnotation(Method method, Class type) { + return Arrays.stream(method.getAnnotations()) + .anyMatch(a -> a.annotationType().isAnnotationPresent(type)); + } + + /** + * Determine whether a method is annotated with any of the annotations + * that would make it a recognised gRPC method. + * + * @param method the {@link Method} to test + * @return {@code true} if the method is an annotated gRPC method + */ + private static boolean hasMethodAnnotations(Method method) { + return METHOD_ANNOTATIONS.stream().anyMatch(method::isAnnotationPresent); + } + + /** + * Determine whether a method has parameters that are annotated with any of + * the annotations that would make it a recognised gRPC method. + * + * @param method the {@link Method} to test + * @return {@code true} if the method is an annotated gRPC method + */ + private static boolean hasParameterAnnotations(Method method) { + return Arrays.stream(method.getParameterAnnotations()) + .flatMap(Arrays::stream) + .anyMatch(a -> PARAMETER_ANNOTATIONS.contains(a.annotationType())); + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethodList.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethodList.java new file mode 100644 index 00000000000..f6997f68df2 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethodList.java @@ -0,0 +1,261 @@ +/* + * 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.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Iterable list of {@link AnnotatedMethod}s on a single class with convenience + * getters to provide additional method information. + */ +public class AnnotatedMethodList implements Iterable { + + private final AnnotatedMethod[] methods; + + /** + * Create new method list from the given array of {@link AnnotatedMethod + * annotated methods}. + * + * @param methods methods to be included in the method list. + */ + private AnnotatedMethodList(AnnotatedMethod... methods) { + this.methods = methods; + } + + /** + * Create an annotated method list for a class. + *

+ * The method list contains {@link Class#getMethods() all methods} available + * on the class. + *

+ * The {@link java.lang.reflect.Method#isBridge() bridge methods} and methods declared directly + * on the {@link Object} class are filtered out. + * + * @param cls class from which the method list is created + * @return an {@link AnnotatedMethodList} containing {@link AnnotatedMethod} instances for + * all of the methods of the specified class + */ + public static AnnotatedMethodList create(Class cls) { + return create(cls, false); + } + + /** + * Create an annotated method list for a class. + *

+ * The method list contains {@link Class#getMethods() all methods} available + * on the class or {@link Class#getDeclaredMethods() declared methods} only, + * depending on the value of the {@code declaredMethods} parameter. + *

+ * The {@link java.lang.reflect.Method#isBridge() bridge methods} and methods declared directly + * on the {@link Object} class are filtered out. + * + * @param cls class from which the method list is created + * @param declaredMethods if {@code true} only the {@link Class#getDeclaredMethods() + * declared methods} will be included in the method list; otherwise + * {@link Class#getMethods() all methods} will be listed + * @return an {@link AnnotatedMethodList} containing {@link AnnotatedMethod} instances for + * the methods of the specified class + */ + public static AnnotatedMethodList create(Class cls, boolean declaredMethods) { + return create(declaredMethods ? allDeclaredMethods(cls) : methodList(cls)); + } + + /** + * Create an annotated method list from the given collection of methods. + *

+ * The {@link Method#isBridge() bridge methods} and methods declared directly + * on the {@link Object} class are filtered out. + * + * @param methods methods to be included in the method list. + * @return an {@link AnnotatedMethodList} containing {@link AnnotatedMethod} instances for + * the methods of the specified class + */ + public static AnnotatedMethodList create(Collection methods) { + AnnotatedMethod[] annotatedMethods + = methods.stream() + .filter(m -> !m.isBridge() && m.getDeclaringClass() != Object.class) + .map(AnnotatedMethod::create) + .toArray(AnnotatedMethod[]::new); + + return new AnnotatedMethodList(annotatedMethods); + } + + /** + * Iterator over the list of {@link AnnotatedMethod annotated methods} contained + * in this method list. + * + * @return method list iterator. + */ + @Override + public Iterator iterator() { + return Arrays.asList(methods).iterator(); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list that are not public. + * + * @return new filtered method sub-list. + */ + public AnnotatedMethodList isNotPublic() { + return filter(m -> !Modifier.isPublic(m.method().getModifiers())); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list that have the specific number of parameters. + * + * @param paramCount number of method parameters. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList hasParameterCount(int paramCount) { + return filter(m -> m.parameterTypes().length == paramCount); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list that declare the specified return type. + * + * @param returnType method return type. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList hasReturnType(Class returnType) { + return filter(m -> m.method().getReturnType() == returnType); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list with a specified method name prefix. + * + * @param prefix method name prefix. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList nameStartsWith(String prefix) { + return filter(m -> m.method().getName().startsWith(prefix)); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list with a specified method-level annotation declared. + * + * @param annotation type. + * + * @param annotation annotation class. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList withAnnotation(Class annotation) { + return filter(m -> m.getAnnotation(annotation) != null); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list with a method-level annotation declared that is itself annotated with + * a specified meta-annotation. + * + * @param meta-annotation type. + * + * @param annotation meta-annotation class. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList withMetaAnnotation(Class annotation) { + return filter(m -> { + for (Annotation a : m.getAnnotations()) { + if (a.annotationType().getAnnotation(annotation) != null) { + return true; + } + } + return false; + }); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list without a specified method-level annotation declared. + * + * @param annotation type. + * + * @param annotation annotation class. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList withoutAnnotation(Class annotation) { + return filter(m -> !m.isAnnotationPresent(annotation)); + } + + /** + * Get a new sub-list of methods containing all the methods from this method + * list without any method-level annotation declared that would itself be + * annotated with a specified meta-annotation. + * + * @param meta-annotation type. + * + * @param annotation meta-annotation class. + * @return new filtered method sub-list. + */ + public AnnotatedMethodList withoutMetaAnnotation(Class annotation) { + return filter(m -> { + for (Annotation a : m.getAnnotations()) { + if (a.annotationType().getAnnotation(annotation) != null) { + return false; + } + } + return true; + }); + } + + /** + * Obtain a {@link Stream} of the {@link Method}s in this {@link AnnotatedMethodList}. + * + * @return a {@link Stream} of the {@link Method}s in this {@link AnnotatedMethodList} + */ + public Stream stream() { + return Arrays.stream(methods); + } + + /** + * Created a new method list containing only the methods supported by the + * {@link Predicate method list predicate}. + * + * @param predicate method list predicate. + * + * @return new filtered method list. + */ + public AnnotatedMethodList filter(Predicate predicate) { + return new AnnotatedMethodList(stream().filter(predicate).toArray(AnnotatedMethod[]::new)); + } + + private static List methodList(Class c) { + return Arrays.asList(c.getMethods()); + } + + private static List allDeclaredMethods(Class c) { + List l = new ArrayList<>(); + while (c != null && c != Object.class) { + l.addAll(ModelHelper.getDeclaredMethods(c)); + c = c.getSuperclass(); + } + return l; + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Bidirectional.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Bidirectional.java new file mode 100644 index 00000000000..6f5dc4df63a --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Bidirectional.java @@ -0,0 +1,46 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a class as representing a + * bi-directional streaming gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@GrpcMethod(type = MethodType.BIDI_STREAMING) +@Documented +@Inherited +public @interface Bidirectional { + /** + * Obtain the name of the method. + *

+ * If not set the name of the actual annotated method is used. + * + * @return name of the method + */ + String name() default ""; +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java new file mode 100644 index 00000000000..3ba8f0339e2 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java @@ -0,0 +1,175 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.function.Supplier; + +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.core.MethodHandler; + +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for bi-directional streaming gRPC methods. + */ +public class BidirectionalMethodHandlerSupplier + extends AbstractMethodHandlerSupplier { + + /** + * Create a supplier of handlers for bi-directional streaming methods. + */ + // method is public because it is loaded via ServiceLoader + public BidirectionalMethodHandlerSupplier() { + super(MethodDescriptor.MethodType.BIDI_STREAMING); + } + + @Override + public boolean supplies(AnnotatedMethod method) { + return super.supplies(method) && determineCallType(method) != CallType.unknown; + } + + @Override + public MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance) { + if (!isRequiredMethodType(method)) { + throw new IllegalArgumentException("Method not annotated as a bi-directional streaming method: " + method); + } + + CallType type = determineCallType(method); + MethodHandler handler; + + switch (type) { + case bidiStreaming: + handler = new BidiStreaming<>(methodName, method, instance); + break; + case unknown: + default: + throw new IllegalArgumentException("Not a supported bi-directional streaming method signature: " + method); + } + return handler; + } + + private CallType determineCallType(AnnotatedMethod method) { + Type returnType = method.returnType(); + CallType callType; + + Type[] parameterTypes = method.parameterTypes(); + int paramCount = parameterTypes.length; + + if (paramCount == 1) { + if (StreamObserver.class.equals(parameterTypes[0]) + && StreamObserver.class.equals(returnType)) { + // Assume that the first parameter is the response observer value + // and the return is the request observer + // Signature is StreamObserver invoke(StreamObserver) + callType = CallType.bidiStreaming; + } else { + // Signature is unsupported - invoke() + callType = CallType.unknown; + } + } else { + // Signature is unsupported + callType = CallType.unknown; + } + + return callType; + } + + // ----- CallType enumeration ------------------------------------------- + + /** + * An enumeration representing different supported types + * of bi-directional streaming method signatures. + */ + private enum CallType { + /** + * An standard bi-directional streaming call. + *

+         *     StreamObserver<ReqT> invoke(StreamObserver<RespT> observer)
+         * 
+ */ + bidiStreaming, + /** + * A call type not recognised by this supplier. + */ + unknown + } + + // ----- call handler inner classes ------------------------------------- + + /** + * A base class for bi-directional streaming {@link MethodHandler}s. + * + * @param the request type + * @param the response type + */ + public abstract static class AbstractServerStreamingHandler + extends AbstractHandler { + + AbstractServerStreamingHandler(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance, MethodDescriptor.MethodType.BIDI_STREAMING); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + } + + // ----- BidiStreaming call handler ------------------------------------- + + /** + * A bi-directional streaming {@link MethodHandler} that + * calls a standard bi-directional streaming method handler method of the form. + *
+     *     StreamObserver<ReqT> invoke(StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class BidiStreaming + extends AbstractServerStreamingHandler { + + BidiStreaming(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(getGenericResponseType(method.genericReturnType())); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + @SuppressWarnings("unchecked") + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + return (StreamObserver) method.invoke(instance, observer); + } + + @Override + @SuppressWarnings("unchecked") + public Object bidirectional(Object[] args, BidirectionalClient client) { + try { + return client.bidiStreaming(methodName(), (StreamObserver) args[0]); + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java new file mode 100644 index 00000000000..45822c54560 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java @@ -0,0 +1,46 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a class as representing a + * client streaming gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@GrpcMethod(type = MethodType.CLIENT_STREAMING) +@Documented +@Inherited +public @interface ClientStreaming { + /** + * Obtain the name of the method. + *

+ * If not set the name of the actual annotated method is used. + * + * @return name of the method + */ + String name() default ""; +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java new file mode 100644 index 00000000000..e95350df1dc --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java @@ -0,0 +1,399 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.helidon.grpc.core.MethodHandler; + +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.completeAsync; + +/** + * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for client streaming gRPC methods. + */ +public class ClientStreamingMethodHandlerSupplier + extends AbstractMethodHandlerSupplier { + + /** + * Create a supplier of handlers for client streaming methods. + */ + // method is public because it is loaded via ServiceLoader + public ClientStreamingMethodHandlerSupplier() { + super(MethodDescriptor.MethodType.CLIENT_STREAMING); + } + + @Override + public boolean supplies(AnnotatedMethod method) { + return super.supplies(method) && determineCallType(method) != CallType.unknown; + } + + @Override + @SuppressWarnings("unchecked") + public MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance) { + if (!isRequiredMethodType(method)) { + throw new IllegalArgumentException("Method not annotated as a client streaming method: " + method); + } + + CallType type = determineCallType(method); + MethodHandler handler; + + switch (type) { + case clientStreaming: + handler = new ClientStreaming<>(methodName, method, instance); + break; + case futureResponse: + handler = new FutureResponse<>(methodName, method, instance); + break; + case clientStreamingIterable: + handler = new ClientStreamingIterable(methodName, method, instance); + break; + case clientStreamingStream: + handler = new ClientStreamingStream(methodName, method, instance); + break; + case unknown: + default: + throw new IllegalArgumentException("Not a supported client streaming method signature: " + method); + } + return handler; + } + + private CallType determineCallType(AnnotatedMethod method) { + Class returnType = method.returnType(); + Class[] parameterTypes = method.parameterTypes(); + int paramCount = parameterTypes.length; + CallType callType; + + if (paramCount == 1) { + if (StreamObserver.class.isAssignableFrom(parameterTypes[0]) + && StreamObserver.class.equals(returnType)) { + // Assume that the first parameter is the response observer value + // and the return is the request observer + // Signature is StreamObserver invoke(StreamObserver) + callType = CallType.clientStreaming; + } else if (Iterable.class.isAssignableFrom(parameterTypes[0]) + && CompletionStage.class.isAssignableFrom(returnType)) { + // ** This is a client side only handler ** + // Assume that the first parameter is the requests to stream + // and the return is the response + // Signature is CompletionStage invoke(Iterable) + callType = CallType.clientStreamingIterable; + } else if (Stream.class.isAssignableFrom(parameterTypes[0]) + && CompletionStage.class.isAssignableFrom(returnType)) { + // ** This is a client side only handler ** + // Assume that the first parameter is the requests to stream + // and the return is the response + // Signature is CompletionStage invoke(Stream) + callType = CallType.clientStreamingStream; + } else if (CompletionStage.class.isAssignableFrom(parameterTypes[0]) + && StreamObserver.class.equals(returnType)) { + // Assume that the first parameter is the response CompletableStage value + // and the return is the request observer + // Signature is StreamObserver invoke(CompletableStage) + callType = CallType.futureResponse; + } else { + // Signature is unsupported - invoke() + callType = CallType.unknown; + } + } else { + // Signature is unsupported + callType = CallType.unknown; + } + + return callType; + } + + // ----- CallType enumeration ------------------------------------------- + + /** + * An enumeration representing different supported types + * of client streaming method signatures. + */ + private enum CallType { + /** + * A standard client streaming call. + *

+         *     StreamObserver<ReqT> invoke(StreamObserver<RespT> observer)
+         * 
+ */ + clientStreaming, + /** + * A client side only client streaming call with an iterable request. + *
+         *     RespT invoke(Iterable<ReqT> requests)
+         * 
+ */ + clientStreamingIterable, + /** + * A client side only client streaming call with an stream request. + *
+         *     RespT invoke(Stream<ReqT> requests)
+         * 
+ */ + clientStreamingStream, + /** + * A standard client streaming call with an async response. + *
+         *     StreamObserver<ReqT> invoke(CompletionStage<RespT> future)
+         * 
+ */ + futureResponse, + /** + * A call type not recognised by this supplier. + */ + unknown + } + + // ----- call handler inner classes ------------------------------------- + + /** + * A base class for client streaming {@link MethodHandler}s. + * + * @param the request type + * @param the response type + */ + public abstract static class AbstractClientStreamingHandler + extends AbstractHandler { + + AbstractClientStreamingHandler(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance, MethodDescriptor.MethodType.CLIENT_STREAMING); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + } + + // ----- ClientStreaming call handler ----------------------------------- + + /** + * A client streaming {@link MethodHandler} that + * calls a standard client streaming method handler method of the form. + *
+     *     StreamObserver<ReqT> invoke(StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ClientStreaming + extends AbstractClientStreamingHandler { + + ClientStreaming(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(getGenericResponseType(method.genericReturnType())); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + @SuppressWarnings("unchecked") + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + return (StreamObserver) method.invoke(instance, observer); + } + + @Override + @SuppressWarnings("unchecked") + public Object clientStreaming(Object[] args, ClientStreaming client) { + return client.clientStreaming(methodName(), (StreamObserver) args[0]); + } + } + + // ----- FutureResponse call handler ------------------------------------ + + /** + * A client streaming {@link MethodHandler} that + * calls a standard client streaming method handler method of the form. + *
+     *     StreamObserver<ReqT> invoke(CompletableFuture<RespT> future)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class FutureResponse + extends AbstractClientStreamingHandler { + + FutureResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(getGenericResponseType(method.genericReturnType())); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + @SuppressWarnings("unchecked") + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + CompletableFuture future = new CompletableFuture<>(); + completeAsync(observer, future); + return (StreamObserver) method.invoke(instance, future); + } + + @Override + @SuppressWarnings("unchecked") + public Object clientStreaming(Object[] args, ClientStreaming client) { + FutureObserver observer = new FutureObserver<>((CompletableFuture) args[0]); + return client.clientStreaming(methodName(), observer); + } + } + + // ----- ClientStreamingIterable call handler --------------------------- + + /** + * A client side only client streaming {@link MethodHandler} that + * streams requests from an iterable. + *
+     *     CompletionStage<RespT> invoke(Iterable<ReqT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ClientStreamingIterable + extends AbstractClientStreamingHandler { + + ClientStreamingIterable(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(getGenericResponseType(method.genericReturnType())); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + public boolean clientOnly() { + return true; + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + @Override + @SuppressWarnings("unchecked") + public Object clientStreaming(Object[] args, ClientStreaming client) { + try { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver responseObserver = new FutureObserver<>(future); + Iterable iterable = (Iterable) args[0]; + StreamObserver requestObserver = client.clientStreaming(methodName(), responseObserver); + + for (Object value : iterable) { + requestObserver.onNext(value); + } + + requestObserver.onCompleted(); + return future; + } catch (Throwable thrown) { + throw Status.INTERNAL.withCause(thrown).asRuntimeException(); + } + } + } + + + // ----- ClientStreamingIterable call handler --------------------------- + + /** + * A client side only client streaming {@link MethodHandler} that + * streams requests from a stream. + *
+     *     CompletionStage<RespT> invoke(Stream<ReqT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ClientStreamingStream + extends AbstractClientStreamingHandler { + + ClientStreamingStream(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(getGenericResponseType(method.genericReturnType())); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + public boolean clientOnly() { + return true; + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + @Override + @SuppressWarnings("unchecked") + public Object clientStreaming(Object[] args, ClientStreaming client) { + try { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver responseObserver = new FutureObserver<>(future); + StreamObserver requestObserver = client.clientStreaming(methodName(), responseObserver); + Stream stream = (Stream) args[0]; + + stream.forEach(requestObserver::onNext); + requestObserver.onCompleted(); + return future; + } catch (Throwable thrown) { + throw Status.INTERNAL.withCause(thrown).asRuntimeException(); + } + } + } + + + /** + * A {@link StreamObserver} that completes a {@link CompletableFuture} + * with its received result. + * + * @param the result type + */ + private static class FutureObserver + implements StreamObserver { + + private CompletableFuture future; + private T value; + + private FutureObserver(CompletableFuture future) { + this.future = future; + } + + @Override + public void onNext(T value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java new file mode 100644 index 00000000000..ddd59270e07 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java @@ -0,0 +1,75 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +/** + * An annotation used to mark a class as representing a gRPC service. + */ +@Qualifier +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Grpc { + /** + * Obtain the service name. + * + * @return the service name + */ + String name() default ""; + + /** + * Obtain the service version. + * + * @return the service version + */ + int version() default 0; + + /** + * An {@link jakarta.enterprise.util.AnnotationLiteral} for the {@link Grpc} annotation. + */ + class Literal + extends AnnotationLiteral implements Grpc { + + /** + * The singleton instance of {@link Literal}. + */ + public static final Literal INSTANCE = new Literal(); + + private static final long serialVersionUID = 1L; + + @Override + public String name() { + return ""; + } + + @Override + public int version() { + return 0; + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcCdiExtension.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcCdiExtension.java new file mode 100644 index 00000000000..2cd38e4d2c1 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcCdiExtension.java @@ -0,0 +1,52 @@ +/* + * 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.core; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessAnnotatedType; +import jakarta.enterprise.inject.spi.WithAnnotations; + +/** + * An extension that processes beans as they are discovered. + */ +public class GrpcCdiExtension implements Extension { + + /** + * Determine whether a discovered bean has a superclass or implements an + * interface that is annotated with {@link Grpc} and if so then also + * annotate the bean with the same annotation. + *

+ * This is required so that we can support the use-case where an interface has been + * annotated with {@link Grpc} but the implementation class has not but the + * implementation class is annotated with a bean discovering annotation such as + * {@link jakarta.enterprise.context.ApplicationScoped}. We need to make sure that the + * gRPC server can locate beans so we add the {@link Grpc} from the interface to + * the bean. + * + * @param event the {@link ProcessAnnotatedType} event + */ + public void beforeBean(@Observes @WithAnnotations(Grpc.class) ProcessAnnotatedType event) { + AnnotatedType type = event.getAnnotatedType(); + Class javaClass = type.getJavaClass(); + Class annotatedClass = ModelHelper.getAnnotatedResourceClass(javaClass, Grpc.class); + if (annotatedClass != javaClass && annotatedClass.isAnnotationPresent(Grpc.class)) { + event.configureAnnotatedType().add(annotatedClass.getAnnotation(Grpc.class)); + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java new file mode 100644 index 00000000000..0aa9efd805a --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java @@ -0,0 +1,76 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies that a class is a gRPC interceptor. + *

+ * The class should be a discoverable CDI bean, for example it could be discovered + * by being annotated with a valid CDI scope. + * + *

+ * The annotated class can be a {@link io.grpc.ServerInterceptor}. + *

+ * @Interceptor
+ * @ApplicationScoped
+ * public class ValidationInterceptor
+ *         implements io.grpc.ServerInterceptor { ... }
+ * 
+ *

+ * + * Or the annotated class can be a {@link io.grpc.ClientInterceptor}. + *

+ * @Interceptor
+ * @ApplicationScoped
+ * public class ValidationInterceptor
+ *         implements io.grpc.ClientInterceptor { ... }
+ * 
+ *

+ * + * This annotation is optional if the {@link GrpcInterceptors Interceptors} + * annotation or is used to associate the interceptor with the target class. + * It is required when a {@linkplain GrpcInterceptorBinding interceptor binding} + * is used.

+ */ +@Qualifier +@Retention(RUNTIME) +@Target(TYPE) +@Documented +public @interface GrpcInterceptor { + + /** + * An {@link AnnotationLiteral} for the {@link GrpcInterceptor} annotation. + */ + class Literal extends AnnotationLiteral implements GrpcInterceptor { + /** + * The singleton instance of {@link GrpcInterceptor.Literal}. + */ + public static final Literal INSTANCE = new Literal(); + + private static final long serialVersionUID = 1L; + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptorBinding.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptorBinding.java new file mode 100644 index 00000000000..9d2e20556a7 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptorBinding.java @@ -0,0 +1,42 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 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. + *

+ * The annotation type that is marked as a binding must be applied to a client of server gRPC interceptor + * implementation class (marked with the {@code jakarta.interceptor.Interceptor @Interceptor} annotation to associate that annotation with an interceptor. The annotation + * may then be applied instead of, or in addition to, the {@code jakarta.interceptor.Interceptors @Interceptors} annotation to specify + * what interceptors are attached to the class or method. + *

+ * The associated annotation type must be associated only with {@link java.lang.annotation.ElementType#TYPE TYPE}s and/or + * {@link java.lang.annotation.ElementType#METHOD METHOD}s. + */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface GrpcInterceptorBinding { +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java new file mode 100644 index 00000000000..9621c68a7f4 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java @@ -0,0 +1,51 @@ +/* + * 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.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares an ordered list of gRPC interceptors for a target gRPC + * service class or a gRPC service method of a target class. + *

+ * The classes specified must be implementations of either + * {@link io.grpc.ClientInterceptor} or {@link io.grpc.ServerInterceptor}. + * + *

+ * @GrpcService
+ * @GrpcInterceptors(ValidationInterceptor.class)
+ * public class OrderService { ... }
+ * 
+ * + *
+ * @Unary
+ * @Interceptors({ValidationInterceptor.class, SecurityInterceptor.class})
+ * public void updateOrder(Order order) { ... }
+ * 
+ */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GrpcInterceptors { + /** + * An ordered list of interceptors. + * + * @return the ordered list of interceptors + */ + Class[] value(); +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java new file mode 100644 index 00000000000..a35d1a72e32 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java @@ -0,0 +1,41 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.grpc.core.MarshallerSupplier; + +/** + * An annotation used to annotate a type or method to specify the + * named marshaller supplier to use for rpc method calls. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface GrpcMarshaller { + /** + * Obtain the type of the {@link io.helidon.grpc.core.MarshallerSupplier} to use. + * @return the type of the {@link io.helidon.grpc.core.MarshallerSupplier} to use + */ + String value() default MarshallerSupplier.DEFAULT; +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMethod.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMethod.java new file mode 100644 index 00000000000..44e6ddc544d --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMethod.java @@ -0,0 +1,52 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a class as representing a gRPC service + * or a method as a gRPC service method. + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface GrpcMethod { + /** + * Obtain the name of the method. + *

+ * If not set the name of the actual annotated method is used. + * + * @return name of the method + */ + String name() default ""; + + /** + * Obtain the gRPC method type. + * + * @return the gRPC method type + */ + MethodType type(); +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java new file mode 100644 index 00000000000..299678351dd --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java @@ -0,0 +1,57 @@ +/* + * 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.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +/** + * An qualifier annotation to specify that an in-process {@link io.grpc.Channel} + * should be injected. + *

+ * For example: + *

+ *     @jakarta.inject.Inject
+ *     @io.helidon.microprofile.grpc.core.InProcessChannel
+ *     private io.grpc.Channel channel;
+ * 
+ */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface InProcessGrpcChannel { + + /** + * An {@link AnnotationLiteral} for the {@link InProcessGrpcChannel} annotation. + */ + class Literal + extends AnnotationLiteral + implements InProcessGrpcChannel { + + /** + * The singleton instance of {@link Literal}. + */ + public static final Literal INSTANCE = new Literal(); + + private static final long serialVersionUID = 1L; + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java new file mode 100644 index 00000000000..a51e0d53808 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java @@ -0,0 +1,158 @@ +/* + * 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.core; + +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import io.grpc.Status; + +/** + * A supplier of instances of objects. + */ +public interface Instance { + + /** + * Create a {@link Supplier} that supplies a + * singleton. + * + * @param instance the singleton instance to supply + * @param the type of instance supplied + * @return the singleton instance + * + * @throws java.lang.NullPointerException if the instance parameter is null + */ + static Supplier singleton(T instance) { + return new SingletonInstance<>(Objects.requireNonNull(instance)); + } + + /** + * Create an {@link Supplier} that supplies a + * singleton of the specified Class. + *

+ * The Class provided must have a no-args default constructor. + * + * @param cls the Class of the singleton instance to supply + * @param the type of instance supplied + * @return the singleton instance of the specified Class + * + * @throws java.lang.NullPointerException if the class is null + */ + static Supplier singleton(Class cls) { + return new SingletonInstance<>(cls); + } + + /** + * Create an {@link Supplier} that supplies a + * new instances of the specified Class. + *

+ * The Class provided must have a no-args default constructor. + *

+ * A new instance of the specified Class is created for every + * call to {@link Supplier#get()}. + * + * @param cls the Class of the singleton instance to supply + * @param the type of instance supplied + * @return the singleton instance of the specified Class + * + * @throws java.lang.NullPointerException if the class is null + */ + static Supplier create(Class cls) { + return new NewInstance<>(cls); + } + + /** + * A {@link Supplier} implementation that supplies new instances + * of a class each time its {@link Supplier#get() get()} method + * is called. + * + * @param the type of instance supplied + */ + class NewInstance + implements Supplier { + + private final Class instanceClass; + + protected NewInstance(Class instanceClass) { + this.instanceClass = Objects.requireNonNull(instanceClass); + } + + @Override + public T get() { + try { + return instanceClass.newInstance(); + } catch (Throwable e) { + throw Status.INTERNAL.withCause(e).asRuntimeException(); + } + } + } + + /** + * A {@link Supplier} implementation that supplies the same singleton + * instance of a value each time its {@link Supplier#get() get()} + * method is called. + * + * @param the type of instance supplied + */ + class SingletonInstance + implements Supplier { + + private final Class instanceClass; + + private T instance; + + private final Lock instanceAccess = new ReentrantLock(true); + + private SingletonInstance(Class instanceClass) { + this.instanceClass = instanceClass; + } + + private SingletonInstance(T instance) { + this.instanceClass = null; + this.instance = Objects.requireNonNull(instance); + } + + @Override + public T get() { + return ensureInstance(); + } + + private T ensureInstance() { + return accessInstance(() -> { + if (instance == null) { + try { + instance = instanceClass.newInstance(); + } catch (Throwable e) { + throw Status.INTERNAL.withCause(e).asRuntimeException(); + } + } + return instance; + }); + } + + private T accessInstance(Supplier operation) { + instanceAccess.lock(); + try { + return operation.get(); + } finally { + instanceAccess.unlock(); + } + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java new file mode 100644 index 00000000000..3c9a908326c --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.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 io.helidon.microprofile.grpc.core; + +import java.util.function.Supplier; + +import io.helidon.grpc.core.MethodHandler; + +/** + * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for {@link AnnotatedMethod}s. + *

+ * Implementation classes may be annotated with {@link jakarta.annotation.Priority} + * to influence their priority order when determining which supplier is used if + * more than one supplier is able to supply a handler for a method. + * The built-in default suppliers have a priority of zero, which is also the default + * priority for classes that are not annotated. + */ +public interface MethodHandlerSupplier { + + /** + * Determine whether this {@link MethodHandlerSupplier} can supply + * a {@link io.helidon.grpc.core.MethodHandler} for a given method and type. + * + * @param method the {@link AnnotatedMethod} to supply a handler for + * @return {@code true} if this supplier can supply a handler for the method + */ + boolean supplies(AnnotatedMethod method); + + /** + * Supply a {@link io.helidon.grpc.core.MethodHandler} for a method. + * @param methodName the gRPC method name + * @param method the method to supply a {@link io.helidon.grpc.core.MethodHandler} for + * @param instance the supplier to supply the actual call handler + * @param the request type + * @param the response type + * @return a {@link io.helidon.grpc.core.MethodHandler} for the method + * @throws java.lang.NullPointerException if the method is null + */ + MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance); +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java new file mode 100644 index 00000000000..1b585fe569b --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java @@ -0,0 +1,322 @@ +/* + * 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.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.stream.StreamSupport; + +import io.helidon.grpc.core.MarshallerSupplier; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Named; + +/** + * Common model helper methods. + */ +public final class ModelHelper { + + /** + * Get the class in the provided resource class ancestor hierarchy that + * is actually annotated with the specified annotation. + *

+ * If the annotation is not present in the class hierarchy the resource class + * is returned. + * + * @param resourceClass resource class + * @param annotation the annotation to look for + * + * @return resource class or it's ancestor that is annotated with + * the specified annotation. + */ + public static Class getAnnotatedResourceClass(Class resourceClass, Class annotation) { + + Class foundInterface = null; + + // traverse the class hierarchy to find the annotation + // Annotation in the super-classes must take precedence over annotation in the + // implemented interfaces + Class cls = resourceClass; + do { + if (cls.getDeclaredAnnotation(annotation) != null) { + return cls; + } + + // if no annotation found on the class currently traversed, check for annotation in the interfaces on this + // level - if not already previously found + if (foundInterface == null) { + for (final Class i : cls.getInterfaces()) { + if (i.getDeclaredAnnotation(annotation) != null) { + // store the interface reference in case no annotation will be found in the super-classes + foundInterface = i; + break; + } + } + } + cls = cls.getSuperclass(); + } while (cls != null); + + if (foundInterface != null) { + return foundInterface; + } + + return resourceClass; + } + + /** + * Get collection of methods declared on given class. + * + * @param clazz class for which to get the declared methods. + * @return methods declared on the {@code clazz} class. + */ + public static Collection getDeclaredMethods(final Class clazz) { + return Arrays.asList(clazz.getDeclaredMethods()); + } + + /** + * Find a method in a class. If there exists a public method on the class + * that has the same name and parameters then that public method is + * returned. + *

+ * Otherwise, if there exists a public method on the class that has + * the same name and the same number of parameters, + * and each generic parameter type, in order, of the public method is equal + * to the generic parameter type, in the same order or is an instance of + * {@link TypeVariable} then that public method is returned. + * + * @param cls the class to search for a public method + * @param m the method to find + * @return public method found. + */ + public static Method findMethodOnClass(final Class cls, final Method m) { + try { + return cls.getMethod(m.getName(), m.getParameterTypes()); + } catch (final NoSuchMethodException e) { + for (final Method method : cls.getMethods()) { + if (method.getName().equals(m.getName()) + && method.getParameterTypes().length == m.getParameterTypes().length) { + if (compareParameterTypes(m.getGenericParameterTypes(), + method.getGenericParameterTypes())) { + return method; + } + } + } + return null; + } + } + + /** + * Compare generic parameter types of two methods. + * + * @param first generic parameter types of the first method. + * @param second generic parameter types of the second method. + * @return {@code true} if the given types are understood to be equal, {@code false} otherwise. + * @see #compareParameterTypes(java.lang.reflect.Type, java.lang.reflect.Type) + */ + private static boolean compareParameterTypes(final Type[] first, final Type[] second) { + for (int i = 0; i < first.length; i++) { + if (!first[i].equals(second[i])) { + if (!compareParameterTypes(first[i], second[i])) { + return false; + } + } + } + return true; + } + + /** + * Compare respective generic parameter types of two methods. + * + * @param first generic parameter type of the first method. + * @param second generic parameter type of the second method. + * @return {@code true} if the given types are understood to be equal, {@code false} otherwise. + */ + @SuppressWarnings("unchecked") + private static boolean compareParameterTypes(final Type first, final Type second) { + if (first instanceof Class) { + final Class clazz = (Class) first; + + if (second instanceof Class) { + return ((Class) second).isAssignableFrom(clazz); + } else if (second instanceof TypeVariable) { + return checkTypeBounds(clazz, ((TypeVariable) second).getBounds()); + } + } + return second instanceof TypeVariable; + } + + @SuppressWarnings("unchecked") + private static boolean checkTypeBounds(final Class type, final Type[] bounds) { + for (final Type bound : bounds) { + if (bound instanceof Class) { + if (!((Class) bound).isAssignableFrom(type)) { + return false; + } + } + } + return true; + } + + /** + * Gets the component type of the array. + * + * @param type must be an array. + * @return array component type. + * @throws IllegalArgumentException in case the type is not an array type. + */ + public static Type getArrayComponentType(final Type type) { + if (type instanceof Class) { + final Class c = (Class) type; + return c.getComponentType(); + } + if (type instanceof GenericArrayType) { + return ((GenericArrayType) type).getGenericComponentType(); + } + + throw new IllegalArgumentException(); + } + + /** + * Get Array class of component type. + * + * @param c the component class of the array + * @return the array class. + */ + public static Class getArrayForComponentType(final Class c) { + try { + final Object o = Array.newInstance(c, 0); + return o.getClass(); + } catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Obtain the named {@link MarshallerSupplier} specified by the annotation. + * + * @param annotation the annotation specifying the {@link MarshallerSupplier}. + * + * @return the {@link MarshallerSupplier} specified by the annotation + */ + public static MarshallerSupplier getMarshallerSupplier(GrpcMarshaller annotation) { + String name = annotation == null ? MarshallerSupplier.DEFAULT : annotation.value(); + + Instance instance = null; + try { + instance = CDI.current().select(MarshallerSupplier.class, new NamedLiteral(name)); + } catch (IllegalStateException e) { + // falls through + } + + if (instance == null || instance.isUnsatisfied()) { + // fall back to service loader discovery + return StreamSupport.stream(ServiceLoader.load(MarshallerSupplier.class).spliterator(), false) + .filter(s -> hasName(s, name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Could not load MarshallerSupplier from annotation " + + annotation)); + } else if (instance.isAmbiguous()) { + throw new IllegalArgumentException("There are multiple MarshallerSupplier beans with name '" + name + "'"); + } + + return instance.get(); + } + + private static boolean hasName(MarshallerSupplier supplier, String name) { + Class cls = supplier.getClass(); + Named named = cls.getAnnotation(Named.class); + + return named != null && Objects.equals(named.value(), name); + } + + /** + * Obtain the generic type for a {@link Type}. + * + * @param type the type to obtain the generic type of + * @return the generic type + */ + public static Class getGenericType(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType() instanceof Class) { + Type t = parameterizedType.getActualTypeArguments()[0]; + if (t instanceof Class) { + return (Class) t; + } else if (t instanceof ParameterizedType) { + // the type is a nested generic e.g. List> + // we're only interested in the outer type, in the example above List + return (Class) ((ParameterizedType) t).getRawType(); + } else { + throw new IllegalArgumentException("Type parameter " + type.toString() + " not a class or " + + "parameterized type whose raw type is a class"); + } + } + } else if (type instanceof GenericArrayType) { + GenericArrayType array = (GenericArrayType) type; + final Class componentRawType = getGenericType(array.getGenericComponentType()); + return getArrayClass(componentRawType); + } + throw new IllegalArgumentException("Type parameter " + type.toString() + " not a class or " + + "parameterized type whose raw type is a class"); + } + + private static Class getArrayClass(Class c) { + try { + Object o = Array.newInstance(c, 0); + return o.getClass(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * An annotation literal for {@link Named}. + */ + static class NamedLiteral extends AnnotationLiteral implements Named { + + private final String value; + + NamedLiteral(String value) { + this.value = value; + } + + @Override + public String value() { + return this.value; + } + } + + /** + * Private constructor for utility classes. + */ + private ModelHelper() { + throw new AssertionError("Instantiation not allowed."); + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java new file mode 100644 index 00000000000..2d615662228 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java @@ -0,0 +1,40 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to indicate the request type of a gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface RequestType { + /** + * Obtain the gRPC request type. + * + * @return the gRPC request type + */ + Class value(); +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java new file mode 100644 index 00000000000..d59738c3dae --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java @@ -0,0 +1,40 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to indicate the response type of a gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ResponseType { + /** + * Obtain the gRPC response type. + * + * @return the gRPC response type + */ + Class value(); +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java new file mode 100644 index 00000000000..6f4c9f85a9b --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java @@ -0,0 +1,46 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a class as representing a + * server streaming gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@GrpcMethod(type = MethodType.SERVER_STREAMING) +@Documented +@Inherited +public @interface ServerStreaming { + /** + * Obtain the name of the method. + *

+ * If not set the name of the actual annotated method is used. + * + * @return name of the method + */ + String name() default ""; +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java new file mode 100644 index 00000000000..f4703f88883 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java @@ -0,0 +1,385 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.core.MethodHandler; + +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.stream; + +/** + * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for server streaming gRPC methods. + */ +public class ServerStreamingMethodHandlerSupplier + extends AbstractMethodHandlerSupplier { + + /** + * Create a supplier of handlers for server streaming methods. + */ + // method is public because it is loaded via ServiceLoader + public ServerStreamingMethodHandlerSupplier() { + super(MethodDescriptor.MethodType.SERVER_STREAMING); + } + + @Override + public boolean supplies(AnnotatedMethod method) { + return super.supplies(method) && determineCallType(method) != CallType.unknown; + } + + @Override + public MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance) { + if (!isRequiredMethodType(method)) { + throw new IllegalArgumentException("Method not annotated as a server streaming method: " + method); + } + + CallType type = determineCallType(method); + MethodHandler handler; + + switch (type) { + case serverStreaming: + handler = new ServerStreaming<>(methodName, method, instance); + break; + case serverStreamingNoRequest: + handler = new ServerStreamingNoRequest<>(methodName, method, instance); + break; + case streamResponse: + handler = new StreamResponse<>(methodName, method, instance); + break; + case streamResponseNoRequest: + handler = new StreamResponseNoRequest<>(methodName, method, instance); + break; + case unknown: + default: + throw new IllegalArgumentException("Not a supported server streaming method signature: " + method); + } + return handler; + } + + private CallType determineCallType(AnnotatedMethod method) { + Type returnType = method.returnType(); + Type[] parameterTypes = method.parameterTypes(); + int paramCount = parameterTypes.length; + boolean voidReturn = void.class.equals(returnType); + CallType callType; + + if (paramCount == 2) { + if (StreamObserver.class.equals(parameterTypes[1]) && voidReturn) { + // Assume that the first parameter is the request value + // Signature is void invoke(ReqT, StreamObserver) + callType = CallType.serverStreaming; + } else { + // Signature is unsupported - invoke(, ) + callType = CallType.unknown; + } + } else if (paramCount == 1) { + if (StreamObserver.class.equals(parameterTypes[0]) && voidReturn) { + // Assume that the first parameter is the result observer and there is no request + // Signature is void invoke(StreamObserver) + callType = CallType.serverStreamingNoRequest; + } else if (Stream.class.equals(returnType)) { + // Assume that the first parameter is the request value and the response is a Stream + // Signature is Stream invoke(ReqT) + callType = CallType.streamResponse; + } else { + // Signature is unsupported - invoke() + callType = CallType.unknown; + } + } else if (paramCount == 0) { + if (Stream.class.equals(returnType)) { + // Assume that the there is no request value and the response is a Stream + // Signature is Stream invoke() + callType = CallType.streamResponseNoRequest; + } else { + // Signature is unsupported - invoke() + callType = CallType.unknown; + } + } else { + // Signature is unsupported - it has more than two parameters + callType = CallType.unknown; + } + + return callType; + } + + // ----- CallType enumeration ------------------------------------------- + + /** + * An enumeration representing different supported types + * of server streaming method signatures. + */ + private enum CallType { + /** + * An standard server streaming call. + *

+         *     void invoke(ReqT request, StreamObserver<RespT> observer)
+         * 
+ */ + serverStreaming, + /** + * A server streaming call that returns a {@link Stream} of responses. + *
+         *     Stream<RespT> invoke(ReqT request)
+         * 
+ */ + streamResponse, + /** + * An server streaming call that takes no request parameter. + *
+         *     void invoke(StreamObserver<RespT> observer)
+         * 
+ */ + serverStreamingNoRequest, + /** + * A server streaming call that takes no request parameter and + * returns a {@link Stream} of responses. + *
+         *     Stream<RespT> invoke()
+         * 
+ */ + streamResponseNoRequest, + /** + * A call type not recognised by this supplier. + */ + unknown + } + + // ----- call handler inner classes ------------------------------------- + + /** + * A base class for server streaming {@link MethodHandler}s. + * + * @param the request type + * @param the response type + */ + public abstract static class AbstractServerStreamingHandler + extends AbstractHandler { + + AbstractServerStreamingHandler(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance, MethodDescriptor.MethodType.SERVER_STREAMING); + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + } + + // ----- ServerStreaming call handler ----------------------------------- + + /** + * A server streaming {@link MethodHandler} that calls a standard server + * streaming method handler method of the form. + *
+     *     void invoke(ReqT request, StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ServerStreaming + extends AbstractServerStreamingHandler { + + ServerStreaming(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(getGenericResponseType(method.genericParameterTypes()[1])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance, request, observer); + } + + @Override + @SuppressWarnings("unchecked") + public Object serverStreaming(Object[] args, ServerStreamingClient client) { + try { + client.serverStreaming(methodName(), args[0], (StreamObserver) args[1]); + return null; + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + } + + // ----- ServerStreamingNoRequest call handler -------------------------- + + /** + * A server streaming {@link MethodHandler} that calls a calls a server + * streaming method handler method of the form. + *
+     *     void invoke(StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ServerStreamingNoRequest + extends AbstractServerStreamingHandler { + + ServerStreamingNoRequest(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance, observer); + } + + @Override + @SuppressWarnings("unchecked") + public Object serverStreaming(Object[] args, ServerStreamingClient client) { + try { + client.serverStreaming(methodName(), Empty.getDefaultInstance(), (StreamObserver) args[0]); + return null; + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + } + + // ----- StreamResponse call handler ------------------------------------ + + /** + * A server streaming {@link MethodHandler} that calls a calls a server + * streaming method handler method of the form. + *
+     *     Stream<RespT> invoke(ReqT request)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class StreamResponse + extends AbstractServerStreamingHandler { + + StreamResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(getGenericResponseType(method.genericReturnType())); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + Stream stream = (Stream) method.invoke(instance, request); + stream(observer, stream); + } + + @Override + public Object serverStreaming(Object[] args, ServerStreamingClient client) { + try { + Observer observer = new Observer<>(); + client.serverStreaming(methodName(), args[0], observer); + return observer.future().get().stream(); + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + } + + // ----- StreamResponse call handler ------------------------------------ + + /** + * A server streaming {@link MethodHandler} that calls a calls a server + * streaming method handler method of the form. + *
+     *     Stream<RespT> invoke()
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class StreamResponseNoRequest + extends AbstractServerStreamingHandler { + + StreamResponseNoRequest(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(getGenericResponseType(method.genericReturnType())); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + Stream stream = (Stream) method.invoke(instance); + stream(observer, stream); + } + + @Override + public Object serverStreaming(Object[] args, ServerStreamingClient client) { + try { + Observer observer = new Observer<>(); + client.serverStreaming(methodName(), Empty.getDefaultInstance(), observer); + return observer.future().get().stream(); + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + } + + /** + * A {@link StreamObserver} that collects all of its responses into a + * {@link List} and then completes a {@link CompletableFuture} when the + * observer completes. + * + * @param the response type + */ + private static class Observer + implements StreamObserver { + + private CompletableFuture> future = new CompletableFuture<>(); + private List list = new ArrayList<>(); + + private CompletableFuture> future() { + return future; + } + + @Override + public void onNext(T value) { + list.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(list); + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java new file mode 100644 index 00000000000..eaba07e0543 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java @@ -0,0 +1,46 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.MethodDescriptor.MethodType; + +/** + * An annotation to mark a method as representing a + * unary gRPC method. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@GrpcMethod(type = MethodType.UNARY) +@Documented +@Inherited +public @interface Unary { + /** + * Obtain the name of the method. + *

+ * If not set the name of the actual annotated method is used. + * + * @return name of the method + */ + String name() default ""; +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java new file mode 100644 index 00000000000..ea1a46536b5 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java @@ -0,0 +1,873 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +import io.helidon.grpc.core.GrpcHelper; +import io.helidon.grpc.core.MethodHandler; + +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for unary gRPC methods. + */ +public class UnaryMethodHandlerSupplier + extends AbstractMethodHandlerSupplier { + + /** + * Create a supplier of handlers for server streaming methods. + */ + // method is public because it is loaded via ServiceLoader + public UnaryMethodHandlerSupplier() { + super(MethodDescriptor.MethodType.UNARY); + } + + @Override + public boolean supplies(AnnotatedMethod method) { + return super.supplies(method) && determineCallType(method) != CallType.unknown; + } + + @Override + public MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance) { + if (!isRequiredMethodType(method)) { + throw new IllegalArgumentException("Method not annotated as a unary method: " + method); + } + + CallType type = determineCallType(method); + MethodHandler handler; + + switch (type) { + case requestResponse: + handler = new RequestResponse<>(methodName, method, instance); + break; + case responseOnly: + handler = new ResponseOnly<>(methodName, method, instance); + break; + case requestNoResponse: + handler = new RequestNoResponse<>(methodName, method, instance); + break; + case noRequestNoResponse: + handler = new NoRequestNoResponse<>(methodName, method, instance); + break; + case futureResponse: + handler = new FutureResponse<>(methodName, method, instance); + break; + case futureResponseNoRequest: + handler = new FutureResponseNoRequest<>(methodName, method, instance); + break; + case unary: + handler = new Unary<>(methodName, method, instance); + break; + case unaryRequest: + handler = new UnaryNoRequest<>(methodName, method, instance); + break; + case unaryFuture: + handler = new UnaryFuture<>(methodName, method, instance); + break; + case unaryFutureNoRequest: + handler = new UnaryFutureNoRequest<>(methodName, method, instance); + break; + case unknown: + default: + throw new IllegalArgumentException("Not a supported unary method signature: " + method); + } + return handler; + } + + /** + * Determine the type of unary method by analyzing the method signature. + * + * @param method the method to analyze + * @return the {@link CallType} of the method + */ + private CallType determineCallType(AnnotatedMethod method) { + Type[] parameterTypes = method.parameterTypes(); + int paramCount = parameterTypes.length; + Type returnType = method.returnType(); + boolean voidReturn = void.class.equals(returnType); + CallType callType; + + if (paramCount == 2) { + if (StreamObserver.class.equals(parameterTypes[1]) && voidReturn) { + // Assume that the first parameter is the request value + // Signature is void invoke(ReqT, StreamObserver) + callType = CallType.unary; + } else if (CompletableFuture.class.equals(parameterTypes[1]) && voidReturn) { + // Assume that the first parameter is the request value + // Signature is void invoke(ReqT, CompletableFuture) + callType = CallType.unaryFuture; + } else { + // Signature is unsupported - invoke(, ) + callType = CallType.unknown; + } + } else if (paramCount == 1) { + if (voidReturn) { + if (StreamObserver.class.equals(parameterTypes[0])) { + // The single parameter is a StreamObserver so assume it is for the response + // Signature is void invoke(StreamObserver) + callType = CallType.unaryRequest; + } else if (CompletableFuture.class.equals(parameterTypes[0])) { + // The single parameter is a CompletableFuture so assume it is for the response + // Signature is void invoke(CompletableFuture) + callType = CallType.unaryFutureNoRequest; + } else { + // Assume that the single parameter is the request value and there is no response + // Signature is void invoke(ReqT) + callType = CallType.requestNoResponse; + } + } else { + if (CompletableFuture.class.equals(returnType)) { + // Assume that the single parameter is the request value and the response is a CompletableFuture + // Signature is CompletableFuture invoke(ReqT) + callType = CallType.futureResponse; + } else if (CompletionStage.class.equals(returnType)) { + // Assume that the single parameter is the request value and the response is a CompletionStage + // Signature is CompletionStage invoke(ReqT) + callType = CallType.futureResponse; + } else { + // Assume that the single parameter is the request value + // and that the return is the response value + // Signature is ResT invoke(ReqT) + callType = CallType.requestResponse; + } + } + } else if (paramCount == 0) { + if (CompletableFuture.class.equals(returnType)) { + // There is no request parameter the response is a CompletableFuture + // Signature is CompletableFuture invoke() + callType = CallType.futureResponseNoRequest; + } else if (CompletionStage.class.equals(returnType)) { + // There is no request parameter the response is a CompletionStage + // Signature is CompletionStage invoke() + callType = CallType.futureResponseNoRequest; + } else if (voidReturn) { + // There is no request parameter and no response + // Signature is void invoke() + callType = CallType.noRequestNoResponse; + } else { + // There is no request parameter only a response + // Signature is ResT invoke() + callType = CallType.responseOnly; + } + } else { + // Signature is unsupported - it has more than two parameters + callType = CallType.unknown; + } + + return callType; + } + + // ----- CallType enumeration ------------------------------------------- + + /** + * An enumeration representing different supported types + * of unary method signatures. + */ + private enum CallType { + /** + * A unary call with a request and response. + *

+         *     RestT invoke(ReqT request)
+         * 
+ */ + requestResponse, + /** + * A unary call with no request only a response. + *
+         *     RestT invoke()
+         * 
+ */ + responseOnly, + /** + * A unary call with a request but no response. + *
+         *     void invoke(ReqT request)
+         * 
+ */ + requestNoResponse, + /** + * A unary call with no request and no response. + *
+         *     void invoke()
+         * 
+ */ + noRequestNoResponse, + /** + * An unary call with a {@link CompletionStage} response. + *
+         *     CompletionStage<ResT> invoke(ReqT request)
+         * 
+ */ + futureResponse, + /** + * An unary call with no request and a {@link CompletionStage} response. + *
+         *     CompletionStage<ResT> invoke()
+         * 
+ */ + futureResponseNoRequest, + /** + * An standard unary call. + *
+         *     void invoke(ReqT request, StreamObserver<RespT> observer)
+         * 
+ */ + unary, + /** + * An standard unary call with no request. + *
+         *     void invoke(StreamObserver<RespT> observer)
+         * 
+ */ + unaryRequest, + /** + * An standard unary call with a {@link CompletableFuture} in place of + * a {@link StreamObserver}. + *
+         *     void invoke(ReqT request, CompletableFuture<RespT> observer)
+         * 
+ */ + unaryFuture, + /** + * An standard unary call without an request and with a {@link CompletableFuture} + * in place of a {@link StreamObserver}. + *
+         *     void invoke(CompletableFuture<RespT> observer)
+         * 
+ */ + unaryFutureNoRequest, + /** + * A call type not recognised by this supplier. + */ + unknown + } + + // ----- call handler inner classes ------------------------------------- + + /** + * A base class for unary method handlers. + * @param the request type + * @param the response type + */ + public abstract static class AbstractUnaryHandler + extends AbstractHandler { + + /** + * The argument to use for a {@code null} request parameter. + */ + static final Empty EMPTY = Empty.getDefaultInstance(); + + AbstractUnaryHandler(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance, MethodDescriptor.MethodType.UNARY); + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + Object invokeUnary(Object request, UnaryClient client) { + try { + return invokeUnaryAsync(request, client) + .toCompletableFuture() + .get(); + } catch (Throwable thrown) { + throw GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL); + } + } + + CompletionStage invokeUnaryAsync(Object request, UnaryClient client) { + try { + return client.unary(methodName(), request); + } catch (Throwable thrown) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL)); + return future; + } + } + + void invokeUnary(Object request, StreamObserver observer, UnaryClient client) { + try { + invokeUnaryAsync(request, client) + .handle((response, error) -> { + if (error == null) { + observer.onNext(response); + observer.onCompleted(); + } else { + observer.onError(error); + } + return null; + }); + } catch (Throwable thrown) { + observer.onError(GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL)); + } + } + + void invokeUnaryAsync(Object request, CompletableFuture future, UnaryClient client) { + try { + invokeUnaryAsync(request, client) + .handle((response, error) -> { + if (error == null) { + future.complete(response); + } else { + future.completeExceptionally(error); + } + return null; + }); + } catch (Throwable thrown) { + future.completeExceptionally(GrpcHelper.ensureStatusRuntimeException(thrown, Status.INTERNAL)); + } + } + } + + // ----- RequestResponse call handler ----------------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     RestT invoke(ReqT request)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class RequestResponse + extends AbstractUnaryHandler { + + RequestResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(method.returnType()); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + RespT response = (RespT) method.invoke(instance, request); + observer.onNext(response); + observer.onCompleted(); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     RestT invoke(ReqT request);
+         * 
+ * so the request is in {@code args[0]}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the request response + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + return invokeUnary(args[0], client); + } + } + + // ----- ResponseOnly call handler -------------------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     RestT invoke()
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class ResponseOnly + extends AbstractUnaryHandler { + + ResponseOnly(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(method.returnType()); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + RespT response = (RespT) method.invoke(instance); + observer.onNext(response); + observer.onCompleted(); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     RestT invoke();
+         * 
+ * so there is no request parameter. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the request response + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + // no request parameter, we cannot send null so we send Types.Empty + return invokeUnary(EMPTY, client); + } + } + + // ----- RequestNoResponse call handler --------------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     void invoke(ReqT request)
+     * 
+ *

+ * Because the underlying handler returns {@code void} the {@link StreamObserver#onNext(Object)} + * method will not be called. + * + * @param the request type + * @param the response type + */ + public static class RequestNoResponse + extends AbstractUnaryHandler { + + RequestNoResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance, request); + observer.onNext((RespT) EMPTY); + observer.onCompleted(); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     RestT invoke(ReqT request);
+         * 
+ * so the request is in {@code args[0]}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + invokeUnary(args[0], client); + return null; + } + } + + // ----- NoRequestNoResponse call handler ------------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     void invoke()
+     * 
+ *

+ * Because the underlying handler returns {@code void} the {@link StreamObserver#onNext(Object)} + * method will not be called. + * + * @param the request type + * @param the response type + */ + public static class NoRequestNoResponse + extends AbstractUnaryHandler { + + NoRequestNoResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance); + observer.onNext((RespT) EMPTY); + observer.onCompleted(); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     void invoke();
+         * 
+ * so there is no request parameter. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + invokeUnary(EMPTY, client); + return null; + } + } + + // ----- FutureResponse call handler ------------------------------------ + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     CompletableFuture<ResT> invoke(ReqT request)
+     * 
+ *

+ * If the future returned completes normally and has a none null none + * {@link Void} result then that result will be passed to the + * {@link StreamObserver#onNext(Object)} method. + * If the future completes exceptionally then the error will be passed to + * the {@link StreamObserver#onError(Throwable)} method. + * + * @param the request type + * @param the response type + */ + public static class FutureResponse + extends AbstractUnaryHandler { + + FutureResponse(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(getGenericResponseType(method.genericReturnType())); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + CompletableFuture future = (CompletableFuture) method.invoke(instance, request); + future.handle((response, thrown) -> handleFuture(response, thrown, observer)); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     CompletableFuture<ResT> invoke(ReqT request)
+         * 
+ * so the request parameter is in {@code args[0]}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + return invokeUnaryAsync(args[0], client); + } + } + + // ----- FutureResponseNoRequest call handler --------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     CompletableFuture<ResT> invoke()
+     * 
+ *

+ * If the future returned completes normally and has a none null none + * {@link Void} result then that result will be passed to the + * {@link StreamObserver#onNext(Object)} method. + * If the future completes exceptionally then the error will be passed to + * the {@link StreamObserver#onError(Throwable)} method. + * + * @param the request type + * @param the response type + */ + public static class FutureResponseNoRequest + extends AbstractUnaryHandler { + + FutureResponseNoRequest(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(getGenericResponseType(method.genericReturnType())); + } + + @Override + @SuppressWarnings("unchecked") + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + CompletableFuture future = (CompletableFuture) method.invoke(instance); + future.handle((response, thrown) -> handleFuture(response, thrown, observer)); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     CompletableFuture<ResT> invoke()
+         * 
+ * so there is no request parameter. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + public Object unary(Object[] args, UnaryClient client) { + return invokeUnaryAsync(EMPTY, client); + } + } + + // ----- Unary call handler --------------------------------------------- + + /** + * A unary {@link MethodHandler} that calls a standard unary method handler + * method of the form. + *
+     *     void invoke(ReqT request, StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class Unary + extends AbstractUnaryHandler { + + Unary(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(getGenericResponseType(method.genericParameterTypes()[1])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance, request, observer); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     void invoke(ReqT request, StreamObserver<RespT> observer)
+         * 
+ * so the request parameter is in {@code args[0]} and the {@link StreamObserver} + * to receive the response is in {@code args[1}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + @SuppressWarnings("unchecked") + public Object unary(Object[] args, UnaryClient client) { + invokeUnary(args[0], (StreamObserver) args[1], client); + return null; + } + } + + // ----- UnaryNoRequest call handler ------------------------------------ + + /** + * A unary {@link MethodHandler} that calls a unary method handler method + * of the form. + *
+     *     void invoke(StreamObserver<RespT> observer)
+     * 
+ * + * @param the request type + * @param the response type + */ + public static class UnaryNoRequest + extends AbstractUnaryHandler { + + UnaryNoRequest(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + method.invoke(instance, observer); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     void invoke(StreamObserver<RespT> observer)
+         * 
+ * so there is no request parameter and the {@link StreamObserver} + * to receive the response is in {@code args[0}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + @SuppressWarnings("unchecked") + public Object unary(Object[] args, UnaryClient client) { + invokeUnary(EMPTY, (StreamObserver) args[0], client); + return null; + } + } + + // ----- UnaryFuture call handler --------------------------------------- + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     void invoke(ReqT request, CompletableFuture<RespT> future)
+     * 
+ *

+ * If the future completes normally and has a none null none {@link Void} + * result then that result will be passed to the + * {@link StreamObserver#onNext(Object)} method. + * If the future completes exceptionally then the error will be passed to + * the {@link StreamObserver#onError(Throwable)} method. + * + * @param the request type + * @param the response type + */ + public static class UnaryFuture + extends AbstractUnaryHandler { + + UnaryFuture(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setRequestType(method.parameterTypes()[0]); + setResponseType(getGenericResponseType(method.genericParameterTypes()[1])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + CompletableFuture future = new CompletableFuture<>(); + future.handleAsync((response, thrown) -> handleFuture(response, thrown, observer)); + method.invoke(instance, request, future); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     void invoke(ReqT request, CompletableFuture<RespT> future)
+         * 
+ * so the request parameter is in {@code args[0]} and the {@link CompletableFuture} + * to receive the response is in {@code args[1}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + @SuppressWarnings("unchecked") + public Object unary(Object[] args, UnaryClient client) { + invokeUnaryAsync(args[0], (CompletableFuture) args[1], client); + return null; + } + } + + // ----- UnaryFutureNoRequest call handler ------------------------------ + + /** + * A unary {@link MethodHandler} that calls a handler method of the form. + *
+     *     void invoke(CompletableFuture<RespT> future)
+     * 
+ *

+ * If the future completes normally and has a none null none {@link Void} + * result then that result will be passed to the + * {@link StreamObserver#onNext(Object)} method. + * If the future completes exceptionally then the error will be passed to + * the {@link StreamObserver#onError(Throwable)} method. + * + * @param the request type + * @param the response type + */ + public static class UnaryFutureNoRequest + extends AbstractUnaryHandler { + + UnaryFutureNoRequest(String methodName, AnnotatedMethod method, Supplier instance) { + super(methodName, method, instance); + setResponseType(getGenericResponseType(method.genericParameterTypes()[0])); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + CompletableFuture future = new CompletableFuture<>(); + future.handleAsync((response, thrown) -> handleFuture(response, thrown, observer)); + method.invoke(instance, future); + } + + /** + * Invoke the client call. + *

+ * The call is from a method signature: + *

+         *     void invoke(CompletableFuture<RespT> future)
+         * 
+ * so there is no request parameter and the {@link CompletableFuture} + * to receive the response is in {@code args[1}. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to + * + * @return the method signature return is {@code void} so this method + * always returns null + */ + @Override + @SuppressWarnings("unchecked") + public Object unary(Object[] args, UnaryClient client) { + invokeUnaryAsync(EMPTY, (CompletableFuture) args[0], client); + return null; + } + } +} diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/package-info.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/package-info.java new file mode 100644 index 00000000000..3687f1005fe --- /dev/null +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Core gRPC microprofile classes. + */ +package io.helidon.microprofile.grpc.core; diff --git a/microprofile/grpc/core/src/main/java/module-info.java b/microprofile/grpc/core/src/main/java/module-info.java new file mode 100644 index 00000000000..b6a32f9cc81 --- /dev/null +++ b/microprofile/grpc/core/src/main/java/module-info.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/** + * gRPC microprofile core module + */ +module io.helidon.microprofile.grpc.core { + exports io.helidon.microprofile.grpc.core; + + requires io.helidon.common; + requires transitive io.helidon.grpc.core; + requires transitive io.helidon.webclient.grpc; + requires transitive io.helidon.microprofile.config; + + requires transitive jakarta.cdi; + + requires java.logging; + requires jakarta.inject; + + uses io.helidon.microprofile.grpc.core.MethodHandlerSupplier; + uses io.helidon.grpc.core.MarshallerSupplier; + + provides io.helidon.microprofile.grpc.core.MethodHandlerSupplier + with io.helidon.microprofile.grpc.core.BidirectionalMethodHandlerSupplier, + io.helidon.microprofile.grpc.core.ClientStreamingMethodHandlerSupplier, + io.helidon.microprofile.grpc.core.ServerStreamingMethodHandlerSupplier, + io.helidon.microprofile.grpc.core.UnaryMethodHandlerSupplier; + + provides jakarta.enterprise.inject.spi.Extension + with io.helidon.microprofile.grpc.core.GrpcCdiExtension; +} \ No newline at end of file diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplierTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplierTest.java new file mode 100644 index 00000000000..e893e457330 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplierTest.java @@ -0,0 +1,225 @@ +/* + * 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.core; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@SuppressWarnings("unchecked") +public class AbstractMethodHandlerSupplierTest { + + @Test + public void shouldHandleBidirectionalInvocationError() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getClientStreamingMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + BadHandlerStub stub = new BadHandlerStub<>(method, + () -> instance, + MethodDescriptor.MethodType.CLIENT_STREAMING); + + StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> stub.invoke(observer)); + + Mockito.verifyNoMoreInteractions(observer); + assertThat(exception, is(notNullValue())); + MatcherAssert.assertThat(exception.getStatus().getCode(), CoreMatchers.is(Status.INTERNAL.getCode())); + } + + @Test + public void shouldHandleClientStreamingInvocationError() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getClientStreamingMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + BadHandlerStub stub = new BadHandlerStub<>(method, + () -> instance, + MethodDescriptor.MethodType.CLIENT_STREAMING); + + StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> stub.invoke(observer)); + + Mockito.verifyNoMoreInteractions(observer); + assertThat(exception, is(notNullValue())); + MatcherAssert.assertThat(exception.getStatus().getCode(), CoreMatchers.is(Status.INTERNAL.getCode())); + } + + @Test + public void shouldHandleServerStreamingInvocationError() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getServerStreamingMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + BadHandlerStub stub = new BadHandlerStub<>(method, + () -> instance, + MethodDescriptor.MethodType.SERVER_STREAMING); + + stub.invoke("foo", observer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StatusException.class); + verify(observer).onError(captor.capture()); + Mockito.verifyNoMoreInteractions(observer); + StatusException exception = captor.getValue(); + assertThat(exception, is(notNullValue())); + MatcherAssert.assertThat(exception.getStatus().getCode(), CoreMatchers.is(Status.INTERNAL.getCode())); + } + + @Test + public void shouldHandleUnaryInvocationError() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getUnaryMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + BadHandlerStub stub = new BadHandlerStub<>(method, () -> instance, MethodDescriptor.MethodType.UNARY); + + stub.invoke("foo", observer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StatusException.class); + verify(observer).onError(captor.capture()); + Mockito.verifyNoMoreInteractions(observer); + StatusException exception = captor.getValue(); + assertThat(exception, is(notNullValue())); + MatcherAssert.assertThat(exception.getStatus().getCode(), CoreMatchers.is(Status.INTERNAL.getCode())); + } + + @Test + public void shouldHandleFutureCompletion() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getUnaryMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + HandlerStub stub = new HandlerStub<>(method, () -> instance, MethodDescriptor.MethodType.UNARY); + + stub.handleFuture("foo", null, observer); + + InOrder inOrder = Mockito.inOrder(observer); + inOrder.verify(observer).onNext("foo"); + inOrder.verify(observer).onCompleted(); + } + + @Test + public void shouldHandleFutureExceptionalCompletion() throws Exception { + AnnotatedMethod method = AnnotatedMethod.create(getUnaryMethod()); + AbstractMethodHandlerSupplierTest instance = mock(AbstractMethodHandlerSupplierTest.class); + StreamObserver observer = mock(StreamObserver.class); + HandlerStub stub = new HandlerStub<>(method, () -> instance, MethodDescriptor.MethodType.UNARY); + + stub.handleFuture(null, new RuntimeException(), observer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StatusException.class); + verify(observer).onError(captor.capture()); + + StatusException exception = captor.getValue(); + assertThat(exception, is(notNullValue())); + MatcherAssert.assertThat(exception.getStatus().getCode(), CoreMatchers.is(Status.INTERNAL.getCode())); + } + + + private Method getBidiMethod() throws Exception { + return getClass().getMethod("bidi", StreamObserver.class); + } + + @Bidirectional + public StreamObserver bidi(StreamObserver observer) { + return null; + } + + private Method getClientStreamingMethod() throws Exception { + return getClass().getMethod("clientStreaming", StreamObserver.class); + } + + @ClientStreaming + public StreamObserver clientStreaming(StreamObserver observer) { + return null; + } + + private Method getServerStreamingMethod() throws Exception { + return getClass().getMethod("serverStreaming", String.class, StreamObserver.class); + } + + @ServerStreaming + public void serverStreaming(String request, StreamObserver observer) { + } + + private Method getUnaryMethod() throws Exception { + return getClass().getMethod("unary", String.class, StreamObserver.class); + } + + @Unary + public void unary(String request, StreamObserver observer) { + } + + + /** + * A stub method handler. + */ + public static class HandlerStub + extends AbstractMethodHandlerSupplier.AbstractHandler { + + public HandlerStub(AnnotatedMethod method, Supplier instance, MethodDescriptor.MethodType methodType) { + super("foo", method, instance, methodType); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + return null; + } + } + + + /** + * A stub method handler. + */ + public static class BadHandlerStub + extends AbstractMethodHandlerSupplier.AbstractHandler { + + public BadHandlerStub(AnnotatedMethod method, Supplier instance, MethodDescriptor.MethodType methodType) { + super("foo", method, instance, methodType); + } + + @Override + protected void invoke(Method method, Object instance, ReqT request, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + throw new RuntimeException(); + } + + @Override + protected StreamObserver invoke(Method method, Object instance, StreamObserver observer) + throws InvocationTargetException, IllegalAccessException { + throw new RuntimeException(); + } + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodListTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodListTest.java new file mode 100644 index 00000000000..53a6b8c328a --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodListTest.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.core; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; + +public class AnnotatedMethodListTest { + + @Test + public void shouldFindAllDeclaredMethods() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + List names = list.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("one", "two", "three", "four", "five")); + } + + @Test + public void shouldFindAllMethods() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class); + List names = list.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("one", "two")); + } + + @Test + public void shouldIterateAllMethods() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + List names = new ArrayList<>(); + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + AnnotatedMethod method = iterator.next(); + names.add(method.method().getName()); + } + assertThat(names, containsInAnyOrder("one", "two", "three", "four", "five")); + } + + @Test + public void shouldGetNonePublicMethods() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.isNotPublic(); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("three", "four", "five")); + } + + @Test + public void shouldGetMethodsWithParameterCount() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.hasParameterCount(2); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("two", "four")); + } + + @Test + public void shouldGetMethodsWithWithReturnType() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.hasReturnType(String.class); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("one", "four")); + } + + @Test + public void shouldGetMethodsWithWithNamePrefix() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.nameStartsWith("t"); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("two", "three")); + } + + @Test + public void shouldGetMethodsWithWithAnnotation() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.withAnnotation(Unary.class); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("one", "three")); + } + + @Test + public void shouldGetMethodsWithoutAnnotation() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.withoutAnnotation(Unary.class); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("two", "four", "five")); + } + + @Test + public void shouldGetMethodsWithMetaAnnotation() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.withMetaAnnotation(GrpcMethod.class); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("one", "two", "three", "five")); + } + + @Test + public void shouldGetMethodsWithoutMetaAnnotation() { + AnnotatedMethodList list = AnnotatedMethodList.create(Stub.class, true); + AnnotatedMethodList methods = list.withoutMetaAnnotation(GrpcMethod.class); + List names = methods.stream() + .map(am -> am.method().getName()) + .collect(Collectors.toList()); + + assertThat(names, containsInAnyOrder("four")); + } + + /** + * A stub class to test annotation processing. + */ + public class Stub { + @Unary + public String one(int a) { + return ""; + } + + @ClientStreaming + public Long two(int a, int b) { + return 0L; + } + + @Unary + private void three(int a) { + } + + @RequestType(String.class) + protected String four(int a, int b) { + return ""; + } + + @ClientStreaming + void five() { + } + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodTest.java new file mode 100644 index 00000000000..15f7e336e91 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/AnnotatedMethodTest.java @@ -0,0 +1,286 @@ +/* + * 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.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import io.grpc.MethodDescriptor; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AnnotatedMethodTest { + + @Test + public void shouldNotAllowNullMethod() { + assertThrows(NullPointerException.class, () -> AnnotatedMethod.create(null)); + } + + @Test + public void shouldHaveSameDeclaredAndActualAnnotatedMethodsIfNoSuperClass() throws Exception { + Method method = GrandParent.class.getDeclaredMethod("one"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(method); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(method))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveSameDeclaredAndActualNonAnnotatedMethodsIfNoSuperClass() throws Exception { + Method method = GrandParent.class.getDeclaredMethod("two"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(method); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(method))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromSuperClass() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("one"); + Method method = GrandParent.class.getDeclaredMethod("one"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveNonAnnotatedMethodOverridingSuperClass() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("two"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(declaredMethod)); + } + + @Test + public void shouldHaveAnnotatedMethodOverridingAnnotatedMethodInSuperClass() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("three"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(declaredMethod)); + } + + @Test + public void shouldHaveAnnotatedMethodNotOverridingMethodInSuperClass() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("four"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(declaredMethod)); + } + + @Test + public void shouldHaveNonAnnotatedMethodNotOverridingMethodInSuperClass() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("five"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(declaredMethod)); + } + + @Test + public void shouldHaveAnnotatedMethodFromGrandParent() throws Exception { + Method declaredMethod = Child.class.getDeclaredMethod("one"); + Method method = GrandParent.class.getDeclaredMethod("one"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromParentNotFromInterface() throws Exception { + Method declaredMethod = Child.class.getDeclaredMethod("three"); + Method method = Parent.class.getDeclaredMethod("three"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromInterface() throws Exception { + Method declaredMethod = Child.class.getDeclaredMethod("six"); + Method method = InterfaceOne.class.getDeclaredMethod("six"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromFirstDeclaredInterface() throws Exception { + Method declaredMethod = Multi.class.getDeclaredMethod("three"); + Method method = InterfaceOne.class.getDeclaredMethod("three"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromSuperInterface() throws Exception { + Method declaredMethod = Service.class.getDeclaredMethod("three"); + Method method = InterfaceOne.class.getDeclaredMethod("three"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldHaveAnnotatedMethodFromParentsInterface() throws Exception { + Method declaredMethod = Child.class.getDeclaredMethod("seven"); + Method method = InterfaceFour.class.getDeclaredMethod("seven"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + + assertThat(annotatedMethod.declaredMethod(), is(sameInstance(declaredMethod))); + assertThat(annotatedMethod.method(), is(method)); + } + + @Test + public void shouldMergeAnnotations() throws Exception { + Method declaredMethod = Parent.class.getDeclaredMethod("one"); + AnnotatedMethod annotatedMethod = AnnotatedMethod.create(declaredMethod); + Annotation[] annotations = annotatedMethod.getAnnotations(); + + assertThat(annotations.length, is(3)); + assertThat(annotatedMethod.getAnnotation(Inject.class), is(notNullValue())); + assertThat(annotatedMethod.getAnnotation(Named.class), is(notNullValue())); + assertThat(annotatedMethod.getAnnotation(GrpcMethod.class), is(notNullValue())); + } + + @Grpc + public static class GrandParent { + + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + @Inject + public void one() { + } + + public void two() { + } + + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + public void three() { + } + } + + public static class Parent + extends GrandParent + implements InterfaceFour { + + @Override + @Named("bar") + public void one() { + } + + @Override + public void two() { + } + + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + @Override + public void three() { + } + + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + public void four() { + } + + public void five() { + } + + @Override + public void seven() { + } + } + + public static class Child + extends Parent + implements InterfaceOne { + @Override + public void one() { + } + + @Override + public void three() { + } + + public void six() { + } + + @Override + public void seven() { + } + } + + public interface InterfaceOne { + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + void three(); + + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + void six(); + } + + public interface InterfaceTwo { + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + void three(); + } + + public interface InterfaceThree + extends InterfaceOne { + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + void six(); + } + + public interface InterfaceFour { + @GrpcMethod(type = MethodDescriptor.MethodType.UNARY) + void seven(); + } + + public static class Multi + implements InterfaceOne, InterfaceTwo { + @Override + public void three() { + } + + @Override + public void six() { + } + } + + public static class Service + implements InterfaceThree { + @Override + public void three() { + } + + @Override + public void six() { + } + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplierTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplierTest.java new file mode 100644 index 00000000000..ba6476e36d8 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplierTest.java @@ -0,0 +1,223 @@ +/* + * 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.core; + +import io.helidon.grpc.core.MethodHandler; + +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class BidirectionalMethodHandlerSupplierTest { + + @Test + public void shouldSupplyBidirectionalMethods() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + assertThat(supplier.supplies(method), is(true)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyBidiHandler() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.bidi(any(StreamObserver.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.BIDI_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + StreamObserver result = handler.invoke(observer); + assertThat(result, is(sameInstance(responseObserver))); + verify(service).bidi(any(StreamObserver.class)); + } + + @Test + public void shouldHandleClientCall() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.bidi(any(StreamObserver.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.BidirectionalClient client = mock(MethodHandler.BidirectionalClient.class); + + when(client.bidiStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.bidirectional(new Object[]{observer}, client); + + assertThat(result, is(sameInstance(response))); + verify(client).bidiStreaming(eq("foo"), same(observer)); + } + + @Test + public void shouldSupplyBidiHandlerWithTypesFromAnnotation() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("bidiReqResp", StreamObserver.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.BIDI_STREAMING)); + } + + @Test + public void shouldNotSupplyNullMethod() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + assertThat(supplier.supplies(null), is(false)); + } + + @Test + public void shouldThrowExceptionSupplingNullMethod() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", null, () -> service)); + } + + @Test + public void shouldNotSupplyNoneBidiHandler() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyBidiAnnotatedMethodWithWrongArgType() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyBidiAnnotatedMethodWithTooManyArgs() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("tooManyArgs", StreamObserver.class, String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyClientStreamingMethods() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyServerStreamingMethods() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyUnaryMethods() { + BidirectionalMethodHandlerSupplier supplier = new BidirectionalMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + // ----- helper methods ------------------------------------------------- + + private AnnotatedMethod getBidiMethod() { + return getMethod("bidi", StreamObserver.class); + } + + private AnnotatedMethod getUnaryMethod() { + return getMethod("unary", String.class, StreamObserver.class); + } + + private AnnotatedMethod getServerStreamingMethod() { + return getMethod("serverStreaming", String.class, StreamObserver.class); + } + + private AnnotatedMethod getClientStreamingMethod() { + return getMethod("clientStreaming", StreamObserver.class); + } + + private AnnotatedMethod getMethod(String name, Class... args) { + try { + return AnnotatedMethod.create(Service.class.getMethod(name, args)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + /** + * The test service with bi-directional streaming methods. + */ + public interface Service { + @Bidirectional + StreamObserver bidi(StreamObserver observer); + + @Bidirectional + @RequestType(Long.class) + @ResponseType(String.class) + StreamObserver bidiReqResp(StreamObserver observer); + + @Bidirectional + StreamObserver badArg(String bad); + + @Bidirectional + StreamObserver tooManyArgs(StreamObserver observer, String bad); + + @Unary + void unary(String request, StreamObserver observer); + + @ServerStreaming + void serverStreaming(String request, StreamObserver observer); + + @ClientStreaming + StreamObserver clientStreaming(StreamObserver request); + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplierTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplierTest.java new file mode 100644 index 00000000000..318a2c13b91 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplierTest.java @@ -0,0 +1,462 @@ +/* + * 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.core; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; + +import io.helidon.grpc.core.MethodHandler; + +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class ClientStreamingMethodHandlerSupplierTest { + + @Test + public void shouldSupplyClientStreamingMethods() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + assertThat(supplier.supplies(method), is(true)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyClientStreamingHandler() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.clientStreaming(any(StreamObserver.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.CLIENT_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + StreamObserver result = handler.invoke(observer); + assertThat(result, is(sameInstance(responseObserver))); + verify(service).clientStreaming(any(StreamObserver.class)); + } + + @Test + public void shouldHandleClientCall() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.clientStreaming(any(StreamObserver.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{observer}, client); + + assertThat(result, is(sameInstance(response))); + verify(client).clientStreaming(eq("foo"), same(observer)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyClientStreamingHandlerForMethodTakingFuture() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("future", CompletableFuture.class); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.future(any(CompletableFuture.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.CLIENT_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + StreamObserver result = handler.invoke(observer); + assertThat(result, is(sameInstance(responseObserver))); + verify(service).future(any(CompletableFuture.class)); + } + + @Test + public void shouldHandleClientCallForMethodTakingFuture() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("future", CompletableFuture.class); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.future(any(CompletableFuture.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + CompletableFuture future = new CompletableFuture<>(); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{future}, client); + + assertThat(result, is(sameInstance(response))); + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + observer.onNext("bar"); + observer.onCompleted(); + + assertThat(future.isDone(), is(true)); + assertThat(future.get(), is("bar")); + } + + @Test + public void shouldHandleClientCallForMethodTakingFutureAndHandleError() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("future", CompletableFuture.class); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.future(any(CompletableFuture.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + CompletableFuture future = new CompletableFuture<>(); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{future}, client); + + assertThat(result, is(sameInstance(response))); + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + RuntimeException exception = new RuntimeException("Oops..."); + observer.onError(exception); + + assertThat(future.isCompletedExceptionally(), is(true)); + } + + @Test + public void shouldHandleClientCallForIterable() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("iterable", Iterable.class); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.future(any(CompletableFuture.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + List list = Arrays.asList("A", "B", "C"); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{list}, client); + + assertThat(result, is(instanceOf(CompletableFuture.class))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + observer.onNext("bar"); + observer.onCompleted(); + + CompletableFuture future = (CompletableFuture) result; + assertThat(future.isDone(), is(true)); + assertThat(future.get(), is("bar")); + } + + @Test + public void shouldHandleClientCallForStream() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("stream", Stream.class); + StreamObserver responseObserver = mock(StreamObserver.class); + Service service = mock(Service.class); + + when(service.future(any(CompletableFuture.class))).thenReturn(responseObserver); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + Stream stream = Stream.of("A", "B", "C"); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{stream}, client); + + assertThat(result, is(instanceOf(CompletableFuture.class))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + observer.onNext("bar"); + observer.onCompleted(); + + CompletableFuture future = (CompletableFuture) result; + assertThat(future.isDone(), is(true)); + assertThat(future.get(), is("bar")); + } + + @Test + public void shouldHandleClientCallForIterableReturningCompletionStage() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("iterableCompletionStage", Iterable.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + List list = Arrays.asList("A", "B", "C"); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{list}, client); + + assertThat(result, is(instanceOf(CompletionStage.class))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + observer.onNext("bar"); + observer.onCompleted(); + + CompletionStage stage = (CompletionStage) result; + CompletableFuture future = stage.toCompletableFuture(); + assertThat(future.isDone(), is(true)); + assertThat(future.get(), is("bar")); + } + + @Test + public void shouldHandleClientCallForStreamReturningCompletionStage() throws Exception { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("streamCompletionStage", Stream.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + Stream stream = Stream.of("A", "B", "C"); + StreamObserver response = mock(StreamObserver.class); + MethodHandler.ClientStreaming client = mock(MethodHandler.ClientStreaming.class); + + when(client.clientStreaming(anyString(), any(StreamObserver.class))).thenReturn(response); + + Object result = handler.clientStreaming(new Object[]{stream}, client); + + assertThat(result, is(instanceOf(CompletionStage.class))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StreamObserver.class); + verify(client).clientStreaming(eq("foo"), captor.capture()); + + StreamObserver observer = captor.getValue(); + observer.onNext("bar"); + observer.onCompleted(); + + CompletionStage stage = (CompletionStage) result; + CompletableFuture future = stage.toCompletableFuture(); + assertThat(future.isDone(), is(true)); + assertThat(future.get(), is("bar")); + } + + @Test + public void shouldSupplyClientStreamingHandlerWithTypesFromAnnotation() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("reqResp", StreamObserver.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(Long.class.equals(handler.getRequestType()), is(true)); + assertThat(String.class.equals(handler.getResponseType()), is(true)); + } + + @Test + public void shouldNotSupplyNullMethod() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + assertThat(supplier.supplies(null), is(false)); + } + + @Test + public void shouldThrowExceptionSupplingNullMethod() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", null, () -> service)); + } + + @Test + public void shouldNotSupplyNoneClientStreamingHandler() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithWrongArgType() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithTooManyArgs() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("tooManyArgs", StreamObserver.class, String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyBidiStreamingMethods() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyServerStreamingMethods() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyUnaryMethods() { + ClientStreamingMethodHandlerSupplier supplier = new ClientStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + // ----- helper methods ------------------------------------------------- + + private AnnotatedMethod getBidiMethod() { + return getMethod("bidi", StreamObserver.class); + } + + private AnnotatedMethod getUnaryMethod() { + return getMethod("unary", String.class, StreamObserver.class); + } + + private AnnotatedMethod getServerStreamingMethod() { + return getMethod("serverStreaming", String.class, StreamObserver.class); + } + + private AnnotatedMethod getClientStreamingMethod() { + return getMethod("clientStreaming", StreamObserver.class); + } + + private AnnotatedMethod getMethod(String name, Class... args) { + try { + return AnnotatedMethod.create(Service.class.getMethod(name, args)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + /** + * The test service with bi-directional streaming methods. + */ + public interface Service { + @ClientStreaming + StreamObserver clientStreaming(StreamObserver observer); + + @ClientStreaming + StreamObserver future(CompletableFuture future); + + @ClientStreaming + CompletableFuture iterable(Iterable requests); + + @ClientStreaming + CompletableFuture stream(Stream requests); + + @ClientStreaming + CompletionStage iterableCompletionStage(Iterable requests); + + @ClientStreaming + CompletionStage streamCompletionStage(Stream requests); + + @ClientStreaming + @RequestType(Long.class) + @ResponseType(String.class) + StreamObserver reqResp(StreamObserver observer); + + @ClientStreaming + StreamObserver badArg(String bad); + + @ClientStreaming + StreamObserver tooManyArgs(StreamObserver observer, String bad); + + @Bidirectional + StreamObserver bidi(StreamObserver observer); + + @Unary + void unary(String request, StreamObserver observer); + + @ServerStreaming + void serverStreaming(String request, StreamObserver observer); + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/InstanceTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/InstanceTest.java new file mode 100644 index 00000000000..a449cec1e2d --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/InstanceTest.java @@ -0,0 +1,87 @@ +/* + * 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.core; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +public class InstanceTest { + + @Test + public void shouldCreateSingletonSupplier() { + Value singleton = new Value(); + Supplier supplier = Instance.singleton(singleton); + assertThat(supplier, is(notNullValue())); + assertThat(supplier.get(), is(sameInstance(singleton))); + } + + @Test + public void shouldSupplySameSingleton() { + Value singleton = new Value(); + Supplier supplier = Instance.singleton(singleton); + + assertThat(supplier.get(), is(sameInstance(singleton))); + assertThat(supplier.get(), is(sameInstance(singleton))); + assertThat(supplier.get(), is(sameInstance(singleton))); + } + + @Test + public void shouldCreateSingletonSupplierFromClass() { + Supplier supplier = Instance.singleton(Value.class); + assertThat(supplier, is(notNullValue())); + assertThat(supplier.get(), is(instanceOf(Value.class))); + } + + @Test + public void shouldSupplySameSingletonFromClass() { + Supplier supplier = Instance.singleton(Value.class); + Object value = supplier.get(); + + assertThat(value, is(instanceOf(Value.class))); + assertThat(supplier.get(), is(sameInstance(value))); + assertThat(supplier.get(), is(sameInstance(value))); + } + + @Test + public void shouldCreateNewInstanceSupplier() { + Supplier supplier = Instance.create(Value.class); + assertThat(supplier, is(notNullValue())); + assertThat(supplier.get(), is(instanceOf(Value.class))); + } + + @Test + public void shouldSupplyNewInstance() { + Supplier supplier = Instance.create(Value.class); + Object valueOne = supplier.get(); + Object valueTwo = supplier.get(); + + assertThat(valueOne, is(instanceOf(Value.class))); + assertThat(valueTwo, is(instanceOf(Value.class))); + assertThat(valueOne, is(not(sameInstance(valueTwo)))); + } + + public static class Value { + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/MethodHandlerTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/MethodHandlerTest.java new file mode 100644 index 00000000000..530c245603b --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/MethodHandlerTest.java @@ -0,0 +1,95 @@ +/* + * 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.core; + +import io.helidon.grpc.core.MethodHandler; + +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.stub.StreamObserver; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@SuppressWarnings("unchecked") +public class MethodHandlerTest { + + @Test + public void shouldNotSupportStreamingMethod() { + MethodHandler handler = new Stub(); + StreamObserver observer = mock(StreamObserver.class); + handler.invoke(observer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(observer).onError(captor.capture()); + Mockito.verifyNoMoreInteractions(observer); + + Throwable throwable = captor.getValue(); + assertThat(throwable.getClass().equals(StatusException.class), is(true)); + Status status = ((StatusException) throwable).getStatus(); + MatcherAssert.assertThat(status, CoreMatchers.is(Status.UNIMPLEMENTED)); + } + + + @Test + public void shouldNotSupportNonStreamingMethod() { + MethodHandler handler = new Stub(); + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(observer).onError(captor.capture()); + Mockito.verifyNoMoreInteractions(observer); + + Throwable throwable = captor.getValue(); + assertThat(throwable.getClass().equals(StatusException.class), is(true)); + Status status = ((StatusException) throwable).getStatus(); + MatcherAssert.assertThat(status, CoreMatchers.is(Status.UNIMPLEMENTED)); + } + + private class Stub + implements MethodHandler { + + @Override + public MethodDescriptor.MethodType type() { + return null; + } + + @Override + public Class getRequestType() { + return null; + } + + @Override + public Class getResponseType() { + return null; + } + + @Override + public String javaMethodName() { + return null; + } + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ModelHelperTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ModelHelperTest.java new file mode 100644 index 00000000000..2c342dc5268 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ModelHelperTest.java @@ -0,0 +1,119 @@ +/* + * 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.core; + +import java.util.AbstractMap; + +import io.helidon.grpc.core.MarshallerSupplier; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ModelHelperTest { + + @Test + public void shouldGetAnnotatedSuperClass() { + Class cls = ModelHelper.getAnnotatedResourceClass(ChildOne.class, Grpc.class); + assertThat(cls, equalTo(Parent.class)); + } + + @Test + public void shouldGetSelfIfAnnotated() { + Class cls = ModelHelper.getAnnotatedResourceClass(Parent.class, Grpc.class); + assertThat(cls, equalTo(Parent.class)); + } + + @Test + public void shouldGetSelfIfNothingAnnotated() { + Class cls = ModelHelper.getAnnotatedResourceClass(NoAnnotated.class, Grpc.class); + assertThat(cls, equalTo(NoAnnotated.class)); + } + + @Test + public void shouldGetAnnotatedSuperClassBeforeInterface() { + Class cls = ModelHelper.getAnnotatedResourceClass(ChildTwo.class, Grpc.class); + assertThat(cls, equalTo(Parent.class)); + } + + @Test + public void shouldGetAnnotatedInterface() { + Class cls = ModelHelper.getAnnotatedResourceClass(ChildThree.class, Grpc.class); + assertThat(cls, equalTo(IFaceOne.class)); + } + + // ----- helper methods ------------------------------------------------- + + private GrpcMarshaller getAnnotation(String method) throws Exception { + return ModelHelperTest.class.getMethod(method).getAnnotation(GrpcMarshaller.class); + } + + @GrpcMarshaller(MarshallerSupplier.PROTO) + public void protoMarshaller() { + } + + @GrpcMarshaller + public void implicitDefaultMarshaller() { + } + + @GrpcMarshaller(MarshallerSupplier.DEFAULT) + public void explicitDefaultMarshaller() { + } + + @Grpc + public static class GrandParent { + } + + @Grpc + public static class Parent + extends GrandParent { + } + + public static class ChildOne + extends Parent { + } + + @Grpc + public interface IFaceOne { + } + + public interface IFaceTwo + extends IFaceOne { + } + + public class ChildTwo + extends Parent + implements IFaceOne { + } + + public class ChildThree + implements IFaceOne { + } + + public class ChildFour + implements IFaceTwo { + } + + public class ChildFive + extends ChildFour { + } + + public abstract class NoAnnotated + extends AbstractMap { + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplierTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplierTest.java new file mode 100644 index 00000000000..375324dfc51 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplierTest.java @@ -0,0 +1,434 @@ +/* + * 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.core; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.grpc.core.MethodHandler; + +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ServerStreamingMethodHandlerSupplierTest { + + @Test + public void shouldSupplyServerStreamingMethods() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + assertThat(supplier.supplies(method), is(true)); + } + + /** + * Test handler for: + *
+     *     void invoke(ReqT request, StreamObserver observer);     
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyServerStreamingHandler() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.SERVER_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).serverStreaming(eq("foo"), any(StreamObserver.class)); + } + + /** + * Test handler for: + *
+     *     void invoke(ReqT request, StreamObserver observer);
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyServerStreamingHandlerForClientCall() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + MethodHandler.ServerStreamingClient client = mock(MethodHandler.ServerStreamingClient.class); + Object result = handler.serverStreaming(new Object[] {"bar", observer}, client); + + assertThat(result, is(nullValue())); + verify(client).serverStreaming(eq("foo"), eq("bar"), same(observer)); + } + + /** + * Test handler for: + *
+     *     void invoke(StreamObserver observer);
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithNoRequest() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("serverStreamingNoRequest", StreamObserver.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.SERVER_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).serverStreamingNoRequest(any(StreamObserver.class)); + } + + /** + * Test handler for: + *
+     *     void invoke(StreamObserver observer);
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithNoRequestForClientCall() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("serverStreamingNoRequest", StreamObserver.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + MethodHandler.ServerStreamingClient client = mock(MethodHandler.ServerStreamingClient.class); + Object result = handler.serverStreaming(new Object[] {observer}, client); + + assertThat(result, is(nullValue())); + verify(client).serverStreaming(eq("foo"), any(), same(observer)); + } + + /** + * Test handler for: + *
+     *     Stream invoke(ReqT request);
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithStreamResponse() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("streamResponse", String.class); + Stream stream = List.of(19L, 20L).stream(); + Service service = mock(Service.class); + + when(service.streamResponse(anyString())).thenReturn(stream); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.SERVER_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).streamResponse("foo"); + ArgumentCaptor captor = ArgumentCaptor.forClass(Long.class); + verify(observer, times(2)).onNext(captor.capture()); + assertThat(captor.getAllValues(), contains(19L, 20L)); + } + + /** + * Test handler for: + *
+     *     Stream invoke(ReqT request);
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithStreamResponseForClientCall() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("streamResponse", String.class); + Stream stream = List.of(19L, 20L).stream(); + Service service = mock(Service.class); + + when(service.streamResponse(anyString())).thenReturn(stream); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.ServerStreamingClient client = mock(MethodHandler.ServerStreamingClient.class); + + doAnswer(invocation -> { + StreamObserver o = invocation.getArgument(2); + o.onNext("One"); + o.onNext("Two"); + o.onCompleted(); + return null; + }).when(client).serverStreaming(anyString(), any(), any(StreamObserver.class)); + + Object result = handler.serverStreaming(new Object[] {"bar"}, client); + + assertThat(result, is(instanceOf(Stream.class))); + List list = (List) ((Stream) result).collect(Collectors.toList()); + assertThat(list, contains("One", "Two")); + verify(client).serverStreaming(eq("foo"), eq("bar"), any(StreamObserver.class)); + } + + /** + * Test handler for: + *
+     *     Stream invoke();
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithStreamResponseWithNoRequest() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("streamResponseNoRequest"); + Stream stream = List.of(19L, 20L).stream(); + Service service = mock(Service.class); + + when(service.streamResponseNoRequest()).thenReturn(stream); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.ServerStreamingClient client = mock(MethodHandler.ServerStreamingClient.class); + + doAnswer(invocation -> { + StreamObserver o = invocation.getArgument(2); + o.onNext("One"); + o.onNext("Two"); + o.onCompleted(); + return null; + }).when(client).serverStreaming(anyString(), any(), any(StreamObserver.class)); + + Object result = handler.serverStreaming(new Object[0], client); + + assertThat(result, is(instanceOf(Stream.class))); + List list = (List) ((Stream) result).collect(Collectors.toList()); + assertThat(list, contains("One", "Two")); + verify(client).serverStreaming(eq("foo"), any(), any(StreamObserver.class)); + } + + /** + * Test handler for: + *
+     *     Stream invoke();
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyHandlerForMethodWithStreamResponseWithNoRequestForClientCall() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("streamResponseNoRequest"); + Stream stream = List.of(19L, 20L).stream(); + Service service = mock(Service.class); + + when(service.streamResponseNoRequest()).thenReturn(stream); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.SERVER_STREAMING)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).streamResponseNoRequest(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Long.class); + verify(observer, times(2)).onNext(captor.capture()); + assertThat(captor.getAllValues(), contains(19L, 20L)); + } + + @Test + public void shouldSupplyHandlerWithTypesFromAnnotation() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("reqResp", Object.class, StreamObserver.class); + Service service = mock(Service.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.SERVER_STREAMING)); + } + + @Test + public void shouldNotSupplyNullMethod() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + assertThat(supplier.supplies(null), is(false)); + } + + @Test + public void shouldThrowExceptionSupplingNullMethod() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", null, () -> service)); + } + + @Test + public void shouldNotSupplyNoneUnaryHandler() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithInvalidSignature() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class, String.class); + + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotGetHandlerMethodAnnotatedMethodWithWrongArgType() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class, String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithTooManyArgs() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("tooManyArgs", String.class, StreamObserver.class, String.class); + Service service = mock(Service.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyBidiStreamingMethods() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyUnaryMethods() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyClientStreamingMethods() { + ServerStreamingMethodHandlerSupplier supplier = new ServerStreamingMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + // ----- helper methods ------------------------------------------------- + + private AnnotatedMethod getBidiMethod() { + return getMethod("bidi", StreamObserver.class); + } + + private AnnotatedMethod getUnaryMethod() { + return getMethod("unary", String.class, StreamObserver.class); + } + + private AnnotatedMethod getServerStreamingMethod() { + return getMethod("serverStreaming", String.class, StreamObserver.class); + } + + private AnnotatedMethod getClientStreamingMethod() { + return getMethod("clientStreaming", StreamObserver.class); + } + + private AnnotatedMethod getMethod(String name, Class... args) { + try { + return AnnotatedMethod.create(Service.class.getMethod(name, args)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + /** + * The unary methods service implementation. + */ + @Grpc + public interface Service { + @ServerStreaming + void serverStreaming(String request, StreamObserver observer); + + @ServerStreaming + void serverStreamingNoRequest(StreamObserver observer); + + @ServerStreaming + Stream streamResponse(String request); + + @ServerStreaming + Stream streamResponseNoRequest(); + + @ServerStreaming + @RequestType(Long.class) + @ResponseType(String.class) + void reqResp(Object request, StreamObserver observer); + + @ServerStreaming + StreamObserver badArg(String bad, String badToo); + + @ServerStreaming + StreamObserver tooManyArgs(String bad, StreamObserver observer, String badToo); + + @Unary + void unary(String request, StreamObserver observer); + + @ClientStreaming + StreamObserver clientStreaming(StreamObserver request); + + @Bidirectional + StreamObserver bidi(StreamObserver observer); + } +} diff --git a/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplierTest.java b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplierTest.java new file mode 100644 index 00000000000..77486c703d7 --- /dev/null +++ b/microprofile/grpc/core/src/test/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplierTest.java @@ -0,0 +1,829 @@ +/* + * 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.core; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.helidon.grpc.core.MethodHandler; + +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("unchecked") +public class UnaryMethodHandlerSupplierTest { + + @Test + public void shouldSupplyUnaryMethods() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + assertThat(supplier.supplies(method), is(true)); + } + + /** + * Test handler for: + *
+     *     void invoke(ReqT request, StreamObserver observer);     
+     * 
+ */ + @Test + @SuppressWarnings("unchecked") + public void shouldSupplyUnaryHandler() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).unary(eq("foo"), any(StreamObserver.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke(ReqT request, StreamObserver observer);
+     * 
+ */ + @Test + public void shouldSupplyUnaryHandlerAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getUnaryMethod(); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + handler.unary(new Object[]{"bar", observer}, client); + verify(client).unary(eq("foo"), eq("bar")); + verify(observer).onNext("done!"); + verify(observer).onCompleted(); + } + + /** + * Test handler for: + *
+     *     RespT invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForRequestResponse() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("requestResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.requestResponse(anyString())).thenReturn(19L); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).requestResponse(eq("foo")); + verify(observer).onNext(19L); + } + + + /** + * Test client call handler for: + *
+     *     RespT invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForRequestResponseAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("requestResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.requestResponse(anyString())).thenReturn(19L); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + Object result = handler.unary(new Object[]{"bar"}, client); + assertThat(result, is("done!")); + verify(client).unary(eq("foo"), eq("bar")); + } + + /** + * Test handler for: + *
+     *     RespT invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForResponseOnly() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("responseOnly"); + UnaryService service = mock(UnaryService.class); + + when(service.responseOnly()).thenReturn(19L); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).responseOnly(); + verify(observer).onNext(19L); + } + + /** + * Test client call handler for: + *
+     *     RespT invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForResponseOnlyAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("responseOnly"); + UnaryService service = mock(UnaryService.class); + + when(service.responseOnly()).thenReturn(19L); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + Object result = handler.unary(new Object[0], client); + assertThat(result, is("done!")); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + } + + /** + * Test handler for: + *
+     *     void invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForRequestNoResponse() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("requestNoResponse", String.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Empty.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).requestNoResponse(eq("foo")); + verify(observer).onNext(isA(Empty.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForRequestNoResponseAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("requestNoResponse", String.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + Object result = handler.unary(new Object[]{"bar"}, client); + assertThat(result, is(nullValue())); + verify(client).unary(eq("foo"), eq("bar")); + } + + /** + * Test handler for: + *
+     *     void invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForNoRequestNoResponse() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("noRequestNoResponse"); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Empty.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).noRequestNoResponse(); + verify(observer).onNext(isA(Empty.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForNoRequestNoResponseAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("noRequestNoResponse"); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + Object result = handler.unary(new Object[0], client); + assertThat(result, is(nullValue())); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + } + + /** + * Test handler for: + *
+     *     CompletableFuture invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForFutureResponse() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("futureResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.futureResponse(anyString())).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).futureResponse(eq("foo")); + verify(observer).onNext(19L); + } + + /** + * Test client call handler for: + *
+     *     CompletableFuture invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForFutureResponseAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("futureResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.futureResponse(anyString())).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + CompletableFuture result = (CompletableFuture) handler.unary(new Object[]{"bar"}, client); + assertThat(result.get(), is("done!")); + verify(client).unary(eq("foo"), eq("bar")); + } + + /** + * Test handler for: + *
+     *     CompletableFuture invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForFutureResponseNoRequest() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("futureResponseNoRequest"); + UnaryService service = mock(UnaryService.class); + + when(service.futureResponseNoRequest()).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).futureResponseNoRequest(); + verify(observer).onNext(19L); + } + + /** + * Test client call handler for: + *
+     *     CompletableFuture invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForFutureResponseNoRequestAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("futureResponseNoRequest"); + UnaryService service = mock(UnaryService.class); + + when(service.futureResponseNoRequest()).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + CompletableFuture result = (CompletableFuture) handler.unary(new Object[0], client); + assertThat(result.get(), is("done!")); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + } + + /** + * Test handler for: + *
+     *     CompletionStage invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForCompletionStageResponse() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("completionStageResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.completionStageResponse(anyString())).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).completionStageResponse(eq("foo")); + verify(observer).onNext(19L); + } + + /** + * Test client call handler for: + *
+     *     CompletionStage invoke(ReqT request);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForCompletionStageResponseAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("completionStageResponse", String.class); + UnaryService service = mock(UnaryService.class); + + when(service.completionStageResponse(anyString())).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + CompletionStage result = (CompletionStage) handler.unary(new Object[]{"bar"}, client); + assertThat(result.toCompletableFuture().get(), is("done!")); + verify(client).unary(eq("foo"), eq("bar")); + } + + /** + * Test handler for: + *
+     *     CompletionStage invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForCompletionStageResponseNoRequest() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("completionStageResponseNoRequest"); + UnaryService service = mock(UnaryService.class); + + when(service.completionStageResponseNoRequest()).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).completionStageResponseNoRequest(); + verify(observer).onNext(19L); + } + + /** + * Test client call handler for: + *
+     *     CompletionStage invoke();
+     * 
+ */ + @Test + public void shouldSupplyHandlerForCompletionStageResponseNoRequestAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("completionStageResponseNoRequest"); + UnaryService service = mock(UnaryService.class); + + when(service.completionStageResponseNoRequest()).thenReturn(CompletableFuture.completedFuture(19L)); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + CompletionStage result = (CompletionStage) handler.unary(new Object[0], client); + assertThat(result.toCompletableFuture().get(), is("done!")); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + } + + /** + * Test handler for: + *
+     *     void invoke(StreamObserver observer);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithNoRequest() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryNoRequest", StreamObserver.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).unaryNoRequest(any(StreamObserver.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke(StreamObserver observer);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithNoRequestAndHandleClientCall() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryNoRequest", StreamObserver.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + StreamObserver observer = mock(StreamObserver.class); + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + handler.unary(new Object[]{observer}, client); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + verify(observer).onNext("done!"); + verify(observer).onCompleted(); + } + + /** + * Test handler for: + *
+     *     void invoke(ReqT request, CompletableFuture future);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithFuture() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryFuture", String.class, CompletableFuture.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(String.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).unaryFuture(eq("foo"), any(CompletableFuture.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke(ReqT request, CompletableFuture future);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithFutureAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryFuture", String.class, CompletableFuture.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + CompletableFuture future = new CompletableFuture<>(); + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + handler.unary(new Object[]{"bar", future}, client); + verify(client).unary(eq("foo"), eq("bar")); + assertThat(future.get(), is("done!")); + } + + /** + * Test handler for: + *
+     *     void invoke(CompletableFuture future);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithFutureNoRequest() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryFutureNoRequest", CompletableFuture.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Empty.class)); + assertThat(handler.getResponseType(), equalTo(Long.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + + StreamObserver observer = mock(StreamObserver.class); + handler.invoke("foo", observer); + verify(service).unaryFutureNoRequest(any(CompletableFuture.class)); + } + + /** + * Test client call handler for: + *
+     *     void invoke(CompletableFuture future);
+     * 
+ */ + @Test + public void shouldSupplyHandlerForUnaryWithFutureNoRequestAndHandleClientCall() throws Exception { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("unaryFutureNoRequest", CompletableFuture.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + + CompletableFuture future = new CompletableFuture<>(); + MethodHandler.UnaryClient client = mock(MethodHandler.UnaryClient.class); + + when(client.unary(anyString(), any())).thenReturn(CompletableFuture.completedFuture("done!")); + + handler.unary(new Object[]{future}, client); + verify(client).unary(eq("foo"), eq(Empty.getDefaultInstance())); + assertThat(future.get(), is("done!")); + } + + @Test + public void shouldSupplyUnaryHandlerWithTypesFromAnnotation() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("reqResp", StreamObserver.class); + UnaryService service = mock(UnaryService.class); + + MethodHandler handler = supplier.get("foo", method, () -> service); + assertThat(handler, is(notNullValue())); + assertThat(handler.getRequestType(), equalTo(Long.class)); + assertThat(handler.getResponseType(), equalTo(String.class)); + assertThat(handler.type(), equalTo(MethodDescriptor.MethodType.UNARY)); + } + + @Test + public void shouldNotSupplyNullMethod() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + assertThat(supplier.supplies(null), is(false)); + } + + @Test + public void shouldThrowExceptionSupplingNullMethod() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + UnaryService service = mock(UnaryService.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", null, () -> service)); + } + + @Test + public void shouldNotSupplyNoneUnaryHandler() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + UnaryService service = mock(UnaryService.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithInvalidSignature() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class, String.class); + + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotGetHandlerMethodAnnotatedMethodWithWrongArgType() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("badArg", String.class, String.class); + UnaryService service = mock(UnaryService.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyMethodAnnotatedMethodWithTooManyArgs() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getMethod("tooManyArgs", StreamObserver.class, String.class); + UnaryService service = mock(UnaryService.class); + + assertThrows(IllegalArgumentException.class, () -> supplier.get("foo", method, () -> service)); + } + + @Test + public void shouldNotSupplyBidiStreamingMethods() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getBidiMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyServerStreamingMethods() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getServerStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + @Test + public void shouldNotSupplyClientStreamingMethods() { + UnaryMethodHandlerSupplier supplier = new UnaryMethodHandlerSupplier(); + AnnotatedMethod method = getClientStreamingMethod(); + assertThat(supplier.supplies(method), is(false)); + } + + // ----- helper methods ------------------------------------------------- + + private AnnotatedMethod getBidiMethod() { + return getMethod("bidi", StreamObserver.class); + } + + private AnnotatedMethod getUnaryMethod() { + return getMethod("unary", String.class, StreamObserver.class); + } + + private AnnotatedMethod getServerStreamingMethod() { + return getMethod("serverStreaming", String.class, StreamObserver.class); + } + + private AnnotatedMethod getClientStreamingMethod() { + return getMethod("clientStreaming", StreamObserver.class); + } + + private AnnotatedMethod getMethod(String name, Class... args) { + try { + return AnnotatedMethod.create(UnaryService.class.getMethod(name, args)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + /** + * The unary methods service implementation. + */ + @Grpc + @SuppressWarnings("CdiManagedBeanInconsistencyInspection") + public interface UnaryService { + @Unary + Long requestResponse(String request); + + @Unary + Long responseOnly(); + + @Unary + void requestNoResponse(String request); + + @Unary + void noRequestNoResponse(); + + @Unary + CompletableFuture futureResponse(String request); + + @Unary + CompletableFuture futureResponseNoRequest(); + + @Unary + CompletionStage completionStageResponse(String request); + + @Unary + CompletionStage completionStageResponseNoRequest(); + + @Unary + void unary(String request, StreamObserver observer); + + @Unary + void unaryNoRequest(StreamObserver observer); + + @Unary + void unaryFuture(String request, CompletableFuture future); + + @Unary + void unaryFutureNoRequest(CompletableFuture future); + + @Unary + @RequestType(Long.class) + @ResponseType(String.class) + Number reqResp(StreamObserver observer); + + @Unary + StreamObserver badArg(String bad, String badToo); + + @Unary + StreamObserver tooManyArgs(StreamObserver observer, String bad); + + @ClientStreaming + StreamObserver clientStreaming(StreamObserver request); + + @Bidirectional + StreamObserver bidi(StreamObserver observer); + + @ServerStreaming + void serverStreaming(String request, StreamObserver observer); + } +} diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml new file mode 100644 index 00000000000..0168d936e7b --- /dev/null +++ b/microprofile/grpc/pom.xml @@ -0,0 +1,72 @@ + + + + + 4.0.0 + + io.helidon.microprofile + helidon-microprofile-project + 4.0.0-SNAPSHOT + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc + Helidon Microprofile gRPC + pom + + + core + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dgrpc.marshaller.java.enabled=true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + -Dgrpc.marshaller.java.enabled=true + false + + **/*IT.java + + + + + verify + verify + + integration-test + verify + + + + + + + + diff --git a/microprofile/pom.xml b/microprofile/pom.xml index 327b8b55752..7cd948698ec 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -53,6 +53,7 @@ messaging cors graphql + grpc scheduling lra bean-validation From 31a9f382fc363dcb09da01bc7ab99a9c4a7d7898 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 4 Jun 2024 15:17:25 -0400 Subject: [PATCH 02/30] Ports helidon-microprofile-server module from 3.x. Implementation is still incomplete, but module now compiles. Signed-off-by: Santiago Pericas-Geertsen --- bom/pom.xml | 10 + grpc/core/pom.xml | 6 +- .../io/helidon/grpc/core/ContextKeys.java | 52 ++ .../io/helidon/grpc/core/WeightedBag.java | 11 + grpc/core/src/main/java/module-info.java | 1 + microprofile/grpc/core/pom.xml | 2 +- .../BidirectionalMethodHandlerSupplier.java | 2 +- .../ClientStreamingMethodHandlerSupplier.java | 2 +- .../helidon/microprofile/grpc/core/Grpc.java | 3 +- .../grpc/core/GrpcMarshaller.java | 4 +- .../grpc/core/MethodHandlerSupplier.java | 10 +- .../ServerStreamingMethodHandlerSupplier.java | 2 +- .../grpc/core/UnaryMethodHandlerSupplier.java | 2 +- microprofile/grpc/pom.xml | 38 +- microprofile/grpc/server/pom.xml | 136 +++ .../server/AnnotatedServiceConfigurer.java | 41 + .../grpc/server/BindableServiceImpl.java | 210 +++++ .../grpc/server/ConstantHealthCheck.java | 58 ++ .../ContextSettingServerInterceptor.java | 104 +++ .../microprofile/grpc/server/GrpcServer.java | 302 +++++++ .../grpc/server/GrpcServerBasicConfig.java | 133 +++ .../grpc/server/GrpcServerCdiExtension.java | 326 ++++++++ .../grpc/server/GrpcServerConfiguration.java | 356 ++++++++ .../grpc/server/GrpcServerImpl.java | 85 ++ .../grpc/server/GrpcServiceBuilder.java | 365 +++++++++ .../grpc/server/GrpcTlsDescriptor.java | 219 +++++ .../grpc/server/package-info.java | 20 + .../grpc/server/spi/GrpcMpContext.java | 76 ++ .../grpc/server/spi/GrpcMpExtension.java | 29 + .../grpc/server/spi/package-info.java | 20 + .../server/src/main/java/module-info.java | 55 ++ .../src/main/resources/META-INF/beans.xml | 25 + .../grpc/server/GrpcServiceBuilderTest.java | 332 ++++++++ .../grpc/server/StubMarshaller.java | 53 ++ .../grpc/server/src/test/proto/services.proto | 58 ++ .../src/test/resources/META-INF/beans.xml | 25 + .../io.helidon.grpc.server.MarshallerSupplier | 17 + ...croprofile.grpc.server.spi.GrpcMpExtension | 17 + .../src/test/resources/logging.properties | 34 + webserver/grpc/pom.xml | 4 + .../helidon/webserver/grpc/GrpcRouting.java | 99 ++- .../webserver/grpc/MethodDescriptor.java | 345 ++++++++ .../webserver/grpc/ServiceDescriptor.java | 771 ++++++++++++++++++ webserver/grpc/src/main/java/module-info.java | 1 + 44 files changed, 4438 insertions(+), 23 deletions(-) create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java create mode 100644 microprofile/grpc/server/pom.xml create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilder.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/package-info.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java create mode 100644 microprofile/grpc/server/src/main/java/module-info.java create mode 100644 microprofile/grpc/server/src/main/resources/META-INF/beans.xml create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java create mode 100644 microprofile/grpc/server/src/test/proto/services.proto create mode 100644 microprofile/grpc/server/src/test/resources/META-INF/beans.xml create mode 100644 microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier create mode 100644 microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension create mode 100644 microprofile/grpc/server/src/test/resources/logging.properties create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java diff --git a/bom/pom.xml b/bom/pom.xml index 82b37cb2f22..8532fa655be 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -100,6 +100,16 @@ helidon-grpc-core ${helidon.version} + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + ${helidon.version} + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-server + ${helidon.version} + io.helidon.integrations.micronaut diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index 652b107f9f3..1d6c291dfc4 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -26,13 +26,17 @@ 4.0.0 helidon-grpc-core - Helidon gRPC related modules + Helidon gRPC Core io.helidon.common helidon-common + + io.helidon.common + helidon-common-context + io.helidon.http helidon-http diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java new file mode 100644 index 00000000000..81c492097e4 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java @@ -0,0 +1,52 @@ +/* + * 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.grpc.core; + +import java.lang.reflect.Method; + +import io.grpc.Context; +import io.grpc.Metadata; + +/** + * A collection of common gRPC {@link Context.Key} and + * {@link Metadata.Key} instances. + */ +public final class ContextKeys { + /** + * The {@link Metadata.Key} to use to obtain the authorization data. + */ + public static final Metadata.Key AUTHORIZATION = + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + /** + * The gRPC context key to use to obtain the Helidon {@link io.helidon.common.context.Context} + * from the gRPC {@link Context}. + */ + public static final Context.Key HELIDON_CONTEXT = + Context.key(io.helidon.common.context.Context.class.getCanonicalName()); + + /** + * The {@link Context.Key} to use to obtain the actual underlying rpc {@link Method}. + */ + public static final Context.Key SERVICE_METHOD = Context.key(Method.class.getName()); + + /** + * Private constructor for utility class. + */ + private ContextKeys() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java index 6199ab63ff8..87e8304fabf 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -104,6 +104,17 @@ public void merge(WeightedBag bag) { bag.contents.forEach((weight, value) -> addAll(value, weight)); } + /** + * Obtain a copy of this {@link WeightedBag}. + * + * @return a copy of this {@link WeightedBag} + */ + public WeightedBag copyMe() { + WeightedBag copy = WeightedBag.create(); + copy.merge(this); + return copy; + } + /** * Add elements to the bag. *

diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java index 2bb58f7a62e..c40228680ff 100644 --- a/grpc/core/src/main/java/module-info.java +++ b/grpc/core/src/main/java/module-info.java @@ -23,6 +23,7 @@ requires io.helidon.common; requires io.helidon.http; + requires io.helidon.common.context; requires transitive io.grpc; requires transitive io.grpc.stub; diff --git a/microprofile/grpc/core/pom.xml b/microprofile/grpc/core/pom.xml index d8ee2f6be43..4bdc1cdf2fd 100644 --- a/microprofile/grpc/core/pom.xml +++ b/microprofile/grpc/core/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc - 4.0.0-SNAPSHOT + 4.1.0-SNAPSHOT helidon-microprofile-grpc-core diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java index 3ba8f0339e2..36c01525b1f 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java @@ -29,7 +29,7 @@ import io.grpc.stub.StreamObserver; /** - * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for bi-directional streaming gRPC methods. + * A supplier of {@link MethodHandler}s for bi-directional streaming gRPC methods. */ public class BidirectionalMethodHandlerSupplier extends AbstractMethodHandlerSupplier { diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java index e95350df1dc..f9add37e532 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java @@ -32,7 +32,7 @@ import static io.helidon.grpc.core.ResponseHelper.completeAsync; /** - * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for client streaming gRPC methods. + * A supplier of {@link MethodHandler}s for client streaming gRPC methods. */ public class ClientStreamingMethodHandlerSupplier extends AbstractMethodHandlerSupplier { diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java index ddd59270e07..661c98a323f 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java @@ -52,8 +52,7 @@ /** * An {@link jakarta.enterprise.util.AnnotationLiteral} for the {@link Grpc} annotation. */ - class Literal - extends AnnotationLiteral implements Grpc { + class Literal extends AnnotationLiteral implements Grpc { /** * The singleton instance of {@link Literal}. diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java index a35d1a72e32..fd71e3765e6 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcMarshaller.java @@ -34,8 +34,8 @@ @Inherited public @interface GrpcMarshaller { /** - * Obtain the type of the {@link io.helidon.grpc.core.MarshallerSupplier} to use. - * @return the type of the {@link io.helidon.grpc.core.MarshallerSupplier} to use + * Obtain the type of the {@link MarshallerSupplier} to use. + * @return the type of the {@link MarshallerSupplier} to use */ String value() default MarshallerSupplier.DEFAULT; } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java index 3c9a908326c..c7f2ef3565d 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/MethodHandlerSupplier.java @@ -20,7 +20,7 @@ import io.helidon.grpc.core.MethodHandler; /** - * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for {@link AnnotatedMethod}s. + * A supplier of {@link MethodHandler}s for {@link AnnotatedMethod}s. *

* Implementation classes may be annotated with {@link jakarta.annotation.Priority} * to influence their priority order when determining which supplier is used if @@ -32,7 +32,7 @@ public interface MethodHandlerSupplier { /** * Determine whether this {@link MethodHandlerSupplier} can supply - * a {@link io.helidon.grpc.core.MethodHandler} for a given method and type. + * a {@link MethodHandler} for a given method and type. * * @param method the {@link AnnotatedMethod} to supply a handler for * @return {@code true} if this supplier can supply a handler for the method @@ -40,13 +40,13 @@ public interface MethodHandlerSupplier { boolean supplies(AnnotatedMethod method); /** - * Supply a {@link io.helidon.grpc.core.MethodHandler} for a method. + * Supply a {@link MethodHandler} for a method. * @param methodName the gRPC method name - * @param method the method to supply a {@link io.helidon.grpc.core.MethodHandler} for + * @param method the method to supply a {@link MethodHandler} for * @param instance the supplier to supply the actual call handler * @param the request type * @param the response type - * @return a {@link io.helidon.grpc.core.MethodHandler} for the method + * @return a {@link MethodHandler} for the method * @throws java.lang.NullPointerException if the method is null */ MethodHandler get(String methodName, AnnotatedMethod method, Supplier instance); diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java index f4703f88883..bc517a11905 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java @@ -36,7 +36,7 @@ import static io.helidon.grpc.core.ResponseHelper.stream; /** - * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for server streaming gRPC methods. + * A supplier of {@link MethodHandler}s for server streaming gRPC methods. */ public class ServerStreamingMethodHandlerSupplier extends AbstractMethodHandlerSupplier { diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java index ea1a46536b5..4151f3f37c5 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java @@ -32,7 +32,7 @@ import io.grpc.stub.StreamObserver; /** - * A supplier of {@link io.helidon.grpc.core.MethodHandler}s for unary gRPC methods. + * A supplier of {@link MethodHandler}s for unary gRPC methods. */ public class UnaryMethodHandlerSupplier extends AbstractMethodHandlerSupplier { diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml index 0168d936e7b..cf839094aeb 100644 --- a/microprofile/grpc/pom.xml +++ b/microprofile/grpc/pom.xml @@ -22,16 +22,44 @@ io.helidon.microprofile helidon-microprofile-project - 4.0.0-SNAPSHOT + 4.1.0-SNAPSHOT io.helidon.microprofile.grpc helidon-microprofile-grpc Helidon Microprofile gRPC + + + io.grpc + grpc-api + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.health + helidon-health + + + io.helidon.webserver + helidon-webserver-grpc + + + io.helidon.config + helidon-config-metadata + + + org.eclipse.microprofile.health + microprofile-health-api + 4.0.1 + + pom core + server @@ -66,6 +94,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml new file mode 100644 index 00000000000..f820c8aeddb --- /dev/null +++ b/microprofile/grpc/server/pom.xml @@ -0,0 +1,136 @@ + + + + + 4.0.0 + + io.helidon.microprofile.grpc + helidon-microprofile-grpc + 4.1.0-SNAPSHOT + + + helidon-microprofile-grpc-server + Helidon Microprofile gRPC Server + The microprofile gRPC Server implementation + + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + + + io.helidon.webserver + helidon-webserver-grpc + + + io.helidon.common + helidon-common + + + io.helidon.config + helidon-config-object-mapping + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + io.helidon.microprofile.config + helidon-microprofile-config + + + org.eclipse.microprofile.health + microprofile-health-api + + + io.helidon.microprofile.server + helidon-microprofile-server + + + io.grpc + grpc-inprocess + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-jdk14 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.mockito + mockito-core + test + + + io.reactivex.rxjava2 + rxjava + test + + + + javax.annotation + javax.annotation-api + provided + true + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java new file mode 100644 index 00000000000..0a2fdd8868b --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java @@ -0,0 +1,41 @@ +/* + * 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.server; + +import io.helidon.webserver.grpc.ServiceDescriptor; + +/** + * A class that may apply modifications to a {@link ServiceDescriptor.Builder} + * for an annotated gRPC service class. + *

+ * Implementations of this class are called by the {@link GrpcServiceBuilder} when + * it builds a {@link ServiceDescriptor} from an annotated class. Instances of + * {@link AnnotatedServiceConfigurer} are discovered using the + * {@link io.helidon.common.HelidonServiceLoader}. This service + * loader supports ordering of configurers. + */ +@FunctionalInterface +public interface AnnotatedServiceConfigurer { + /** + * Apply modifications to a {@link ServiceDescriptor.Builder}. + * + * @param serviceClass the annotated gRPC service class + * @param annotatedClass the class with the {@link io.helidon.microprofile.grpc.core.Grpc} annotation + * @param builder the builder to modify + */ + void accept(Class serviceClass, Class annotatedClass, ServiceDescriptor.Builder builder); +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java new file mode 100644 index 00000000000..b7965ae9bc4 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java @@ -0,0 +1,210 @@ +/* + * 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.server; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.webserver.grpc.MethodDescriptor; +import io.helidon.webserver.grpc.ServiceDescriptor; + +import io.grpc.BindableService; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.protobuf.ProtoFileDescriptorSupplier; +import io.grpc.stub.StreamObserver; + +/** + * A {@link BindableService} implementation that creates {@link ServerServiceDefinition} + * from a {@link ServiceDescriptor}. + */ +class BindableServiceImpl implements BindableService { + /** + * The descriptor of this service. + */ + private final ServiceDescriptor descriptor; + + /** + * The global interceptors to apply. + */ + private final WeightedBag globalInterceptors; + + private BindableServiceImpl(ServiceDescriptor descriptor, WeightedBag interceptors) { + this.descriptor = descriptor; + this.globalInterceptors = interceptors.copyMe(); + } + + /** + * Create a {@link BindableServiceImpl} for a gRPC service. + * + * @param descriptor the service descriptor + * @param interceptors the bag of interceptors to apply to the service + * @return a {@link BindableServiceImpl} for the gRPC service + */ + static BindableServiceImpl create(ServiceDescriptor descriptor, WeightedBag interceptors) { + return new BindableServiceImpl(descriptor, interceptors); + } + + // ---- BindableService implementation ---------------------------------- + + @SuppressWarnings("unchecked") + @Override + public ServerServiceDefinition bindService() { + io.grpc.ServiceDescriptor.Builder serviceDescriptorBuilder = + io.grpc.ServiceDescriptor.newBuilder(descriptor.fullName()); + if (descriptor.proto() != null) { + serviceDescriptorBuilder.setSchemaDescriptor((ProtoFileDescriptorSupplier) descriptor::proto); + } + descriptor.methods() + .forEach(method -> serviceDescriptorBuilder.addMethod(method.descriptor())); + + ServerServiceDefinition.Builder builder = ServerServiceDefinition.builder(serviceDescriptorBuilder.build()); + descriptor.methods() + .forEach(method -> builder.addMethod((io.grpc.MethodDescriptor) method.descriptor(), + wrapCallHandler(method))); + + return builder.build(); + } + + // ---- helpers --------------------------------------------------------- + + private ServerCallHandler wrapCallHandler(MethodDescriptor method) { + ServerCallHandler handler = method.callHandler(); + + WeightedBag priorityServerInterceptors = WeightedBag.create(InterceptorWeights.USER); + priorityServerInterceptors.addAll(globalInterceptors); + priorityServerInterceptors.addAll(descriptor.interceptors()); + priorityServerInterceptors.addAll(method.interceptors()); + List interceptors = priorityServerInterceptors.stream().collect(Collectors.toList()); + + if (!interceptors.isEmpty()) { + LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); + + // iterate the interceptors in reverse order so that the handler chain is in the correct order + for (int i = interceptors.size() - 1; i >= 0; i--) { + ServerInterceptor interceptor = interceptors.get(i); + if (!uniqueInterceptors.contains(interceptor)) { + uniqueInterceptors.add(interceptor); + } + } + + for (ServerInterceptor interceptor : uniqueInterceptors) { + handler = new InterceptingCallHandler<>(descriptor, interceptor, handler); + } + } + + return handler; + } + + static Supplier createSupplier(Callable callable) { + return new CallableSupplier<>(callable); + } + + static class CallableSupplier implements Supplier { + private Callable callable; + + CallableSupplier(Callable callable) { + this.callable = callable; + } + + @Override + public T get() { + try { + return callable.call(); + } catch (Exception e) { + throw new CompletionException(e.getMessage(), e); + } + } + } + + static BiConsumer completeWithResult(StreamObserver observer) { + return new CompletionAction<>(observer, true); + } + + static BiConsumer completeWithoutResult(StreamObserver observer) { + return new CompletionAction<>(observer, false); + } + + static class CompletionAction implements BiConsumer { + private StreamObserver observer; + private boolean sendResult; + + CompletionAction(StreamObserver observer, boolean sendResult) { + this.observer = observer; + this.sendResult = sendResult; + } + + @Override + @SuppressWarnings("unchecked") + public void accept(T result, Throwable error) { + if (error != null) { + observer.onError(error); + } else { + if (sendResult) { + observer.onNext((U) result); + } + observer.onCompleted(); + } + } + } + + /** + * A {@link ServerCallHandler} that wraps a {@link ServerCallHandler} with + * a {@link ServerInterceptor}. + *

+ * If the wrapped {@link ServerInterceptor} implements {@link ServiceDescriptor.Aware} + * then the {@link ServiceDescriptor.Aware#setServiceDescriptor(ServiceDescriptor)} method + * will be called before calling {@link ServerInterceptor#interceptCall(ServerCall, + * Metadata, ServerCallHandler)}. + * + * @param the request type + * @param the response type + */ + static final class InterceptingCallHandler implements ServerCallHandler { + private final ServiceDescriptor serviceDefinition; + private final ServerInterceptor interceptor; + private final ServerCallHandler callHandler; + + private InterceptingCallHandler(ServiceDescriptor serviceDefinition, + ServerInterceptor interceptor, + ServerCallHandler callHandler) { + this.serviceDefinition = serviceDefinition; + this.interceptor = interceptor; + this.callHandler = callHandler; + } + + @Override + public ServerCall.Listener startCall( + ServerCall call, + Metadata headers) { + if (interceptor instanceof ServiceDescriptor.Aware) { + ((ServiceDescriptor.Aware) interceptor).setServiceDescriptor(serviceDefinition); + } + return interceptor.interceptCall(call, headers, callHandler); + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java new file mode 100644 index 00000000000..fd7a7ea79fd --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.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.server; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; + +/** + * A simple {@link HealthCheck} implementation + * that always returns the same response. + */ +public class ConstantHealthCheck implements HealthCheck { + + private final HealthCheckResponse response; + + private ConstantHealthCheck(HealthCheckResponse response) { + this.response = response; + } + + @Override + public HealthCheckResponse call() { + return response; + } + + /** + * Obtain a {@link HealthCheck} that always returns a status of up. + * + * @param name the service name that the health check is for + * @return a {@link HealthCheck} that always returns a status of up + */ + public static HealthCheck up(String name) { + return new ConstantHealthCheck(HealthCheckResponse.named(name).up().build()); + } + + /** + * Obtain a {@link HealthCheck} that always returns a status of down. + * + * @param name the service name that the health check is for + * @return a {@link HealthCheck} that always returns a status of down + */ + public static HealthCheck down(String name) { + return new ConstantHealthCheck(HealthCheckResponse.named(name).down().build()); + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java new file mode 100644 index 00000000000..61f6ac1cc5d --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java @@ -0,0 +1,104 @@ +/* + * 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.server; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.common.Weight; +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.webserver.grpc.MethodDescriptor; +import io.helidon.webserver.grpc.ServiceDescriptor; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * A {@link ServerInterceptor} that sets values into the gRPC call context. + */ +@Weight(InterceptorWeights.CONTEXT) +class ContextSettingServerInterceptor implements ServerInterceptor, ServiceDescriptor.Aware { + + /** + * The {@link ServiceDescriptor} for the service being intercepted. + */ + private ServiceDescriptor serviceDescriptor; + + private ContextSettingServerInterceptor() { + } + + /** + * Create a {@link ContextSettingServerInterceptor}. + * + * @return a {@link ContextSettingServerInterceptor} instance. + */ + static ContextSettingServerInterceptor create() { + return new ContextSettingServerInterceptor(); + } + + @Override + @SuppressWarnings("unchecked") + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + + Context context = Context.current(); + String fullMethodName = call.getMethodDescriptor().getFullMethodName(); + String methodName = extractMethodName(fullMethodName); + MethodDescriptor methodDescriptor = serviceDescriptor.method(methodName); + Map, Object> contextMap = new HashMap<>(); + + // apply context keys from the service followed by the method + // so that the method can override any service keys + contextMap.putAll(serviceDescriptor.context()); + contextMap.putAll(methodDescriptor.context()); + contextMap.put(ServiceDescriptor.SERVICE_DESCRIPTOR_KEY, serviceDescriptor); + + if (!contextMap.isEmpty()) { + for (Map.Entry, Object> entry : contextMap.entrySet()) { + Context.Key key = (Context.Key) entry.getKey(); + context = context.withValue(key, entry.getValue()); + } + } + + return Contexts.interceptCall(context, call, headers, next); + } + + @Override + public void setServiceDescriptor(ServiceDescriptor descriptor) { + this.serviceDescriptor = descriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java new file mode 100644 index 00000000000..d3188d71342 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java @@ -0,0 +1,302 @@ +/* + * 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.server; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +import io.helidon.common.context.Context; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.grpc.ServiceDescriptor; + +import io.grpc.ServerInterceptor; +import org.eclipse.microprofile.health.HealthCheck; + +/** + * Represents a immutably configured gRPC server. + *

+ * Provides a basic lifecycle and monitoring API. + *

+ * Instance can be created from {@link GrpcRouting} and optionally from {@link + * GrpcServerConfiguration} using {@link #create(GrpcRouting)}, {@link + * #create(GrpcServerConfiguration, GrpcRouting)} or {@link #builder(GrpcRouting)} methods + * and their builder enabled overloads. + */ +public interface GrpcServer { + /** + * Gets effective server configuration. + * + * @return Server configuration + */ + GrpcServerConfiguration configuration(); + + /** + * Gets a {@link GrpcServer} context. + * + * @return a server context + */ + Context context(); + + /** + * Starts the server. Has no effect if server is running. + * + * @return a completion stage of starting process + */ + CompletionStage start(); + + /** + * Completion stage is completed when server is shut down. + * + * @return a completion stage of the server + */ + CompletionStage whenShutdown(); + + /** + * Attempt to gracefully shutdown server. It is possible to use returned + * {@link CompletionStage} to react. + *

+ * RequestMethod can be called periodically. + * + * @return to react on finished shutdown process + * @see #start() + */ + CompletionStage shutdown(); + + /** + * Return an array of health checks for this server. + * + * @return an array of {@link HealthCheck} instances for this server + */ + HealthCheck[] healthChecks(); + + /** + * Obtain the deployed services. + * + * @return an immutable {@link Map} of deployed {@link ServiceDescriptor}s + * keyed by service name + */ + Map services(); + + /** + * Returns {@code true} if the server is currently running. A running server + * in the stopping phase returns {@code true} until it is fully stopped. + * + * @return {@code true} if server is running + */ + boolean isRunning(); + + /** + * Returns a port number the default server socket is bound to and is + * listening on; or {@code -1} if unknown or not active. + *

+ * Only supported only when server is running. + * + * @return a listen port; or {@code -1} if unknown or the default server + * socket is not active + */ + int port(); + + /** + * Creates a new instance from a provided configuration and a GrpcRouting. + * + * @param configurationBuilder a server configuration builder that will be + * built as a first step of this method + * execution; may be {@code null} + * @param routing a GrpcRouting instance + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(Supplier configurationBuilder, GrpcRouting routing) { + return create(configurationBuilder != null + ? configurationBuilder.get() + : null, routing); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configurationBuilder a server configuration builder that will be + * built as a first step of this method + * execution; may be {@code null} + * @param routingBuilder a GrpcRouting builder that will be built as a + * second step of this method execution + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routingBuilder' parameter is {@code + * null} + */ + static GrpcServer create(Supplier configurationBuilder, + Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(configurationBuilder != null + ? configurationBuilder.get() + : null, routingBuilder.get()); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configuration a server configuration instance + * @param routingBuilder a GrpcRouting builder that will be built as a second + * step of this method execution + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routingBuilder' parameter is {@code + * null} + */ + static GrpcServer create( + GrpcServerConfiguration configuration, + Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(configuration, routingBuilder.get()); + } + + /** + * Creates new instance form provided GrpcRouting and default configuration. + * + * @param routing a GrpcRouting instance + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'routing' parameter is {@code null} + */ + static GrpcServer create(GrpcRouting routing) { + return create((GrpcServerConfiguration) null, routing); + } + + /** + * Creates new instance form provided configuration and GrpcRouting. + * + * @param configuration a server configuration instance + * @param routing a GrpcRouting instance + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(GrpcServerConfiguration configuration, GrpcRouting routing) { + Objects.requireNonNull(routing, "Parameter 'routing' is null!"); + + return builder(routing) + .config(configuration) + .build(); + } + + /** + * Creates new instance form provided GrpcRouting and default configuration. + * + * @param routingBuilder a GrpcRouting builder instance that will be built as a + * first step of this method execution + * @return a new gRPC server instance + * @throws IllegalStateException if none SPI implementation found + * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} + */ + static GrpcServer create(Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return create(routingBuilder.get()); + } + + /** + * Creates a builder of the {@link GrpcServer}. + * + * @param routingBuilder the GrpcRouting builder; must not be {@code null} + * @return the builder + */ + static Builder builder(Supplier routingBuilder) { + Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); + return builder(routingBuilder.get()); + } + + /** + * Creates a builder of the {@link GrpcServer}. + * + * @param routing the GrpcRouting; must not be {@code null} + * @return the builder + */ + static Builder builder(GrpcRouting routing) { + return new Builder(GrpcServerConfiguration.create(), routing); + } + + /** + * GrpcServer builder class provides a convenient way to timed a + * GrpcServer instance. + */ + final class Builder implements io.helidon.common.Builder { + + private final GrpcRouting routing; + + private GrpcServerConfiguration configuration; + + private Builder(GrpcServerConfiguration configuration, GrpcRouting routing) { + Objects.requireNonNull(configuration, "Parameter 'configuration' must not be null!"); + Objects.requireNonNull(routing, "Parameter 'routing' must not be null!"); + + this.configuration = configuration; + this.routing = routing; + } + + /** + * Set a configuration of the {@link GrpcServer}. + * + * @param configuration the configuration + * @return an updated builder + */ + public Builder config(GrpcServerConfiguration configuration) { + this.configuration = configuration != null ? configuration : GrpcServerConfiguration.create(); + return this; + } + + /** + * Set a configuration of the {@link GrpcServer}. + * + * @param configurationBuilder the configuration builder + * @return an updated builder + */ + public Builder config(Supplier configurationBuilder) { + this.configuration = configurationBuilder != null + ? configurationBuilder.get() + : GrpcServerConfiguration.create(); + return this; + } + + /** + * Builds the {@link GrpcServer} instance as configured by this builder + * and its parameters. + * + * @return a ready to use {@link GrpcServer} + */ + @Override + public GrpcServer build() { + WeightedBag interceptors = WeightedBag.create(); + GrpcServerImpl server = GrpcServerImpl.create(configuration); + + interceptors.add(ContextSettingServerInterceptor.create()); + + // add the global interceptors from the routing AFTER the tracing interceptor + // so that all of those interceptors are included in the trace timings + interceptors.merge(routing.interceptors()); + + for (ServiceDescriptor service : routing.services()) { + server.deploy(service, interceptors); + } + + return server; + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java new file mode 100644 index 00000000000..798ef4679a9 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java @@ -0,0 +1,133 @@ +/* + * 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.server; + +import java.time.Duration; + +import io.helidon.common.context.Context; + +/** + * Configuration class for the {@link GrpcServer} implementations. + */ +public class GrpcServerBasicConfig + implements GrpcServerConfiguration { + + private final String name; + + private final int port; + + private final boolean nativeTransport; + + private final int workers; + + private final GrpcTlsDescriptor tlsConfig; + + private final Context context; + + private final int maxRapidResets; + + private final Duration rapidResetCheckPeriod; + + /** + * Construct {@link GrpcServerBasicConfig} instance. + * + * @param builder the {@link GrpcServerConfiguration.Builder} to use to configure + * this {@link GrpcServerBasicConfig}. + */ + private GrpcServerBasicConfig(GrpcServerConfiguration.Builder builder) { + this.name = builder.name(); + this.port = builder.port(); + this.context = builder.context(); + this.nativeTransport = builder.useNativeTransport(); + this.workers = builder.workers(); + this.tlsConfig = builder.tlsConfig(); + this.maxRapidResets = builder.maxRapidResets(); + this.rapidResetCheckPeriod = builder.rapidResetCheckPeriod(); + } + + /** + * Create a {@link GrpcServerBasicConfig} instance using the specified builder. + * + * @param builder the {@link GrpcServerConfiguration.Builder} to use to configure + * this {@link GrpcServerBasicConfig} + * @return a {@link GrpcServerBasicConfig} instance + */ + static GrpcServerBasicConfig create(GrpcServerConfiguration.Builder builder) { + return new GrpcServerBasicConfig(builder); + } + + // ---- accessors --------------------------------------------------- + + /** + * Get the server name. + * + * @return the server name + */ + @Override + public String name() { + return name; + } + + /** + * Get the server port. + * + * @return the server port + */ + @Override + public int port() { + return port; + } + + @Override + public Context context() { + return context; + } + + /** + * Determine whether use native transport if possible. + *

+ * If native transport support is enabled, gRPC server will use epoll on + * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will + * be used. + * + * @return {@code true} if native transport should be used + */ + @Override + public boolean useNativeTransport() { + return nativeTransport; + } + + @Override + public int workers() { + return workers; + } + + @Override + public GrpcTlsDescriptor tlsConfig() { + return tlsConfig; + } + + @Override + public Duration rapidResetCheckPeriod() { + return rapidResetCheckPeriod; + } + + @Override + public int maxRapidResets() { + return maxRapidResets; + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java new file mode 100644 index 00000000000..56b0713bda4 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java @@ -0,0 +1,326 @@ +/* + * 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.server; + +import java.lang.annotation.Annotation; +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.InProcessGrpcChannel; +import io.helidon.microprofile.grpc.server.spi.GrpcMpContext; +import io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.grpc.GrpcService; + +import io.grpc.BindableService; +import io.grpc.Channel; +import io.grpc.inprocess.InProcessChannelBuilder; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.AfterDeploymentValidation; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeShutdown; +import jakarta.enterprise.inject.spi.Extension; +import org.eclipse.microprofile.config.ConfigProvider; + +/** + * A CDI extension that will start the {@link GrpcServer gRPC server}. + *

+ * The server is started when the {@link AfterDeploymentValidation} event + * is received and will be stopped when the {@link BeforeShutdown} event + * is received. + *

+ * If no gRPC services are discovered the gRPC server will not be started. + */ +public class GrpcServerCdiExtension implements Extension { + + private static final Logger LOGGER = Logger.getLogger(GrpcServerCdiExtension.class.getName()); + private static final Logger STARTUP_LOGGER = Logger.getLogger("io.helidon.microprofile.startup.server"); + + private GrpcServer server; + + + private void startServer(@Observes @Initialized(ApplicationScoped.class) Object event, BeanManager beanManager) { + GrpcRouting.Builder routingBuilder = discoverGrpcRouting(beanManager); + + Config config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); + GrpcServerConfiguration.Builder serverConfiguration = GrpcServerConfiguration.builder(config.get("grpc")); + CompletableFuture startedFuture = new CompletableFuture<>(); + CompletableFuture shutdownFuture = new CompletableFuture<>(); + + loadExtensions(beanManager, config, routingBuilder, serverConfiguration, startedFuture, shutdownFuture); + server = GrpcServer.create(serverConfiguration.build(), routingBuilder.build()); + long beforeT = System.nanoTime(); + + server.start() + .whenComplete((grpcServer, throwable) -> { + if (null != throwable) { + STARTUP_LOGGER.log(Level.SEVERE, throwable, () -> "gRPC server startup failed"); + startedFuture.completeExceptionally(throwable); + } else { + long t = TimeUnit.MILLISECONDS.convert(System.nanoTime() - beforeT, TimeUnit.NANOSECONDS); + + int port = grpcServer.port(); + STARTUP_LOGGER.finest("gRPC server started up"); + LOGGER.info(() -> "gRPC server started on localhost:" + port + " (and all other host addresses) " + + "in " + t + " milliseconds."); + + grpcServer.whenShutdown() + .whenComplete((server, error) -> { + if (error == null) { + shutdownFuture.complete(server); + } else { + shutdownFuture.completeExceptionally(error); + } + }); + + startedFuture.complete(grpcServer); + } + }); + + // inject the server into the producer so that it can be discovered later + ServerProducer serverProducer = beanManager.createInstance().select(ServerProducer.class).get(); + serverProducer.server(server); + } + + private void stopServer(@Observes BeforeShutdown event) { + if (server != null) { + LOGGER.info("Stopping gRPC server"); + long beforeT = System.nanoTime(); + server.shutdown() + .whenComplete((webServer, throwable) -> { + if (null != throwable) { + LOGGER.log(Level.SEVERE, throwable, () -> "An error occurred stopping the gRPC server"); + } else { + long t = TimeUnit.MILLISECONDS.convert(System.nanoTime() - beforeT, TimeUnit.NANOSECONDS); + LOGGER.info(() -> "gRPC Server stopped in " + t + " milliseconds."); + } + }); + } + } + + /** + * Discover the services and interceptors to use to configure the + * {@link GrpcRouting}. + * + * @param beanManager the CDI bean manager + * @return the {@link GrpcRouting} to use or {@code null} if no services + * or routing were discovered + */ + private GrpcRouting.Builder discoverGrpcRouting(BeanManager beanManager) { + Instance instance = beanManager.createInstance(); + GrpcRouting.Builder builder = GrpcRouting.builder(); + + // discover @Grpc annotated beans + // we use the bean manager to do this as we need the actual bean class + beanManager.getBeans(Object.class, Any.Literal.INSTANCE) + .stream() + .filter(this::hasGrpcQualifier) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + register(service, builder, beanClass, beanManager); + }); + + // discover beans of type GrpcService + beanManager.getBeans(GrpcService.class) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + builder.service((GrpcService) service); + }); + + // discover beans of type BindableService + beanManager.getBeans(BindableService.class) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + builder.service((BindableService) service); + }); + + return builder; + } + + private boolean hasGrpcQualifier(Bean bean) { + return bean.getQualifiers() + .stream() + .anyMatch(q -> Grpc.class.isAssignableFrom(q.annotationType())); + } + + /** + * Load any instances of {@link GrpcMpExtension} discovered by the + * {@link ServiceLoader} and allow them to further configure the + * gRPC server. + * + * @param beanManager the {@link BeanManager} + * @param config the Helidon configuration + * @param routingBuilder the {@link GrpcRouting.Builder} + * @param serverConfiguration the {@link GrpcServerConfiguration} + */ + private void loadExtensions(BeanManager beanManager, + Config config, + GrpcRouting.Builder routingBuilder, + GrpcServerConfiguration.Builder serverConfiguration, + CompletionStage whenStarted, + CompletionStage whenShutdown) { + + GrpcMpContext context = new GrpcMpContext() { + @Override + public Config config() { + return config; + } + + @Override + public GrpcServerConfiguration.Builder grpcServerConfiguration() { + return serverConfiguration; + } + + @Override + public GrpcRouting.Builder routing() { + return routingBuilder; + } + + @Override + public BeanManager beanManager() { + return beanManager; + } + + @Override + public CompletionStage whenStarted() { + return whenStarted; + } + + @Override + public CompletionStage whenShutdown() { + return whenShutdown; + } + }; + + HelidonServiceLoader.create(ServiceLoader.load(GrpcMpExtension.class)) + .forEach(ext -> ext.configure(context)); + + beanManager.createInstance() + .select(GrpcMpExtension.class) + .stream() + .forEach(ext -> ext.configure(context)); + } + + /** + * Register the service with the routing. + *

+ * The service is actually a CDI proxy so the real service. + * + * @param service the service to register + * @param builder the gRPC routing + * @param beanManager the {@link BeanManager} to use to locate beans required by the service + */ + private void register(Object service, GrpcRouting.Builder builder, Class cls, BeanManager beanManager) { + GrpcServiceBuilder serviceBuilder = GrpcServiceBuilder.create(cls, () -> service, beanManager); + if (serviceBuilder.isAnnotatedService()) { + builder.service(serviceBuilder.build()); + } else { + LOGGER.log(Level.WARNING, + () -> "Discovered type is not a properly annotated gRPC service " + service.getClass()); + } + } + + /** + * A CDI producer that can supply the running {@link GrpcServer} + * an in-process {@link Channel}. + */ + @ApplicationScoped + public static class ServerProducer { + + private GrpcServer server; + + /** + * Produce the {@link GrpcServer}. + * + * @return the {@link GrpcServer} + */ + @Produces + public GrpcServer server() { + return server; + } + + /** + * Produce a {@link Supplier} that can supply the {@link GrpcServer}. + *

+ * This could be useful where an injection point has the server injected + * before the {@link #startServer} method has actually started it. In that + * case a {@link Supplier Supplier<GrpcServer>} can be injected instead + * that will be able to lazily supply the server. + * + * @return a {@link Supplier} that can supply the {@link GrpcServer} + */ + @Produces + public Supplier supply() { + return this::server; + } + + /** + * Produces an in-process {@link Channel} to connect to the + * running gRPC server. + * + * @return an in-process {@link Channel} to connect to the + * running gRPC server + */ + @Produces + @InProcessGrpcChannel + public Channel channel() { + String name = server.configuration().name(); + return InProcessChannelBuilder.forName(name) + .usePlaintext() + .build(); + } + + /** + * Produces an in-process {@link InProcessChannelBuilder} to + * connect to the running gRPC server. + * + * @return an in-process {@link InProcessChannelBuilder} to + * connect to the running gRPC server + */ + @Produces + @InProcessGrpcChannel + public InProcessChannelBuilder channelBuilder() { + String name = server.configuration().name(); + return InProcessChannelBuilder.forName(name); + } + + void server(GrpcServer server) { + this.server = server; + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java new file mode 100644 index 00000000000..6672e62797c --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java @@ -0,0 +1,356 @@ +/* + * 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.server; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.common.context.Context; +import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * The configuration for a gRPC server. + */ +public interface GrpcServerConfiguration { + /** + * The default server name. + */ + String DEFAULT_NAME = "grpc.server"; + + /** + * The default grpc port. + */ + int DEFAULT_PORT = 1408; + + /** + * The default number of worker threads that will be used if not explicitly set. + */ + int DEFAULT_WORKER_COUNT = Runtime.getRuntime().availableProcessors(); + + /** + * Get the server name. + * + * @return the server name + */ + String name(); + + /** + * Get the server port. + * + * @return the server port + */ + int port(); + + /** + * The top level {@link Context} to be used by the server. + * + * @return a context instance with registered application scoped instances + */ + Context context(); + + /** + * Determine whether use native transport if possible. + *

+ * If native transport support is enabled, gRPC server will use epoll on + * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will + * be used. + * + * @return {@code true} if native transport should be used + */ + boolean useNativeTransport(); + + /** + * Returns a count of threads in s pool used to process gRPC requests. + *

+ * Default value is {@code CPU_COUNT * 2}. + * + * @return a workers count + */ + int workers(); + + /** + * Returns a SslConfiguration to use with the server socket. If not {@code null} then + * the server enforces an SSL communication. + * + * @return a TLS configuration to use + */ + GrpcTlsDescriptor tlsConfig(); + + /** + * Returns the period for counting rapid resets (stream RST sent by client before any data have been sent by server). + * + * @return the period for counting rapid resets + */ + Duration rapidResetCheckPeriod(); + + /** + * Returns the maximum allowed number of rapid resets (stream RST sent by client before any data have been sent by server). + * When reached within {@link #rapidResetCheckPeriod()}, GOAWAY is sent to client and connection is closed. + * + * @return the maximum allowed number of rapid resets + */ + int maxRapidResets(); + + /** + * Creates new instance with default values for all configuration properties. + * + * @return a new instance + */ + static GrpcServerConfiguration create() { + return builder().build(); + } + + /** + * Creates new instance with values from external configuration. + * + * @param config the externalized configuration + * @return a new instance + */ + static GrpcServerConfiguration create(Config config) { + return builder(config).build(); + } + + /** + * Creates new instance of a {@link Builder server configuration builder}. + * + * @return a new builder instance + */ + static Builder builder() { + return new Builder(); + } + + /** + * Creates new instance of a {@link Builder server configuration builder} with defaults from external configuration source. + * + * @param config the externalized configuration + * @return a new builder instance + */ + static Builder builder(Config config) { + return new Builder().config(config); + } + + /** + * A {@link GrpcServerConfiguration} builder. + */ + @Configured + final class Builder implements io.helidon.common.Builder { + private static final AtomicInteger GRPC_SERVER_COUNTER = new AtomicInteger(1); + + private String name = DEFAULT_NAME; + + private int port = DEFAULT_PORT; + + private boolean useNativeTransport; + + private int workers; + + private GrpcTlsDescriptor tlsConfig = null; + + private Context context; + + private int maxRapidResets = 200; + + private Duration rapidResetCheckPeriod = Duration.ofSeconds(30); + + private Builder() { + } + + /** + * Update the builder from configuration. + * + * @param config configuration instance + * @return updated builder + */ + @ConfiguredOption(key = "native", + type = Boolean.class, + value = "false", + description = "Specify if native transport should be used.") + public Builder config(Config config) { + if (config == null) { + return this; + } + + name = config.get("name").asString().orElse(DEFAULT_NAME); + port = config.get("port").asInt().orElse(DEFAULT_PORT); + maxRapidResets = config.get("max-rapid-resets").asInt().orElse(200); + rapidResetCheckPeriod = config.get("rapid-reset-check-period").as(Duration.class).orElse(Duration.ofSeconds(30)); + useNativeTransport = config.get("native").asBoolean().orElse(false); + config.get("workers").asInt().ifPresent(this::workersCount); + + return this; + } + + /** + * Set the name of the gRPC server. + *

+ * Configuration key: {@code name} + * + * @param name the name of the gRPC server + * @return an updated builder + */ + @ConfiguredOption(key = "name", value = DEFAULT_NAME) + public Builder name(String name) { + this.name = name == null ? null : name.trim(); + return this; + } + + /** + * Sets server port. If port is {@code 0} or less then any available ephemeral port will be used. + *

+ * Configuration key: {@code port} + * + * @param port the server port + * @return an updated builder + */ + @ConfiguredOption(value = "" + DEFAULT_PORT) + public Builder port(int port) { + this.port = port < 0 ? 0 : port; + return this; + } + + /** + * Period for counting rapid resets(stream RST sent by client before any data have been sent by server). + * Default value is {@code PT30S}. + * + * @param rapidResetCheckPeriod duration + * @return updated builder + * @see ISO_8601 Durations + * @see #maxRapidResets() + */ + @ConfiguredOption("PT30S") + public Builder rapidResetCheckPeriod(Duration rapidResetCheckPeriod) { + Objects.requireNonNull(rapidResetCheckPeriod); + this.rapidResetCheckPeriod = rapidResetCheckPeriod; + return this; + } + + /** + * Maximum number of rapid resets(stream RST sent by client before any data have been sent by server). + * When reached within {@link #rapidResetCheckPeriod()}, GOAWAY is sent to client and connection is closed. + * Default value is {@code 200}. + * + * @param maxRapidResets maximum number of rapid resets + * @return updated builder + * @see #rapidResetCheckPeriod() + */ + @ConfiguredOption("200") + public Builder maxRapidResets(int maxRapidResets) { + this.maxRapidResets = maxRapidResets; + return this; + } + + /** + * Configure the application scoped context to be used as a parent for webserver request contexts. + * + * @param context top level context + * @return an updated builder + */ + public Builder context(Context context) { + this.context = context; + return this; + } + + /** + * Sets a count of threads in pool used to process HTTP requests. + * Default value is {@code CPU_COUNT * 2}. + *

+ * Configuration key: {@code workers} + * + * @param workers a workers count + * @return an updated builder + */ + @ConfiguredOption(key = "workers", value = "Number of processors available to the JVM") + public Builder workersCount(int workers) { + this.workers = workers; + return this; + } + + /** + * Configures TLS configuration to use with the server socket. If not {@code null} then + * the server enforces an TLS communication. + * + * @param tlsConfig a TLS configuration to use + * @return this builder + */ + public Builder tlsConfig(GrpcTlsDescriptor tlsConfig) { + this.tlsConfig = tlsConfig; + return this; + } + + String name() { + return name; + } + + int port() { + return port; + } + + /** + * Current Helidon {@link Context}. + * + * @return current context + */ + public Context context() { + return context; + } + + GrpcTlsDescriptor tlsConfig() { + return tlsConfig; + } + + boolean useNativeTransport() { + return useNativeTransport; + } + + int workers() { + return workers; + } + + int maxRapidResets() { + return maxRapidResets; + } + + Duration rapidResetCheckPeriod() { + return rapidResetCheckPeriod; + } + + @Override + public GrpcServerConfiguration build() { + if (name == null || name.isEmpty()) { + name = DEFAULT_NAME; + } + + if (port < 0) { + port = 0; + } + + if (context == null) { + context = Context.builder() + .id("grpc-" + GRPC_SERVER_COUNTER.getAndIncrement()) + .build(); + } + + if (workers <= 0) { + workers = DEFAULT_WORKER_COUNT; + } + + return GrpcServerBasicConfig.create(this); + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java new file mode 100644 index 00000000000..c0a88b852f5 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java @@ -0,0 +1,85 @@ +/* + * 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.server; + +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import io.helidon.common.context.Context; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.webserver.grpc.ServiceDescriptor; + +import io.grpc.ServerInterceptor; +import org.eclipse.microprofile.health.HealthCheck; + +class GrpcServerImpl implements GrpcServer { + + static GrpcServerImpl create() { + return new GrpcServerImpl(); + } + + static GrpcServerImpl create(GrpcServerConfiguration config) { + return new GrpcServerImpl(); // TODO + } + + @Override + public GrpcServerConfiguration configuration() { + return null; + } + + @Override + public Context context() { + return null; + } + + @Override + public CompletionStage start() { + return null; + } + + @Override + public CompletionStage whenShutdown() { + return null; + } + + @Override + public CompletionStage shutdown() { + return null; + } + + @Override + public HealthCheck[] healthChecks() { + return new HealthCheck[0]; + } + + @Override + public Map services() { + return null; + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + public int port() { + return 0; + } + + public void deploy(ServiceDescriptor service, WeightedBag interceptors) { + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilder.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilder.java new file mode 100644 index 00000000000..3ae7878429e --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilder.java @@ -0,0 +1,365 @@ +/* + * 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.server; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import io.helidon.common.Builder; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.grpc.core.ContextKeys; +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.GrpcInterceptor; +import io.helidon.microprofile.grpc.core.GrpcInterceptorBinding; +import io.helidon.microprofile.grpc.core.GrpcInterceptors; +import io.helidon.microprofile.grpc.core.GrpcMarshaller; +import io.helidon.microprofile.grpc.core.GrpcMethod; +import io.helidon.microprofile.grpc.core.Instance; +import io.helidon.microprofile.grpc.core.ModelHelper; +import io.helidon.webserver.grpc.MethodDescriptor; +import io.helidon.webserver.grpc.ServiceDescriptor; + +import io.grpc.ServerInterceptor; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.spi.BeanManager; + +/** + * A builder for constructing a {@link ServiceDescriptor} + * instances from an annotated POJOs. + */ +public class GrpcServiceBuilder + extends AbstractServiceBuilder + implements Builder { + + private static final Logger LOGGER = Logger.getLogger(GrpcServiceBuilder.class.getName()); + + private final BeanManager beanManager; + + /** + * Create a new introspection modeller for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @param instance the target instance to call gRPC handler methods on + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + * @throws java.lang.NullPointerException if the service or instance parameters are null + */ + private GrpcServiceBuilder(Class serviceClass, Supplier instance, BeanManager beanManager) { + super(serviceClass, instance); + this.beanManager = beanManager; + } + + /** + * Create a new introspection modeller for a given gRPC service. + * + * @param service the service to call gRPC handler methods on + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + * @return a {@link GrpcServiceBuilder} + * @throws java.lang.NullPointerException if the service is null + */ + public static GrpcServiceBuilder create(Object service, BeanManager beanManager) { + return new GrpcServiceBuilder(service.getClass(), Instance.singleton(service), beanManager); + } + + /** + * Create a new introspection modeller for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + * @return a {@link GrpcServiceBuilder} + * @throws java.lang.NullPointerException if the service class is null + */ + public static GrpcServiceBuilder create(Class serviceClass, BeanManager beanManager) { + return new GrpcServiceBuilder(Objects.requireNonNull(serviceClass), createInstanceSupplier(serviceClass), beanManager); + } + + /** + * Create a {@link GrpcServiceBuilder} for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @param instance the target instance to call gRPC handler methods on + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + * @return a {@link GrpcServiceBuilder} + * @throws java.lang.NullPointerException if the service or instance parameters are null + */ + public static GrpcServiceBuilder create(Class serviceClass, Supplier instance, BeanManager beanManager) { + return new GrpcServiceBuilder(serviceClass, instance, beanManager); + } + + /** + * Create a {@link ServiceDescriptor.Builder} introspected class. + * + * @return a {@link ServiceDescriptor.Builder} for the introspected class. + */ + @Override + public ServiceDescriptor build() { + checkForNonPublicMethodIssues(); + + Class annotatedServiceClass = annotatedServiceClass(); + AnnotatedMethodList methodList = AnnotatedMethodList.create(annotatedServiceClass); + String name = determineServiceName(annotatedServiceClass); + + ServiceDescriptor.Builder builder = ServiceDescriptor.builder(serviceClass(), name) + .marshallerSupplier(getMarshallerSupplier()); + + addServiceMethods(builder, methodList, beanManager); + configureServiceInterceptors(builder, beanManager); + + Class serviceClass = serviceClass(); + Class annotatedClass = annotatedServiceClass(); + HelidonServiceLoader.create(ServiceLoader.load(AnnotatedServiceConfigurer.class)) + .forEach(configurer -> configurer.accept(serviceClass, annotatedClass, builder)); + + return builder.build(); + } + + /** + * Add methods to the {@link ServiceDescriptor.Builder}. + * + * @param builder the {@link ServiceDescriptor.Builder} to add the method to + * @param methodList the list of methods to add + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + */ + private void addServiceMethods(ServiceDescriptor.Builder builder, AnnotatedMethodList methodList, BeanManager beanManager) { + for (AnnotatedMethod am : methodList.withAnnotation(GrpcMethod.class)) { + addServiceMethod(builder, am, beanManager); + } + for (AnnotatedMethod am : methodList.withMetaAnnotation(GrpcMethod.class)) { + addServiceMethod(builder, am, beanManager); + } + } + + /** + * Add a method to the {@link ServiceDescriptor.Builder}. + *

+ * The method configuration will be determined by the annotations present on the + * method and the method signature. + * + * @param builder the {@link ServiceDescriptor.Builder} to add the method to + * @param method the {@link AnnotatedMethod} representing the method to add + * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use + * to look-up CDI beans. + */ + @SuppressWarnings("unchecked") + private void addServiceMethod(ServiceDescriptor.Builder builder, AnnotatedMethod method, BeanManager beanManager) { + GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(GrpcMethod.class); + String name = determineMethodName(method, annotation); + Supplier instanceSupplier = instanceSupplier(); + + 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(); + List interceptors = lookupMethodInterceptors(beanManager, method); + + GrpcInterceptors grpcInterceptors = method.getAnnotation(GrpcInterceptors.class); + + if (grpcInterceptors != null) { + for (Class interceptorClass : grpcInterceptors.value()) { + ServerInterceptor interceptor = resolveInterceptor(beanManager, interceptorClass); + if (interceptor != null) { + interceptors.add(interceptor); + } + } + } + + AnnotatedMethodConfigurer configurer = new AnnotatedMethodConfigurer(method, + requestType, + responseType, + interceptors); + + switch (annotation.type()) { + case UNARY: + builder.unary(name, handler, configurer); + break; + case CLIENT_STREAMING: + builder.clientStreaming(name, handler, configurer); + break; + case SERVER_STREAMING: + builder.serverStreaming(name, handler, configurer); + break; + case BIDI_STREAMING: + builder.bidirectional(name, handler, configurer); + break; + case UNKNOWN: + default: + LOGGER.log(Level.SEVERE, () -> "Unrecognized method type " + annotation.type()); + } + } + + private void configureServiceInterceptors(ServiceDescriptor.Builder builder, BeanManager beanManager) { + if (beanManager != null) { + Class serviceClass = serviceClass(); + Class annotatedClass = annotatedServiceClass(); + + configureServiceInterceptors(builder, beanManager, serviceClass()); + + if (!serviceClass.equals(annotatedClass)) { + configureServiceInterceptors(builder, beanManager, annotatedServiceClass()); + } + } + } + + private void configureServiceInterceptors(ServiceDescriptor.Builder builder, BeanManager beanManager, Class cls) { + if (beanManager != null) { + for (Annotation annotation : cls.getAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(GrpcInterceptorBinding.class)) { + builder.intercept(lookupInterceptor(annotation, beanManager)); + } + } + + GrpcInterceptors grpcInterceptors = cls.getAnnotation(GrpcInterceptors.class); + if (grpcInterceptors != null) { + for (Class interceptorClass : grpcInterceptors.value()) { + if (ServerInterceptor.class.isAssignableFrom(interceptorClass)) { + ServerInterceptor interceptor = resolveInterceptor(beanManager, interceptorClass); + if (interceptor != null) { + builder.intercept(interceptor); + } + } + } + } + } + } + + private List lookupMethodInterceptors(BeanManager beanManager, AnnotatedMethod method) { + if (beanManager == null) { + return Collections.emptyList(); + } + + List interceptors = new ArrayList<>(); + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(GrpcInterceptorBinding.class)) { + interceptors.add(lookupInterceptor(annotation, beanManager)); + } + } + return interceptors; + } + + private ServerInterceptor lookupInterceptor(Annotation annotation, BeanManager beanManager) { + jakarta.enterprise.inject.Instance instance; + instance = beanManager.createInstance() + .select(ServerInterceptor.class, GrpcInterceptor.Literal.INSTANCE); + + List interceptors = instance.stream() + .filter(interceptor -> hasAnnotation(interceptor, annotation)) + .collect(Collectors.toList()); + + if (interceptors.size() == 1) { + return interceptors.get(0); + } else if (interceptors.size() > 1) { + throw new IllegalStateException("gRPC interceptor annotation" + + "resolves to ambiguous interceptor implementations " + + annotation); + } else { + throw new IllegalStateException("Cannot resolve a gRPC interceptor bean for annotation" + + annotation); + } + } + + private ServerInterceptor resolveInterceptor(BeanManager beanManager, Class cls) { + if (beanManager == null) { + return null; + } + + if (ServerInterceptor.class.isAssignableFrom(cls)) { + jakarta.enterprise.inject.Instance instance = beanManager.createInstance().select(cls, Any.Literal.INSTANCE); + if (instance.isResolvable()) { + return (ServerInterceptor) instance.get(); + } else { + try { + return (ServerInterceptor) cls.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException("Cannot create gRPC interceptor", e); + } + } + } else { + throw new IllegalArgumentException("Specified interceptor class " + cls + " is not a " + ServerInterceptor.class); + } + } + + private boolean hasAnnotation(ServerInterceptor interceptor, Annotation annotation) { + Annotation[] annotations = getClass(interceptor).getAnnotations(); + return Arrays.asList(annotations).contains(annotation); + } + + private Class getClass(Object o) { + Class cls = o.getClass(); + while (cls.isSynthetic()) { + cls = cls.getSuperclass(); + } + return cls; + } + + /** + * A {@link Consumer} of {@link MethodDescriptor.Rules} that + * applies configuration changes based on annotations present + * on the gRPC method. + */ + private static class AnnotatedMethodConfigurer + implements MethodDescriptor.Configurer { + + private final AnnotatedMethod method; + private final Class requestType; + private final Class responseType; + private final List interceptors; + + private AnnotatedMethodConfigurer(AnnotatedMethod method, + Class requestType, + Class responseType, + List interceptors) { + this.method = method; + this.requestType = requestType; + this.responseType = responseType; + this.interceptors = interceptors; + } + + @Override + public void configure(MethodDescriptor.Rules rules) { + rules.addContextValue(ContextKeys.SERVICE_METHOD, method.declaredMethod()) + .requestType(requestType) + .responseType(responseType); + + if (method.isAnnotationPresent(GrpcMarshaller.class)) { + rules.marshallerSupplier(ModelHelper.getMarshallerSupplier(method.getAnnotation(GrpcMarshaller.class))); + } + + interceptors.forEach(rules::intercept); + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java new file mode 100644 index 00000000000..d75c0ce05e8 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java @@ -0,0 +1,219 @@ +/* + * 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.server; + +import io.helidon.common.configurable.Resource; +import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.config.objectmapping.Value; + +/** + * GrpcTlsDescriptor contains details about configuring TLS of a {@link io.grpc.Channel}. + */ +public class GrpcTlsDescriptor { + private final boolean enabled; + private final boolean jdkSSL; + private final Resource tlsCert; + private final Resource tlsKey; + private final Resource tlsCaCert; + + private GrpcTlsDescriptor(boolean enabled, boolean jdkSSL, Resource tlsCert, Resource tlsKey, Resource tlsCaCert) { + this.enabled = enabled; + this.jdkSSL = jdkSSL; + this.tlsCert = tlsCert; + this.tlsKey = tlsKey; + this.tlsCaCert = tlsCaCert; + } + + /** + * Return a new instance of {@link Builder}. + * + * @return a new instance of {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Return an instance of builder based on the specified external config. + * + * @param config external config + * @return an instance of builder + */ + public static Builder builder(Config config) { + return new Builder(config); + } + + /** + * Create an instance of a TLS configuration from external configuration source. + * + * @param config external config + * @return an instance of a TLS configuration + */ + public static GrpcTlsDescriptor create(Config config) { + return builder(config).build(); + } + + /** + * Check if TLS is enabled. If this is false, then none of the other configuration values are used. + * + * @return true if TLS is enabled; false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Check if JDK SSL has be used. Only used for TLS enabled server channels.A Ignored by client channel. + * + * @return true if JDK ssl has to be used; false otherwise + */ + public boolean isJdkSSL() { + return jdkSSL; + } + + /** + * Get the tlsCert path. Can be either client or server cert. + * + * @return the path to tls certificate + */ + public Resource tlsCert() { + return tlsCert; + } + + /** + * Get the client private key path. Can be either client or server private key. + * + * @return the path to tls private key + */ + public Resource tlsKey() { + return tlsKey; + } + + /** + * Get the CA (certificate authority) certificate path. + * + * @return the path to CA certificate + */ + public Resource tlsCaCert() { + return tlsCaCert; + } + + /** + * Builder to build a new instance of {@link GrpcTlsDescriptor}. + */ + @Configured + public static class Builder implements io.helidon.common.Builder { + + private boolean enabled = true; + private boolean jdkSSL; + private Resource tlsCert; + private Resource tlsKey; + private Resource tlsCaCert; + + private Builder() { + + } + + private Builder(Config config) { + if (config == null) { + return; + } + + config.get("tls-cert.resource").as(Resource::create).ifPresent(this::tlsCert); + config.get("tls-key.resource").as(Resource::create).ifPresent(this::tlsKey); + config.get("tls-ca-cert.resource").as(Resource::create).ifPresent(this::tlsCaCert); + + this.jdkSSL = config.get("jdk-ssl").asBoolean().orElse(false); + this.enabled = config.get("enabled").asBoolean().orElse(true); + } + + /** + * Enable or disable TLS. If enabled is false, then the rest of the TLS configuration properties are ignored. + * + * @param enabled true to enable, false otherwise + * @return this instance for fluent API + */ + @ConfiguredOption(value = "true") + @Value(withDefault = "true") + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Sets the type of SSL implementation to be used. + * + * @param jdkSSL true to use JDK based SSL, false otherwise + * @return this instance for fluent API + */ + @ConfiguredOption(key = "jdk-ssl", value = "false") + public Builder jdkSSL(boolean jdkSSL) { + this.jdkSSL = jdkSSL; + return this; + } + + /** + * Set the client tlsCert path. Required only if mutual auth is desired. + * + * @param tlsCert the path to client's certificate + * @return this instance for fluent API + */ + @ConfiguredOption + @Value(key = "tls-cert") + public Builder tlsCert(Resource tlsCert) { + this.tlsCert = tlsCert; + return this; + } + + /** + * Set the client private key path. Required only if mutual auth is desired. + * + * @param tlsKey the 's TLS private key + * @return this instance for fluent API + */ + @ConfiguredOption + @Value(key = "tls-key") + public Builder tlsKey(Resource tlsKey) { + this.tlsKey = tlsKey; + return this; + } + + /** + * Set the CA (certificate authority) certificate path. + * + * @param caCert the path to CA certificate + * @return this instance for fluent API + */ + @ConfiguredOption(key = "tls-ca-cert") + @Value(key = "tls-ca-cert") + public Builder tlsCaCert(Resource caCert) { + this.tlsCaCert = caCert; + return this; + } + + /** + * Create and return a new instance of {@link GrpcTlsDescriptor}. + * + * @return a new instance of {@link GrpcTlsDescriptor} + */ + public GrpcTlsDescriptor build() { + return new GrpcTlsDescriptor(enabled, jdkSSL, tlsCert, tlsKey, tlsCaCert); + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/package-info.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/package-info.java new file mode 100644 index 00000000000..fbcd798127a --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Microprofile 1.0 gRPC server implementation. + */ +package io.helidon.microprofile.grpc.server; diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java new file mode 100644 index 00000000000..13b48d70123 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java @@ -0,0 +1,76 @@ +/* + * 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.server.spi; + +import java.util.concurrent.CompletionStage; + +import io.helidon.config.Config; +import io.helidon.microprofile.grpc.server.GrpcServer; +import io.helidon.microprofile.grpc.server.GrpcServerConfiguration; +import io.helidon.webserver.grpc.GrpcRouting; + +import jakarta.enterprise.inject.spi.BeanManager; + +/** + * A context to allow a microprofile gRPC server extensions to configure additional + * services or components for the gRPC server or use the CDI bean manager. + */ +public interface GrpcMpContext { + + /** + * Obtain the Helidon configuration. + * + * @return the Helidon configuration + */ + Config config(); + + /** + * Obtain the {@link GrpcServerConfiguration}. + * + * @return the {@link GrpcServerConfiguration} + */ + GrpcServerConfiguration.Builder grpcServerConfiguration(); + + /** + * Obtain the {@link GrpcRouting.Builder} to allow modifications + * to be made to the routing before the server is configured. + * + * @return the {@link GrpcRouting.Builder} + */ + GrpcRouting.Builder routing(); + + /** + * Obtain the {@link jakarta.enterprise.inject.spi.BeanManager}. + * + * @return the {@link jakarta.enterprise.inject.spi.BeanManager} + */ + BeanManager beanManager(); + + /** + * Return a completion stage is completed when the gRPC server is started. + * + * @return a completion stage is completed when the gRPC server is started + */ + CompletionStage whenStarted(); + + /** + * Return a completion stage is completed when the gRPC server is shut down. + * + * @return a completion stage is completed when the gRPC server is shut down + */ + CompletionStage whenShutdown(); +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java new file mode 100644 index 00000000000..cf44c3252ed --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java @@ -0,0 +1,29 @@ +/* + * 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.server.spi; + +/** + * Microprofile service to extend features of the gRPC server. + */ +public interface GrpcMpExtension { + /** + * Allow the service to add configuration through the context. + * + * @param context context to obtain configuration objects + */ + void configure(GrpcMpContext context); +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java new file mode 100644 index 00000000000..5cf2f30af3d --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Microprofile gRPC server implementation. + */ +package io.helidon.microprofile.grpc.server.spi; diff --git a/microprofile/grpc/server/src/main/java/module-info.java b/microprofile/grpc/server/src/main/java/module-info.java new file mode 100644 index 00000000000..298e1ad1c3b --- /dev/null +++ b/microprofile/grpc/server/src/main/java/module-info.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +/** + * gRPC microprofile server module + */ +module io.helidon.microprofile.grpc.server { + exports io.helidon.microprofile.grpc.server; + exports io.helidon.microprofile.grpc.server.spi; + + requires transitive io.helidon.webserver.grpc; + requires transitive io.helidon.microprofile.grpc.core; + + requires io.helidon.common; + requires io.helidon.grpc.core; + requires io.helidon.microprofile.server; + requires io.helidon.config.mp; + requires io.helidon.config.objectmapping; + + requires io.grpc; + requires io.grpc.inprocess; + requires io.grpc.protobuf.lite; + requires com.google.protobuf; + + requires java.logging; + + requires microprofile.health.api; + requires io.helidon.common.configurable; + requires io.helidon.config; + requires io.helidon.config.metadata; + requires io.helidon.common.context; + + uses io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; + uses io.helidon.microprofile.grpc.server.GrpcServerCdiExtension; + uses io.helidon.microprofile.grpc.server.AnnotatedServiceConfigurer; + + provides jakarta.enterprise.inject.spi.Extension + with io.helidon.microprofile.grpc.server.GrpcServerCdiExtension; + + // needed when running with modules - to make private methods accessible + opens io.helidon.microprofile.grpc.server to weld.core.impl, io.helidon.microprofile.cdi; +} diff --git a/microprofile/grpc/server/src/main/resources/META-INF/beans.xml b/microprofile/grpc/server/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..a0938bff7d4 --- /dev/null +++ b/microprofile/grpc/server/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java new file mode 100644 index 00000000000..66216e01dd8 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java @@ -0,0 +1,332 @@ +/* + * 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.server; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.helidon.webserver.grpc.MethodDescriptor; +import io.helidon.webserver.grpc.ServiceDescriptor; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.GrpcMarshaller; +import io.helidon.microprofile.grpc.core.GrpcMethod; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.stub.StreamObserver; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Disabled +public class GrpcServiceBuilderTest { + + private BeanManager beanManager; + + @BeforeEach + public void setup() { + beanManager = mock(BeanManager.class); + Instance instance = mock(Instance.class); + + when(beanManager.createInstance()).thenReturn(instance); + } + + @Test + public void shouldUseServiceNameFromAnnotation() { + ServiceOne service = new ServiceOne(); + GrpcServiceBuilder modeller = GrpcServiceBuilder.create(service, beanManager); + ServiceDescriptor descriptor = modeller.build(); + + assertThat(descriptor.name(), is("ServiceOne/foo")); + } + + @Test + public void shouldCreateDescriptorFoServiceWithNestedGenericParameters() { + GrpcServiceBuilder modeller = GrpcServiceBuilder.create(ServiceSix.class, beanManager); + ServiceDescriptor descriptor = modeller.build(); + assertThat(descriptor.name(), is(ServiceSix.class.getSimpleName())); + } + + @Test + public void shouldUseDefaultServiceName() { + ServiceTwo service = new ServiceTwo(); + GrpcServiceBuilder modeller = GrpcServiceBuilder.create(service, beanManager); + ServiceDescriptor descriptor = modeller.build(); + + assertThat(descriptor.name(), is("ServiceTwo")); + } + + @Test + public void shouldCreateServiceFromInstance() { + ServiceOne service = new ServiceOne(); + assertServiceOne(GrpcServiceBuilder.create(service, beanManager)); + } + + @Test + public void shouldCreateServiceFromClass() { + assertServiceOne(GrpcServiceBuilder.create(ServiceOne.class, beanManager)); + } + + @Test + public void shouldCreateServiceFromClassWithoutBeanManager() { + assertServiceOne(GrpcServiceBuilder.create(ServiceOne.class, null)); + } + + public void assertServiceOne(GrpcServiceBuilder builder) { + ServiceDescriptor descriptor = builder.build(); + assertThat(descriptor.name(), is("ServiceOne/foo")); + assertThat(descriptor.methods().size(), is(4)); + + MethodDescriptor methodDescriptor; + io.grpc.MethodDescriptor grpcDescriptor; + + methodDescriptor = descriptor.method("unary"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("unary")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceOne/foo/unary")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.UNARY)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("clientStreaming"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("clientStreaming")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceOne/foo/clientStreaming")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("serverStreaming"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("serverStreaming")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceOne/foo/serverStreaming")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("bidiStreaming"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("bidiStreaming")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceOne/foo/bidiStreaming")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + } + + @Test + public void shouldCreateServiceWithMethodNamesFromAnnotation() { + ServiceTwo service = new ServiceTwo(); + GrpcServiceBuilder builder = GrpcServiceBuilder.create(service, beanManager); + + ServiceDescriptor descriptor = builder.build(); + assertThat(descriptor.name(), is("ServiceTwo")); + assertThat(descriptor.methods().size(), is(4)); + + MethodDescriptor methodDescriptor; + io.grpc.MethodDescriptor grpcDescriptor; + + methodDescriptor = descriptor.method("One"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("One")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceTwo/One")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.UNARY)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("Two"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("Two")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceTwo/Two")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("Three"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("Three")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceTwo/Three")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + + methodDescriptor = descriptor.method("Four"); + assertThat(methodDescriptor, is(notNullValue())); + assertThat(methodDescriptor.name(), is("Four")); + assertThat(methodDescriptor.callHandler(), is(notNullValue())); + + grpcDescriptor = methodDescriptor.descriptor(); + assertThat(grpcDescriptor, is(notNullValue())); + assertThat(grpcDescriptor.getFullMethodName(), is("ServiceTwo/Four")); + assertThat(grpcDescriptor.getType(), is(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(grpcDescriptor.getRequestMarshaller(), is(instanceOf(StubMarshaller.class))); + assertThat(grpcDescriptor.getResponseMarshaller(), is(instanceOf(StubMarshaller.class))); + } + + @SuppressWarnings("unchecked") + public void assertSingleton(GrpcServiceBuilder builder) { + ServiceDescriptor descriptor = builder.build(); + + MethodDescriptor methodDescriptor = descriptor.method("unary"); + ServerCallHandler handler = methodDescriptor.callHandler(); + + ServerCall callOne = mock(ServerCall.class); + ServerCall callTwo = mock(ServerCall.class); + when(callOne.getMethodDescriptor()).thenReturn(methodDescriptor.descriptor()); + when(callTwo.getMethodDescriptor()).thenReturn(methodDescriptor.descriptor()); + + ServerCall.Listener listenerOne = handler.startCall(callOne, new Metadata()); + ServerCall.Listener listenerTwo = handler.startCall(callTwo, new Metadata()); + + listenerOne.onMessage("foo"); + listenerOne.onHalfClose(); + listenerTwo.onMessage("foo"); + listenerTwo.onHalfClose(); + + ArgumentCaptor captorOne = ArgumentCaptor.forClass(ServiceFive.class); + ArgumentCaptor captorTwo = ArgumentCaptor.forClass(ServiceFive.class); + + verify(callOne).sendMessage(captorOne.capture()); + verify(callTwo).sendMessage(captorTwo.capture()); + + assertThat(captorOne.getValue(), is(sameInstance(captorTwo.getValue()))); + } + + @Grpc(name = "ServiceOne/foo") + @GrpcMarshaller("stub") + public static class ServiceOne { + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void unary(String param, StreamObserver observer) { + } + + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING) + public StreamObserver clientStreaming(StreamObserver observer) { + return null; + } + + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + public void serverStreaming(String param, StreamObserver observer) { + } + + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public StreamObserver bidiStreaming(StreamObserver observer) { + return null; + } + } + + @Grpc + @GrpcMarshaller("stub") + public static class ServiceTwo { + @GrpcMethod(name = "One", type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void unary(String param, StreamObserver observer) { + } + + @GrpcMethod(name = "Two", type = io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING) + public StreamObserver clientStreaming(StreamObserver observer) { + return null; + } + + @GrpcMethod(name = "Three", type = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + public void serverStreaming(String param, StreamObserver observer) { + } + + @GrpcMethod(name = "Four", type = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public StreamObserver bidiStreaming(StreamObserver observer) { + return null; + } + } + + @Grpc + @GrpcMarshaller("stub") + public static class ServiceThree { + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void unary(String param, StreamObserver observer) { + } + } + + @Grpc + @GrpcMarshaller("stub") + public static class ServiceFour { + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + @GrpcMarshaller("stub") + public void unary(String param, StreamObserver observer) { + } + } + + @Grpc + @GrpcMarshaller("stub") + @Singleton + public static class ServiceFive { + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + @GrpcMarshaller("stub") + public void unary(String param, StreamObserver observer) { + observer.onNext(this); + observer.onCompleted(); + } + } + + @Grpc + @GrpcMarshaller("stub") + public static class ServiceSix { + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public List> unary(List> param) { + return Collections.singletonList(Collections.singletonMap(1, "One")); + } + } +} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java new file mode 100644 index 00000000000..43a4c6c572e --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.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 io.helidon.microprofile.grpc.server; + +import java.io.InputStream; + +import io.helidon.grpc.core.MarshallerSupplier; + +import io.grpc.MethodDescriptor; +import jakarta.inject.Named; + +/** + * A stub {@link io.grpc.MethodDescriptor.Marshaller}. + *

+ * This marshaller will not actually work and should not + * be used as a real marshaller. + */ +public class StubMarshaller + implements MethodDescriptor.Marshaller { + + @Override + public InputStream stream(T value) { + return null; + } + + @Override + public T parse(InputStream stream) { + return null; + } + + @Named("stub") + public static class Supplier + implements MarshallerSupplier { + @Override + public MethodDescriptor.Marshaller get(Class clazz) { + return new StubMarshaller<>(); + } + } +} diff --git a/microprofile/grpc/server/src/test/proto/services.proto b/microprofile/grpc/server/src/test/proto/services.proto new file mode 100644 index 00000000000..185dce004e3 --- /dev/null +++ b/microprofile/grpc/server/src/test/proto/services.proto @@ -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. + */ + +syntax = "proto3"; +option java_package = "io.helidon.microprofile.grpc.server.test"; + +service UnaryService { + rpc requestResponse (TestRequest) returns (TestResponse) {} + rpc responseOnly (Empty) returns (TestResponse) {} + rpc requestNoResponse (TestRequest) returns (Empty) {} + rpc noRequestNoResponse (Empty) returns (Empty) {} + rpc futureResponse (TestRequest) returns (TestResponse) {} + rpc futureResponseNoRequest (Empty) returns (TestResponse) {} + rpc unary (TestRequest) returns (TestResponse) {} + rpc unaryNoRequest (Empty) returns (TestResponse) {} + rpc unaryFuture (TestRequest) returns (TestResponse) {} + rpc unaryFutureNoRequest (Empty) returns (TestResponse) {} +} + +service ServerStreamingService { + rpc streaming (TestRequest) returns (stream TestResponse) {} + rpc stream (TestRequest) returns (stream TestResponse) {} + rpc streamingNoRequest (Empty) returns (stream TestResponse) {} + rpc streamNoRequest (Empty) returns (stream TestResponse) {} +} + +service ClientStreamingService { + rpc streaming (stream TestRequest) returns (TestResponse) {} + rpc futureResponse (stream TestRequest) returns (TestResponse) {} +} + +service BidiService { + rpc bidi (stream TestRequest) returns (stream TestResponse) {} +} + +message Empty { +} + +message TestRequest { + string message = 1; +} + +message TestResponse { + string message = 1; +} diff --git a/microprofile/grpc/server/src/test/resources/META-INF/beans.xml b/microprofile/grpc/server/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..dcbcc918faf --- /dev/null +++ b/microprofile/grpc/server/src/test/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier new file mode 100644 index 00000000000..5aa70cd2a64 --- /dev/null +++ b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier @@ -0,0 +1,17 @@ +# +# 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. +# + +io.helidon.microprofile.grpc.server.StubMarshaller$Supplier diff --git a/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension new file mode 100644 index 00000000000..b1e27c19138 --- /dev/null +++ b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension @@ -0,0 +1,17 @@ +# +# 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. +# + +io.helidon.microprofile.grpc.server.GrpcServerCdiExtensionIT$ExtensionTwo \ No newline at end of file diff --git a/microprofile/grpc/server/src/test/resources/logging.properties b/microprofile/grpc/server/src/test/resources/logging.properties new file mode 100644 index 00000000000..ab333c926fe --- /dev/null +++ b/microprofile/grpc/server/src/test/resources/logging.properties @@ -0,0 +1,34 @@ +# +# 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. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO +#io.netty.level=INFO diff --git a/webserver/grpc/pom.xml b/webserver/grpc/pom.xml index b20c32c4cd7..7ff8a3c06fc 100644 --- a/webserver/grpc/pom.xml +++ b/webserver/grpc/pom.xml @@ -43,6 +43,10 @@ io.helidon.webserver helidon-webserver-http2 + + io.helidon.grpc + helidon-grpc-core + io.grpc grpc-core diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index 24fa0ce724c..e39d914015d 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -17,18 +17,23 @@ package io.helidon.webserver.grpc; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; - -import io.helidon.http.HttpPrologue; -import io.helidon.http.PathMatchers; -import io.helidon.webserver.Routing; +import java.util.Map; import com.google.protobuf.Descriptors; import io.grpc.BindableService; +import io.grpc.ServerInterceptor; import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; import io.grpc.stub.ServerCalls; +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.http.HttpPrologue; +import io.helidon.http.PathMatchers; +import io.helidon.webserver.Routing; /** * GRPC specific routing. @@ -37,9 +42,13 @@ public class GrpcRouting implements Routing { private static final GrpcRouting EMPTY = GrpcRouting.builder().build(); private final ArrayList routes; + private final WeightedBag interceptors; + private final ArrayList services; private GrpcRouting(Builder builder) { this.routes = new ArrayList<>(builder.routes); + this.interceptors = builder.interceptors; + this.services = new ArrayList<>(builder.services.values()); } @Override @@ -79,6 +88,26 @@ public void afterStop() { } } + /** + * Weighted bag of server interceptors associated with routing. + * + * @return weighted bag of server interceptors + */ + public WeightedBag interceptors() { + return interceptors; + } + + /** + * Obtain a {@link List} of the {@link ServiceDescriptor} instances + * contained in this {@link GrpcRouting}. + * + * @return a {@link List} of the {@link ServiceDescriptor} instances + * contained in this {@link GrpcRouting} + */ + public List services() { + return services; + } + Grpc findRoute(HttpPrologue prologue) { for (GrpcRoute route : routes) { PathMatchers.MatchResult accepts = route.accepts(prologue); @@ -95,6 +124,8 @@ public void afterStop() { */ public static class Builder implements io.helidon.common.Builder { private final List routes = new LinkedList<>(); + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private final Map services = new LinkedHashMap<>(); private Builder() { } @@ -114,6 +145,63 @@ public Builder service(GrpcService service) { return route(GrpcServiceRoute.create(service)); } + /** + * Configure a bindable service. + * + * @param service service to add + * @return updated builder + */ + public Builder service(BindableService service) { + throw new UnsupportedOperationException("Not implemented"); // TODO + } + + /** + * Configure a service using a {@link io.grpc.ServiceDescriptor}. + * + * @param service service to add + * @return updated builder + */ + public Builder service(ServiceDescriptor service) { + String name = service.name(); + if (services.containsKey(name)) { + throw new IllegalArgumentException("Attempted to register service name " + name + " multiple times"); + } + services.put(name, service); + return this; + } + + /** + * Add one or more global {@link ServerInterceptor} instances that will intercept calls + * to all services in the {@link GrpcRouting} built by this builder. + *

+ * If the added interceptors are annotated with the {@link io.helidon.common.Weight} + * or if they implemented the {@link io.helidon.common.Weighted} interface, + * that value will be used to assign a weight to use when applying the interceptor + * otherwise a weight of {@link InterceptorWeights#USER} will be used. + * + * @param interceptors one or more global {@link ServerInterceptor}s + * @return this builder to allow fluent method chaining + */ + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + /** + * Add one or more global {@link ServerInterceptor} instances that will intercept calls + * to all services in the {@link GrpcRouting} built by this builder. + *

+ * The added interceptors will be applied using the specified weight. + * + * @param weight the weight to assign to the interceptors + * @param interceptors one or more global {@link ServerInterceptor}s + * @return this builder to allow fluent method chaining + */ + public Builder intercept(int weight, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), weight); + return this; + } + /** * Unary route. * @@ -129,7 +217,6 @@ public Builder unary(Descriptors.FileDescriptor proto, String serviceName, String methodName, ServerCalls.UnaryMethod method) { - return route(Grpc.unary(proto, serviceName, methodName, method)); } @@ -166,7 +253,6 @@ public Builder serverStream(Descriptors.FileDescriptor proto, String serviceName, String methodName, ServerCalls.ServerStreamingMethod method) { - return route(Grpc.serverStream(proto, serviceName, methodName, method)); } @@ -185,7 +271,6 @@ public Builder clientStream(Descriptors.FileDescriptor proto, String serviceName, String methodName, ServerCalls.ClientStreamingMethod method) { - return route(Grpc.clientStream(proto, serviceName, methodName, method)); } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java new file mode 100644 index 00000000000..8660e65d1ab --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java @@ -0,0 +1,345 @@ +/* + * 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.webserver.grpc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.WeightedBag; + +import io.grpc.Context; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * Encapsulates all metadata necessary to define a gRPC method. + * + * @param request type + * @param response type + */ +public class MethodDescriptor { + private final String name; + private final io.grpc.MethodDescriptor descriptor; + private final ServerCallHandler callHandler; + private final Map, Object> context; + private final WeightedBag interceptors; + + private MethodDescriptor(String name, + io.grpc.MethodDescriptor descriptor, + ServerCallHandler callHandler, + Map, Object> context, + WeightedBag interceptors) { + this.name = name; + this.descriptor = descriptor; + this.callHandler = callHandler; + this.context = context; + this.interceptors = interceptors.copyMe(); + } + + /** + * Return the name of the method. + * + * @return method name + */ + public String name() { + return name; + } + + /** + * Return gRPC method descriptor. + * + * @return gRPC method descriptor + */ + public io.grpc.MethodDescriptor descriptor() { + return descriptor; + } + + /** + * Return the call handler. + * + * @return call handler + */ + public ServerCallHandler callHandler() { + return callHandler; + } + + /** + * Obtain the {@link Map} of {@link Context.Key}s and values to add to the + * call context when this method is invoked. + * + * @return an unmodifiable {@link Map} of {@link Context.Key}s and values to + * add to the call context when this method is invoked + */ + public Map, Object> context() { + return Collections.unmodifiableMap(context); + } + + /** + * Obtain the {@link io.grpc.ServerInterceptor}s to use for this method. + * + * @return the {@link io.grpc.ServerInterceptor}s to use for this method + */ + public WeightedBag interceptors() { + return interceptors.readOnly(); + } + + static Builder builder(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + return new Builder<>(serviceName, name, descriptor, callHandler); + } + + static MethodDescriptor create(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + return builder(serviceName, name, descriptor, callHandler).build(); + } + + @Override + public String toString() { + String fullName = descriptor == null ? null : descriptor.getFullMethodName(); + return "MethodDescriptor(" + + "name='" + name + '\'' + + "fullName='" + fullName + "\')"; + } + + /** + * Method configuration API. + * + * @param request type + * @param response type + */ + public interface Rules { + /** + * Add a {@link Context.Key} and value to be added to the call {@link io.grpc.Context} + * when this method is invoked. + * + * @param key the {@link Context.Key} to add + * @param value the value to map to the {@link Context.Key} + * @param the type of the {@link Context.Key} and value + * @return this {@link Rules} instance for fluent call chaining + * @throws NullPointerException if the key parameter is null + */ + + Rules addContextValue(Context.Key key, T value); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for the method. + * + * @param interceptors one or more {@link ServerInterceptor}s to register + * @return this builder to allow fluent method chaining + */ + Rules intercept(ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor 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 ServerInterceptor}s to register + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ServerInterceptor... 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 ServiceDescriptor.Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Set the request type. + *

+ * Setting the request type is optional as it is used to obtain the + * correct marshaller so if the marshaller supplier being used is type + * agnostic, such as Java serialization then whereas some marshallers + * such as Protocol Buffers require a type. + * + * @param requestType the type of the request message + * @param the type of the request message + * @return this {@link ServiceDescriptor.Rules} instance + * for fluent call chaining + */ + Rules requestType(Class requestType); + + /** + * Set the response type. + *

+ * Setting the response type is optional as it is used to obtain the + * correct marshaller so if the marshaller supplier being used is type + * agnostic, such as Java serialization then whereas some marshallers + * such as Protocol Buffers require a type. + * + * @param responseType the type of the request message + * @param the type of the request message + * @return this {@link ServiceDescriptor.Rules} instance + * for fluent call chaining + */ + Rules responseType(Class responseType); + } + + /** + * An interface implemented by classes that can configure + * a {@link Rules}. + * + * @param request type + * @param response type + */ + @FunctionalInterface + public interface Configurer { + /** + * Apply extra configuration to a {@link Rules}. + * + * @param rules the {@link Rules} to configure + */ + void configure(Rules rules); + } + + /** + * {@link MethodDescriptor} builder implementation. + * + * @param request type + * @param response type + */ + static final class Builder + implements Rules, + io.helidon.common.Builder, MethodDescriptor> { + private final String name; + private final io.grpc.MethodDescriptor.Builder descriptor; + private final ServerCallHandler callHandler; + + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + + private final Map, Object> context = new HashMap<>(); + + private Class requestType; + + private Class responseType; + + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.create(); + private MarshallerSupplier marshallerSupplier; + + Builder(String serviceName, + String name, + io.grpc.MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { + + this.name = name; + this.callHandler = callHandler; + this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name); + } + + Builder fullname(String name) { + descriptor.setFullMethodName(name); + 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 addContextValue(Context.Key key, T value) { + context.put(Objects.requireNonNull(key, "The context key cannot be null"), value); + return this; + } + + @Override + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + processInterceptors(interceptors); + return this; + } + + @Override + public Rules intercept(int priority, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + processInterceptors(interceptors); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public Builder requestType(Class requestType) { + this.requestType = requestType; + return (Builder) this; + } + + @Override + @SuppressWarnings("unchecked") + public Builder responseType(Class responseType) { + this.responseType = responseType; + return (Builder) this; + } + + @Override + @SuppressWarnings("unchecked") + public MethodDescriptor build() { + MarshallerSupplier supplier = this.marshallerSupplier; + + if (supplier == null) { + supplier = defaultMarshallerSupplier; + } + + if (requestType != null) { + descriptor.setRequestMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(requestType)); + } + + if (responseType != null) { + descriptor.setResponseMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(responseType)); + } + + return new MethodDescriptor<>(name, + descriptor.build(), + callHandler, + context, + interceptors); + } + + @SuppressWarnings("unchecked") + private void processInterceptors(ServerInterceptor... interceptors) { + // If any interceptors implement MethodDescriptor.Configurer allow them to apply further configuration + Arrays.stream(interceptors) + .filter(interceptor -> Configurer.class.isAssignableFrom(interceptor.getClass())) + .map(Configurer.class::cast) + .forEach(interceptor -> interceptor.configure(this)); + } + } +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java new file mode 100644 index 00000000000..0722385e121 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java @@ -0,0 +1,771 @@ +/* + * 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.webserver.grpc; + +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.Objects; + +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.Context; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; +import io.grpc.protobuf.ProtoFileDescriptorSupplier; +import io.grpc.stub.ServerCalls; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * Encapsulates all metadata necessary to create and deploy a gRPC service. + */ +public class ServiceDescriptor { + /** + * The {@link io.grpc.Context.Key} to use to obtain the {@link io.grpc.ServiceDescriptor}. + */ + public static final Context.Key SERVICE_DESCRIPTOR_KEY = + Context.key("Helidon.ServiceDescriptor"); + + private final String name; + private final String fullName; + private final String packageName; + private final Map> methods; + private final WeightedBag interceptors; + private final Map, Object> context; + private final Descriptors.FileDescriptor proto; + + private ServiceDescriptor(String name, + Map> methods, + WeightedBag interceptors, + Map, Object> context, + Descriptors.FileDescriptor proto) { + String assignedName = Objects.requireNonNull(name); + this.methods = methods; + this.context = Collections.unmodifiableMap(context); + this.interceptors = interceptors.copyMe(); + this.proto = proto; + + this.packageName = proto == null ? "" : proto.getPackage(); + String servicePrefix = packageName + (!packageName.isEmpty() ? "." : ""); + if (!servicePrefix.isEmpty() && assignedName.startsWith(servicePrefix)) { + // If assignedName is already prefixed with package name, strip the package name part + // so name is in simple format + this.name = assignedName.replace(servicePrefix, ""); + // Use the assigned name as the fullName since it is already prefixed with the package name + this.fullName = assignedName; + } else { + this.name = assignedName; + this.fullName = servicePrefix + assignedName; + } + } + + /** + * Return service name. + * + * @return service name + */ + public String name() { + return name; + } + + /** + * Returns the service name prefixed with package directive if one exists. + * + * @return service name prefixed with package directive if one exists. + */ + public String fullName() { + return fullName; + } + + /** + * Returns package name from proto file. + * + * @return package name from proto file + */ + public String packageName() { + return packageName; + } + + /** + * Return {@link MethodDescriptor} for a specified method name. + * + * @param name method name + * @return method descriptor for the specified name + */ + public MethodDescriptor method(String name) { + return methods.get(name); + } + + /** + * Return service methods. + * + * @return service methods + */ + public Collection> methods() { + return Collections.unmodifiableCollection(methods.values()); + } + + /** + * Return service interceptors. + * + * @return service interceptors + */ + public WeightedBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Return context map. + * + * @return context map + */ + public Map, Object> context() { + return context; + } + + /** + * Return a proto file descriptor. + * + * @return a proto file descriptor + */ + public Descriptors.FileDescriptor proto() { + return proto; + } + + @Override + public String toString() { + return "ServiceDescriptor(name='" + fullName + '\'' + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServiceDescriptor that = (ServiceDescriptor) o; + return fullName.equals(that.fullName); + } + + @Override + public int hashCode() { + return Objects.hash(fullName); + } + + /** + * Create a {@link Builder}. + * + * @param serviceClass the {@link Class} representing the service + * @param name the name of the service + * @return a {@link Builder} + */ + public static Builder builder(Class serviceClass, String name) { + return new Builder(serviceClass, name); + } + + /** + * Create a {@link Builder}. + * + * @param service the {@link GrpcService} to use to initialise the builder + * @return a {@link Builder} + */ + public static Builder builder(GrpcService service) { + return new Builder(service); + } + + /** + * Create a {@link Builder}. + * + * @param service the {@link BindableService} to use to initialise the builder + * @return a {@link Builder} + */ + public static Builder builder(BindableService service) { + return new Builder(service); + } + + // ---- inner interface: Config ----------------------------------------- + + /** + * Fluent configuration interface for the {@link ServiceDescriptor}. + */ + public interface Rules { + /** + * Set the name for the service. + * + * @param name the service name + * @return this {@link Rules} instance for fluent call chaining + * @throws NullPointerException if the name is null + * @throws IllegalArgumentException if the name is a blank String + */ + Rules name(String name); + + /** + * Obtain the name fo the service this configuration configures. + * + * @return the name fo the service this configuration configures + */ + String name(); + + /** + * Register the proto 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); + + /** + * Add one or more {@link ServerInterceptor} instances that will intercept calls + * to this service. + *

+ * If the added interceptors are annotated with the {@link io.helidon.common.Weight} + * annotation then that value will be used to assign a weight to use when applying + * the interceptor otherwise a priority of {@link InterceptorWeights#USER} will + * be used. + * + * @param interceptors one or more {@link ServerInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(ServerInterceptor... interceptors); + + /** + * Add one or more {@link ServerInterceptor} 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 ServerInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor interceptors} for a named method of the service. + *

+ * If the added interceptors are annotated with the {@link io.helidon.common.Weight} + * annotation then that value will be used to assign a weight to use when applying + * the interceptor otherwise a priority of {@link InterceptorWeights#USER} will + * be used. + * + * @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 name + */ + Rules intercept(String methodName, ServerInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.ServerInterceptor 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, ServerInterceptor... interceptors); + + /** + * Add value to the {@link io.grpc.Context} for the service. + * + * @param key the key for the context value + * @param value the value to add + * @param the type of the value + * @return this {@link Rules} instance for fluent call chaining + */ + Rules addContextValue(Context.Key key, V value); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param method the unary method to register + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name, ServerCalls.UnaryMethod method); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param method the unary method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name, + ServerCalls.UnaryMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param method the server streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, ServerCalls.ServerStreamingMethod method); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param method the server streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, + ServerCalls.ServerStreamingMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param method the client streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, ServerCalls.ClientStreamingMethod method); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param method the client streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, + ServerCalls.ClientStreamingMethod method, + MethodDescriptor.Configurer configurer); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param method the bi-directional streaming method to register + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, ServerCalls.BidiStreamingMethod method); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param method the bi-directional streaming method to register + * @param configurer the method configurer + * @param the method request type + * @param the method response type + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, + ServerCalls.BidiStreamingMethod method, + MethodDescriptor.Configurer configurer); + } + + // ---- inner class: Configurer ----------------------------------------- + + /** + * An interface implemented by classs that can configure + * a {@link Rules}. + */ + @FunctionalInterface + public interface Configurer { + /** + * Apply extra configuration to a {@link Rules}. + * + * @param rules the {@link Rules} to configure + */ + void configure(Rules rules); + } + + // ---- inner class: Aware ---------------------------------------------- + + /** + * Allows users to specify that they would like to have access to a + * {@link ServiceDescriptor} within their {@link io.grpc.ServerInterceptor} + * implementation. + */ + public interface Aware { + /** + * Set service descriptor. + * + * @param descriptor service descriptor instance + */ + void setServiceDescriptor(ServiceDescriptor descriptor); + } + + // ---- inner class: Builder -------------------------------------------- + + /** + * A {@link ServiceDescriptor} builder. + */ + public static final class Builder implements Rules, io.helidon.common.Builder { + private final Class serviceClass; + + private String name; + private Descriptors.FileDescriptor proto; + private MarshallerSupplier marshallerSupplier = MarshallerSupplier.create(); + private final Map> methodBuilders = new LinkedHashMap<>(); + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private final Map, Object> context = new HashMap<>(); + + Builder(Class serviceClass, String name) { + this.name = name == null || name.trim().isEmpty() ? serviceClass.getSimpleName() : name.trim(); + this.serviceClass = serviceClass; + } + + Builder(GrpcService service) { + this.name = service.serviceName(); + this.serviceClass = service.getClass(); + + // TODO service.update(this); + } + + @SuppressWarnings("unchecked") + Builder(BindableService service) { + ServerServiceDefinition def = service.bindService(); + + this.name = def.getServiceDescriptor().getName(); + this.serviceClass = service.getClass(); + + Object schemaDescriptor = def.getServiceDescriptor().getSchemaDescriptor(); + if (schemaDescriptor instanceof ProtoFileDescriptorSupplier) { + this.proto = ((ProtoFileDescriptorSupplier) schemaDescriptor).getFileDescriptor(); + } + + for (ServerMethodDefinition smd : def.getMethods()) { + io.grpc.MethodDescriptor md = smd.getMethodDescriptor(); + ServerCallHandler handler = smd.getServerCallHandler(); + String methodName = extractMethodName(md.getFullMethodName()); + MethodDescriptor.Builder descriptor = MethodDescriptor.builder(this.name, methodName, + (io.grpc.MethodDescriptor.Builder) md.toBuilder(), handler) + .marshallerSupplier(marshallerSupplier); + + methodBuilders.put(methodName, descriptor); + } + } + + @Override + public String name() { + return name; + } + + @Override + public Builder name(String name) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("name cannot be blank"); + } + + this.name = name.trim(); + for (Map.Entry> entry : methodBuilders.entrySet()) { + entry.getValue().fullname(name + "/" + entry.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, ServerCalls.UnaryMethod method) { + return unary(name, method, null); + } + + @Override + public Builder unary(String name, + ServerCalls.UnaryMethod method, + MethodDescriptor.Configurer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.UNARY, + ServerCalls.asyncUnaryCall(method), + configurer)); + return this; + } + + @Override + public Builder serverStreaming(String name, ServerCalls.ServerStreamingMethod method) { + return serverStreaming(name, method, null); + } + + @Override + public Builder serverStreaming(String name, + ServerCalls.ServerStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING, + ServerCalls.asyncServerStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder clientStreaming(String name, ServerCalls.ClientStreamingMethod method) { + return clientStreaming(name, method, null); + } + + @Override + public Builder clientStreaming(String name, + ServerCalls.ClientStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder bidirectional(String name, ServerCalls.BidiStreamingMethod method) { + return bidirectional(name, method, null); + } + + @Override + public Builder bidirectional(String name, + ServerCalls.BidiStreamingMethod method, + MethodDescriptor.Configurer configurer) { + + methodBuilders.put(name, createMethodDescriptor(name, + io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall(method), + configurer)); + return this; + } + + @Override + public Builder intercept(ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Builder intercept(int priority, ServerInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder intercept(String methodName, ServerInterceptor... interceptors) { + MethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); + } + + method.intercept(interceptors); + return this; + } + + @Override + public Builder intercept(String methodName, int priority, ServerInterceptor... interceptors) { + MethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); + } + + method.intercept(priority, interceptors); + return this; + } + + @Override + public Builder addContextValue(Context.Key key, V value) { + context.put(key, value); + return this; + } + + @Override + public ServiceDescriptor build() { + Map> methods = new LinkedHashMap<>(); + String fullName = getFullName(); + for (Map.Entry> entry : methodBuilders.entrySet()) { + String methodName = entry.getKey(); + String fullMethodName = io.grpc.MethodDescriptor.generateFullMethodName(fullName, methodName); + methods.put(methodName, entry.getValue().fullname(fullMethodName).build()); + } + return new ServiceDescriptor(name, methods, interceptors, context, proto); + } + + @Override + public String toString() { + return "ServiceDescriptor.Builder(name='" + name + '\'' + ')'; + } + + // ---- helpers ----------------------------------------------------- + + private MethodDescriptor.Builder createMethodDescriptor( + String methodName, + io.grpc.MethodDescriptor.MethodType methodType, + ServerCallHandler callHandler, + MethodDescriptor.Configurer configurer) { + + io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() + .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(getFullName(), methodName)) + .setType(methodType) + .setSampledToLocalTracing(true); + + Class requestType = getTypeFromMethodDescriptor(methodName, true); + Class responseType = getTypeFromMethodDescriptor(methodName, false); + + MethodDescriptor.Builder builder = MethodDescriptor.builder(this.name, methodName, grpcDesc, callHandler) + .defaultMarshallerSupplier(marshallerSupplier) + .requestType(requestType) + .responseType(responseType) + .fullname(getFullName()); + + if (configurer != null) { + configurer.configure(builder); + } + + return builder; + } + + @SuppressWarnings("unchecked") + 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 (Class) 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 name + String className = pkg + "." + outerClass + type.getName(); + + // 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 (Class) serviceClass.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String getPackageName() { + String pkg = proto.getOptions().getJavaPackage(); + return "".equals(pkg) ? proto.getPackage() : pkg; + } + + /** + * Returns the service name prefixed with package directive if one exists. + */ + private String getFullName() { + String pkg = proto == null ? "" : proto.getPackage(); + String serviceName = name; + if (!pkg.isEmpty() && !serviceName.startsWith(pkg + ".")) { + serviceName = pkg + "." + serviceName; + } + return serviceName; + } + + 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 name 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/webserver/grpc/src/main/java/module-info.java b/webserver/grpc/src/main/java/module-info.java index db94d3070ee..3a289cdab0e 100644 --- a/webserver/grpc/src/main/java/module-info.java +++ b/webserver/grpc/src/main/java/module-info.java @@ -41,6 +41,7 @@ requires transitive io.grpc; requires transitive io.grpc.stub; requires transitive io.helidon.common.config; + requires io.helidon.grpc.core; exports io.helidon.webserver.grpc; From 5264aa72f2b6010642f20f97f116405dcc0ed39d Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 6 Jun 2024 11:06:57 -0400 Subject: [PATCH 03/30] Restores unit tests in MP gRPC module. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/grpc/server/pom.xml | 5 ++ .../grpc/server/GrpcServerImpl.java | 7 +-- .../microprofile/grpc/server/EchoService.java | 47 +++++++++++++++++++ .../grpc/server/EchoServiceTest.java | 35 ++++++++++++++ .../grpc/server/GrpcServiceBuilderTest.java | 13 ++--- .../grpc/server/StubMarshaller.java | 7 ++- .../grpc/server/src/test/proto/echo.proto | 30 ++++++++++++ ...> io.helidon.grpc.core.MarshallerSupplier} | 0 ...croprofile.grpc.server.spi.GrpcMpExtension | 17 ------- 9 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java create mode 100644 microprofile/grpc/server/src/test/proto/echo.proto rename microprofile/grpc/server/src/test/resources/META-INF/services/{io.helidon.grpc.server.MarshallerSupplier => io.helidon.grpc.core.MarshallerSupplier} (100%) delete mode 100644 microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml index f820c8aeddb..ebf165bcf89 100644 --- a/microprofile/grpc/server/pom.xml +++ b/microprofile/grpc/server/pom.xml @@ -76,6 +76,11 @@ org.slf4j slf4j-jdk14 + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + org.junit.jupiter junit-jupiter-api diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java index c0a88b852f5..3aa7a151e7b 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.grpc.server; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import io.helidon.common.context.Context; @@ -47,17 +48,17 @@ public Context context() { @Override public CompletionStage start() { - return null; + return CompletableFuture.completedFuture(null); } @Override public CompletionStage whenShutdown() { - return null; + return CompletableFuture.completedFuture(null); } @Override public CompletionStage shutdown() { - return null; + return CompletableFuture.completedFuture(null); } @Override diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java new file mode 100644 index 00000000000..0180267b819 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java @@ -0,0 +1,47 @@ +/* + * 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.server; + +import io.grpc.stub.StreamObserver; +import io.helidon.grpc.server.test.Echo.EchoRequest; +import io.helidon.grpc.server.test.Echo.EchoResponse; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.Unary; +import jakarta.enterprise.context.ApplicationScoped; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +/** + * A simple test gRPC echo service. + */ +@Grpc +@ApplicationScoped +public class EchoService { + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + @Unary + public void echo(EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + EchoResponse response = EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java new file mode 100644 index 00000000000..9c88a54fe88 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.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.server; + +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import jakarta.enterprise.inject.spi.CDI; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddExtension(GrpcServerCdiExtension.class) +class EchoServiceTest { + + @Test + void test() { + assertThat(CDI.current(), is(notNullValue())); + } +} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java index 66216e01dd8..2548d3d1346 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java @@ -20,22 +20,20 @@ import java.util.List; import java.util.Map; -import io.helidon.webserver.grpc.MethodDescriptor; -import io.helidon.webserver.grpc.ServiceDescriptor; -import io.helidon.microprofile.grpc.core.Grpc; -import io.helidon.microprofile.grpc.core.GrpcMarshaller; -import io.helidon.microprofile.grpc.core.GrpcMethod; - import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.stub.StreamObserver; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.GrpcMarshaller; +import io.helidon.microprofile.grpc.core.GrpcMethod; +import io.helidon.webserver.grpc.MethodDescriptor; +import io.helidon.webserver.grpc.ServiceDescriptor; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.inject.Singleton; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; import org.mockito.ArgumentCaptor; import static org.hamcrest.CoreMatchers.is; @@ -47,7 +45,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Disabled public class GrpcServiceBuilderTest { private BeanManager beanManager; diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java index 43a4c6c572e..a20f296d99d 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StubMarshaller.java @@ -29,8 +29,7 @@ * This marshaller will not actually work and should not * be used as a real marshaller. */ -public class StubMarshaller - implements MethodDescriptor.Marshaller { +public class StubMarshaller implements MethodDescriptor.Marshaller { @Override public InputStream stream(T value) { @@ -43,8 +42,8 @@ public T parse(InputStream stream) { } @Named("stub") - public static class Supplier - implements MarshallerSupplier { + public static class Supplier implements MarshallerSupplier { + @Override public MethodDescriptor.Marshaller get(Class clazz) { return new StubMarshaller<>(); diff --git a/microprofile/grpc/server/src/test/proto/echo.proto b/microprofile/grpc/server/src/test/proto/echo.proto new file mode 100644 index 00000000000..739b442d005 --- /dev/null +++ b/microprofile/grpc/server/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.grpc.server.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.core.MarshallerSupplier similarity index 100% rename from microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.server.MarshallerSupplier rename to microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.grpc.core.MarshallerSupplier diff --git a/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension b/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension deleted file mode 100644 index b1e27c19138..00000000000 --- a/microprofile/grpc/server/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.server.spi.GrpcMpExtension +++ /dev/null @@ -1,17 +0,0 @@ -# -# 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. -# - -io.helidon.microprofile.grpc.server.GrpcServerCdiExtensionIT$ExtensionTwo \ No newline at end of file From 1d3a2a2436fe49c6e4ebd132bdb69bc0b738b846 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 11 Jun 2024 11:17:09 -0400 Subject: [PATCH 04/30] Complete end-to-end gRPC MP test passing. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/grpc/server/pom.xml | 31 +- .../grpc/server/ConstantHealthCheck.java | 58 --- .../grpc/server/GrpcMpCdiExtension.java | 119 ++++++ .../microprofile/grpc/server/GrpcServer.java | 302 --------------- .../grpc/server/GrpcServerBasicConfig.java | 133 ------- .../grpc/server/GrpcServerCdiExtension.java | 326 ---------------- .../grpc/server/GrpcServerConfiguration.java | 356 ------------------ .../grpc/server/GrpcServerImpl.java | 86 ----- .../grpc/server/GrpcTlsDescriptor.java | 219 ----------- .../grpc/server/spi/GrpcMpContext.java | 76 ---- .../grpc/server/spi/GrpcMpExtension.java | 29 -- .../grpc/server/spi/package-info.java | 20 - .../server/src/main/java/module-info.java | 8 +- .../src/main/resources/META-INF/beans.xml | 25 -- .../microprofile/grpc/server/EchoService.java | 47 --- .../grpc/server/EchoServiceTest.java | 73 +++- .../grpc/server/src/test/proto/echo.proto | 2 +- .../src/test/resources/application.yaml | 30 ++ .../grpc/server/src/test/resources/client.p12 | Bin 0 -> 4181 bytes .../src/test/resources/logging.properties | 11 +- .../grpc/server/src/test/resources/server.p12 | Bin 0 -> 4133 bytes .../webserver/grpc}/BindableServiceImpl.java | 9 +- .../java/io/helidon/webserver/grpc/Grpc.java | 37 +- .../helidon/webserver/grpc/GrpcRouting.java | 65 ++-- .../webserver/grpc/GrpcServiceRoute.java | 30 ++ .../webserver/grpc/MethodDescriptor.java | 26 ++ 26 files changed, 364 insertions(+), 1754 deletions(-) delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java delete mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java delete mode 100644 microprofile/grpc/server/src/main/resources/META-INF/beans.xml delete mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java create mode 100644 microprofile/grpc/server/src/test/resources/application.yaml create mode 100644 microprofile/grpc/server/src/test/resources/client.p12 create mode 100644 microprofile/grpc/server/src/test/resources/server.p12 rename {microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server => webserver/grpc/src/main/java/io/helidon/webserver/grpc}/BindableServiceImpl.java (96%) diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml index ebf165bcf89..2d9a63013e4 100644 --- a/microprofile/grpc/server/pom.xml +++ b/microprofile/grpc/server/pom.xml @@ -38,10 +38,18 @@ io.helidon.webserver helidon-webserver-grpc + + io.helidon.webserver + helidon-webserver-http2 + io.helidon.common helidon-common + + io.helidon.config + helidon-config-yaml-mp + io.helidon.config helidon-config-object-mapping @@ -63,6 +71,14 @@ io.helidon.microprofile.server helidon-microprofile-server + + io.helidon.logging + helidon-logging-common + + + io.helidon.logging + helidon-logging-jul + io.grpc grpc-inprocess @@ -76,11 +92,6 @@ org.slf4j slf4j-jdk14 - - io.helidon.microprofile.testing - helidon-microprofile-testing-junit5 - test - org.junit.jupiter junit-jupiter-api @@ -112,6 +123,16 @@ provided true + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java deleted file mode 100644 index fd7a7ea79fd..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ConstantHealthCheck.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.server; - -import org.eclipse.microprofile.health.HealthCheck; -import org.eclipse.microprofile.health.HealthCheckResponse; - -/** - * A simple {@link HealthCheck} implementation - * that always returns the same response. - */ -public class ConstantHealthCheck implements HealthCheck { - - private final HealthCheckResponse response; - - private ConstantHealthCheck(HealthCheckResponse response) { - this.response = response; - } - - @Override - public HealthCheckResponse call() { - return response; - } - - /** - * Obtain a {@link HealthCheck} that always returns a status of up. - * - * @param name the service name that the health check is for - * @return a {@link HealthCheck} that always returns a status of up - */ - public static HealthCheck up(String name) { - return new ConstantHealthCheck(HealthCheckResponse.named(name).up().build()); - } - - /** - * Obtain a {@link HealthCheck} that always returns a status of down. - * - * @param name the service name that the health check is for - * @return a {@link HealthCheck} that always returns a status of down - */ - public static HealthCheck down(String name) { - return new ConstantHealthCheck(HealthCheckResponse.named(name).down().build()); - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java new file mode 100644 index 00000000000..f4b72e23590 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java @@ -0,0 +1,119 @@ +/* + * 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.server; + +import java.lang.annotation.Annotation; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.grpc.GrpcService; + +import io.grpc.BindableService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; + +/** + * A CDI extension that will discover and register gRPC routes. + */ +public class GrpcMpCdiExtension implements Extension { + + private static final Logger LOGGER = Logger.getLogger(GrpcMpCdiExtension.class.getName()); + + private void discoverRoutes(@Observes @Initialized(ApplicationScoped.class) Object event, BeanManager beanManager) { + GrpcRouting.Builder routingBuilder = discoverGrpcRouting(beanManager); + ServerCdiExtension extension = beanManager.getExtension(ServerCdiExtension.class); + extension.addRouting(routingBuilder); + } + + /** + * Discover the services and interceptors to use to configure the {@link GrpcRouting}. + * + * @param beanManager the CDI bean manager + * @return the {@link GrpcRouting} to use or {@code null} if no services + * or routing were discovered + */ + private GrpcRouting.Builder discoverGrpcRouting(BeanManager beanManager) { + Instance instance = beanManager.createInstance(); + GrpcRouting.Builder builder = GrpcRouting.builder(); + + // discover @Grpc annotated beans + // we use the bean manager to do this as we need the actual bean class + beanManager.getBeans(Object.class, Any.Literal.INSTANCE) + .stream() + .filter(this::hasGrpcQualifier) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + register(service, builder, beanClass, beanManager); + }); + + // discover beans of type GrpcService + beanManager.getBeans(GrpcService.class) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + builder.service((GrpcService) service); + }); + + // discover beans of type BindableService + beanManager.getBeans(BindableService.class) + .forEach(bean -> { + Class beanClass = bean.getBeanClass(); + Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); + Object service = instance.select(beanClass, qualifiers).get(); + builder.service((BindableService) service); + }); + + return builder; + } + + private boolean hasGrpcQualifier(Bean bean) { + return bean.getQualifiers() + .stream() + .anyMatch(q -> Grpc.class.isAssignableFrom(q.annotationType())); + } + + /** + * Register the service with the routing. + *

+ * The service is actually a CDI proxy so the real service. + * + * @param service the service to register + * @param builder the gRPC routing + * @param beanManager the {@link BeanManager} to use to locate beans required by the service + */ + private void register(Object service, GrpcRouting.Builder builder, Class cls, BeanManager beanManager) { + GrpcServiceBuilder serviceBuilder = GrpcServiceBuilder.create(cls, () -> service, beanManager); + if (serviceBuilder.isAnnotatedService()) { + builder.service(serviceBuilder.build()); + } else { + LOGGER.log(Level.WARNING, + () -> "Discovered type is not a properly annotated gRPC service " + service.getClass()); + } + } +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java deleted file mode 100644 index d3188d71342..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServer.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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.server; - -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; - -import io.helidon.common.context.Context; -import io.helidon.grpc.core.WeightedBag; -import io.helidon.webserver.grpc.GrpcRouting; -import io.helidon.webserver.grpc.ServiceDescriptor; - -import io.grpc.ServerInterceptor; -import org.eclipse.microprofile.health.HealthCheck; - -/** - * Represents a immutably configured gRPC server. - *

- * Provides a basic lifecycle and monitoring API. - *

- * Instance can be created from {@link GrpcRouting} and optionally from {@link - * GrpcServerConfiguration} using {@link #create(GrpcRouting)}, {@link - * #create(GrpcServerConfiguration, GrpcRouting)} or {@link #builder(GrpcRouting)} methods - * and their builder enabled overloads. - */ -public interface GrpcServer { - /** - * Gets effective server configuration. - * - * @return Server configuration - */ - GrpcServerConfiguration configuration(); - - /** - * Gets a {@link GrpcServer} context. - * - * @return a server context - */ - Context context(); - - /** - * Starts the server. Has no effect if server is running. - * - * @return a completion stage of starting process - */ - CompletionStage start(); - - /** - * Completion stage is completed when server is shut down. - * - * @return a completion stage of the server - */ - CompletionStage whenShutdown(); - - /** - * Attempt to gracefully shutdown server. It is possible to use returned - * {@link CompletionStage} to react. - *

- * RequestMethod can be called periodically. - * - * @return to react on finished shutdown process - * @see #start() - */ - CompletionStage shutdown(); - - /** - * Return an array of health checks for this server. - * - * @return an array of {@link HealthCheck} instances for this server - */ - HealthCheck[] healthChecks(); - - /** - * Obtain the deployed services. - * - * @return an immutable {@link Map} of deployed {@link ServiceDescriptor}s - * keyed by service name - */ - Map services(); - - /** - * Returns {@code true} if the server is currently running. A running server - * in the stopping phase returns {@code true} until it is fully stopped. - * - * @return {@code true} if server is running - */ - boolean isRunning(); - - /** - * Returns a port number the default server socket is bound to and is - * listening on; or {@code -1} if unknown or not active. - *

- * Only supported only when server is running. - * - * @return a listen port; or {@code -1} if unknown or the default server - * socket is not active - */ - int port(); - - /** - * Creates a new instance from a provided configuration and a GrpcRouting. - * - * @param configurationBuilder a server configuration builder that will be - * built as a first step of this method - * execution; may be {@code null} - * @param routing a GrpcRouting instance - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} - */ - static GrpcServer create(Supplier configurationBuilder, GrpcRouting routing) { - return create(configurationBuilder != null - ? configurationBuilder.get() - : null, routing); - } - - /** - * Creates new instance form provided configuration and GrpcRouting. - * - * @param configurationBuilder a server configuration builder that will be - * built as a first step of this method - * execution; may be {@code null} - * @param routingBuilder a GrpcRouting builder that will be built as a - * second step of this method execution - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'routingBuilder' parameter is {@code - * null} - */ - static GrpcServer create(Supplier configurationBuilder, - Supplier routingBuilder) { - Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); - return create(configurationBuilder != null - ? configurationBuilder.get() - : null, routingBuilder.get()); - } - - /** - * Creates new instance form provided configuration and GrpcRouting. - * - * @param configuration a server configuration instance - * @param routingBuilder a GrpcRouting builder that will be built as a second - * step of this method execution - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'routingBuilder' parameter is {@code - * null} - */ - static GrpcServer create( - GrpcServerConfiguration configuration, - Supplier routingBuilder) { - Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); - return create(configuration, routingBuilder.get()); - } - - /** - * Creates new instance form provided GrpcRouting and default configuration. - * - * @param routing a GrpcRouting instance - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'routing' parameter is {@code null} - */ - static GrpcServer create(GrpcRouting routing) { - return create((GrpcServerConfiguration) null, routing); - } - - /** - * Creates new instance form provided configuration and GrpcRouting. - * - * @param configuration a server configuration instance - * @param routing a GrpcRouting instance - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} - */ - static GrpcServer create(GrpcServerConfiguration configuration, GrpcRouting routing) { - Objects.requireNonNull(routing, "Parameter 'routing' is null!"); - - return builder(routing) - .config(configuration) - .build(); - } - - /** - * Creates new instance form provided GrpcRouting and default configuration. - * - * @param routingBuilder a GrpcRouting builder instance that will be built as a - * first step of this method execution - * @return a new gRPC server instance - * @throws IllegalStateException if none SPI implementation found - * @throws NullPointerException if 'GrpcRouting' parameter is {@code null} - */ - static GrpcServer create(Supplier routingBuilder) { - Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); - return create(routingBuilder.get()); - } - - /** - * Creates a builder of the {@link GrpcServer}. - * - * @param routingBuilder the GrpcRouting builder; must not be {@code null} - * @return the builder - */ - static Builder builder(Supplier routingBuilder) { - Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); - return builder(routingBuilder.get()); - } - - /** - * Creates a builder of the {@link GrpcServer}. - * - * @param routing the GrpcRouting; must not be {@code null} - * @return the builder - */ - static Builder builder(GrpcRouting routing) { - return new Builder(GrpcServerConfiguration.create(), routing); - } - - /** - * GrpcServer builder class provides a convenient way to timed a - * GrpcServer instance. - */ - final class Builder implements io.helidon.common.Builder { - - private final GrpcRouting routing; - - private GrpcServerConfiguration configuration; - - private Builder(GrpcServerConfiguration configuration, GrpcRouting routing) { - Objects.requireNonNull(configuration, "Parameter 'configuration' must not be null!"); - Objects.requireNonNull(routing, "Parameter 'routing' must not be null!"); - - this.configuration = configuration; - this.routing = routing; - } - - /** - * Set a configuration of the {@link GrpcServer}. - * - * @param configuration the configuration - * @return an updated builder - */ - public Builder config(GrpcServerConfiguration configuration) { - this.configuration = configuration != null ? configuration : GrpcServerConfiguration.create(); - return this; - } - - /** - * Set a configuration of the {@link GrpcServer}. - * - * @param configurationBuilder the configuration builder - * @return an updated builder - */ - public Builder config(Supplier configurationBuilder) { - this.configuration = configurationBuilder != null - ? configurationBuilder.get() - : GrpcServerConfiguration.create(); - return this; - } - - /** - * Builds the {@link GrpcServer} instance as configured by this builder - * and its parameters. - * - * @return a ready to use {@link GrpcServer} - */ - @Override - public GrpcServer build() { - WeightedBag interceptors = WeightedBag.create(); - GrpcServerImpl server = GrpcServerImpl.create(configuration); - - interceptors.add(ContextSettingServerInterceptor.create()); - - // add the global interceptors from the routing AFTER the tracing interceptor - // so that all of those interceptors are included in the trace timings - interceptors.merge(routing.interceptors()); - - for (ServiceDescriptor service : routing.services()) { - server.deploy(service, interceptors); - } - - return server; - } - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java deleted file mode 100644 index 798ef4679a9..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerBasicConfig.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.server; - -import java.time.Duration; - -import io.helidon.common.context.Context; - -/** - * Configuration class for the {@link GrpcServer} implementations. - */ -public class GrpcServerBasicConfig - implements GrpcServerConfiguration { - - private final String name; - - private final int port; - - private final boolean nativeTransport; - - private final int workers; - - private final GrpcTlsDescriptor tlsConfig; - - private final Context context; - - private final int maxRapidResets; - - private final Duration rapidResetCheckPeriod; - - /** - * Construct {@link GrpcServerBasicConfig} instance. - * - * @param builder the {@link GrpcServerConfiguration.Builder} to use to configure - * this {@link GrpcServerBasicConfig}. - */ - private GrpcServerBasicConfig(GrpcServerConfiguration.Builder builder) { - this.name = builder.name(); - this.port = builder.port(); - this.context = builder.context(); - this.nativeTransport = builder.useNativeTransport(); - this.workers = builder.workers(); - this.tlsConfig = builder.tlsConfig(); - this.maxRapidResets = builder.maxRapidResets(); - this.rapidResetCheckPeriod = builder.rapidResetCheckPeriod(); - } - - /** - * Create a {@link GrpcServerBasicConfig} instance using the specified builder. - * - * @param builder the {@link GrpcServerConfiguration.Builder} to use to configure - * this {@link GrpcServerBasicConfig} - * @return a {@link GrpcServerBasicConfig} instance - */ - static GrpcServerBasicConfig create(GrpcServerConfiguration.Builder builder) { - return new GrpcServerBasicConfig(builder); - } - - // ---- accessors --------------------------------------------------- - - /** - * Get the server name. - * - * @return the server name - */ - @Override - public String name() { - return name; - } - - /** - * Get the server port. - * - * @return the server port - */ - @Override - public int port() { - return port; - } - - @Override - public Context context() { - return context; - } - - /** - * Determine whether use native transport if possible. - *

- * If native transport support is enabled, gRPC server will use epoll on - * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will - * be used. - * - * @return {@code true} if native transport should be used - */ - @Override - public boolean useNativeTransport() { - return nativeTransport; - } - - @Override - public int workers() { - return workers; - } - - @Override - public GrpcTlsDescriptor tlsConfig() { - return tlsConfig; - } - - @Override - public Duration rapidResetCheckPeriod() { - return rapidResetCheckPeriod; - } - - @Override - public int maxRapidResets() { - return maxRapidResets; - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java deleted file mode 100644 index 56b0713bda4..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerCdiExtension.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * 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.server; - -import java.lang.annotation.Annotation; -import java.util.ServiceLoader; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; - -import io.helidon.common.HelidonServiceLoader; -import io.helidon.config.Config; -import io.helidon.config.mp.MpConfig; -import io.helidon.microprofile.grpc.core.Grpc; -import io.helidon.microprofile.grpc.core.InProcessGrpcChannel; -import io.helidon.microprofile.grpc.server.spi.GrpcMpContext; -import io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; -import io.helidon.webserver.grpc.GrpcRouting; -import io.helidon.webserver.grpc.GrpcService; - -import io.grpc.BindableService; -import io.grpc.Channel; -import io.grpc.inprocess.InProcessChannelBuilder; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Initialized; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Any; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.Produces; -import jakarta.enterprise.inject.spi.AfterDeploymentValidation; -import jakarta.enterprise.inject.spi.Bean; -import jakarta.enterprise.inject.spi.BeanManager; -import jakarta.enterprise.inject.spi.BeforeShutdown; -import jakarta.enterprise.inject.spi.Extension; -import org.eclipse.microprofile.config.ConfigProvider; - -/** - * A CDI extension that will start the {@link GrpcServer gRPC server}. - *

- * The server is started when the {@link AfterDeploymentValidation} event - * is received and will be stopped when the {@link BeforeShutdown} event - * is received. - *

- * If no gRPC services are discovered the gRPC server will not be started. - */ -public class GrpcServerCdiExtension implements Extension { - - private static final Logger LOGGER = Logger.getLogger(GrpcServerCdiExtension.class.getName()); - private static final Logger STARTUP_LOGGER = Logger.getLogger("io.helidon.microprofile.startup.server"); - - private GrpcServer server; - - - private void startServer(@Observes @Initialized(ApplicationScoped.class) Object event, BeanManager beanManager) { - GrpcRouting.Builder routingBuilder = discoverGrpcRouting(beanManager); - - Config config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); - GrpcServerConfiguration.Builder serverConfiguration = GrpcServerConfiguration.builder(config.get("grpc")); - CompletableFuture startedFuture = new CompletableFuture<>(); - CompletableFuture shutdownFuture = new CompletableFuture<>(); - - loadExtensions(beanManager, config, routingBuilder, serverConfiguration, startedFuture, shutdownFuture); - server = GrpcServer.create(serverConfiguration.build(), routingBuilder.build()); - long beforeT = System.nanoTime(); - - server.start() - .whenComplete((grpcServer, throwable) -> { - if (null != throwable) { - STARTUP_LOGGER.log(Level.SEVERE, throwable, () -> "gRPC server startup failed"); - startedFuture.completeExceptionally(throwable); - } else { - long t = TimeUnit.MILLISECONDS.convert(System.nanoTime() - beforeT, TimeUnit.NANOSECONDS); - - int port = grpcServer.port(); - STARTUP_LOGGER.finest("gRPC server started up"); - LOGGER.info(() -> "gRPC server started on localhost:" + port + " (and all other host addresses) " - + "in " + t + " milliseconds."); - - grpcServer.whenShutdown() - .whenComplete((server, error) -> { - if (error == null) { - shutdownFuture.complete(server); - } else { - shutdownFuture.completeExceptionally(error); - } - }); - - startedFuture.complete(grpcServer); - } - }); - - // inject the server into the producer so that it can be discovered later - ServerProducer serverProducer = beanManager.createInstance().select(ServerProducer.class).get(); - serverProducer.server(server); - } - - private void stopServer(@Observes BeforeShutdown event) { - if (server != null) { - LOGGER.info("Stopping gRPC server"); - long beforeT = System.nanoTime(); - server.shutdown() - .whenComplete((webServer, throwable) -> { - if (null != throwable) { - LOGGER.log(Level.SEVERE, throwable, () -> "An error occurred stopping the gRPC server"); - } else { - long t = TimeUnit.MILLISECONDS.convert(System.nanoTime() - beforeT, TimeUnit.NANOSECONDS); - LOGGER.info(() -> "gRPC Server stopped in " + t + " milliseconds."); - } - }); - } - } - - /** - * Discover the services and interceptors to use to configure the - * {@link GrpcRouting}. - * - * @param beanManager the CDI bean manager - * @return the {@link GrpcRouting} to use or {@code null} if no services - * or routing were discovered - */ - private GrpcRouting.Builder discoverGrpcRouting(BeanManager beanManager) { - Instance instance = beanManager.createInstance(); - GrpcRouting.Builder builder = GrpcRouting.builder(); - - // discover @Grpc annotated beans - // we use the bean manager to do this as we need the actual bean class - beanManager.getBeans(Object.class, Any.Literal.INSTANCE) - .stream() - .filter(this::hasGrpcQualifier) - .forEach(bean -> { - Class beanClass = bean.getBeanClass(); - Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); - Object service = instance.select(beanClass, qualifiers).get(); - register(service, builder, beanClass, beanManager); - }); - - // discover beans of type GrpcService - beanManager.getBeans(GrpcService.class) - .forEach(bean -> { - Class beanClass = bean.getBeanClass(); - Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); - Object service = instance.select(beanClass, qualifiers).get(); - builder.service((GrpcService) service); - }); - - // discover beans of type BindableService - beanManager.getBeans(BindableService.class) - .forEach(bean -> { - Class beanClass = bean.getBeanClass(); - Annotation[] qualifiers = bean.getQualifiers().toArray(new Annotation[0]); - Object service = instance.select(beanClass, qualifiers).get(); - builder.service((BindableService) service); - }); - - return builder; - } - - private boolean hasGrpcQualifier(Bean bean) { - return bean.getQualifiers() - .stream() - .anyMatch(q -> Grpc.class.isAssignableFrom(q.annotationType())); - } - - /** - * Load any instances of {@link GrpcMpExtension} discovered by the - * {@link ServiceLoader} and allow them to further configure the - * gRPC server. - * - * @param beanManager the {@link BeanManager} - * @param config the Helidon configuration - * @param routingBuilder the {@link GrpcRouting.Builder} - * @param serverConfiguration the {@link GrpcServerConfiguration} - */ - private void loadExtensions(BeanManager beanManager, - Config config, - GrpcRouting.Builder routingBuilder, - GrpcServerConfiguration.Builder serverConfiguration, - CompletionStage whenStarted, - CompletionStage whenShutdown) { - - GrpcMpContext context = new GrpcMpContext() { - @Override - public Config config() { - return config; - } - - @Override - public GrpcServerConfiguration.Builder grpcServerConfiguration() { - return serverConfiguration; - } - - @Override - public GrpcRouting.Builder routing() { - return routingBuilder; - } - - @Override - public BeanManager beanManager() { - return beanManager; - } - - @Override - public CompletionStage whenStarted() { - return whenStarted; - } - - @Override - public CompletionStage whenShutdown() { - return whenShutdown; - } - }; - - HelidonServiceLoader.create(ServiceLoader.load(GrpcMpExtension.class)) - .forEach(ext -> ext.configure(context)); - - beanManager.createInstance() - .select(GrpcMpExtension.class) - .stream() - .forEach(ext -> ext.configure(context)); - } - - /** - * Register the service with the routing. - *

- * The service is actually a CDI proxy so the real service. - * - * @param service the service to register - * @param builder the gRPC routing - * @param beanManager the {@link BeanManager} to use to locate beans required by the service - */ - private void register(Object service, GrpcRouting.Builder builder, Class cls, BeanManager beanManager) { - GrpcServiceBuilder serviceBuilder = GrpcServiceBuilder.create(cls, () -> service, beanManager); - if (serviceBuilder.isAnnotatedService()) { - builder.service(serviceBuilder.build()); - } else { - LOGGER.log(Level.WARNING, - () -> "Discovered type is not a properly annotated gRPC service " + service.getClass()); - } - } - - /** - * A CDI producer that can supply the running {@link GrpcServer} - * an in-process {@link Channel}. - */ - @ApplicationScoped - public static class ServerProducer { - - private GrpcServer server; - - /** - * Produce the {@link GrpcServer}. - * - * @return the {@link GrpcServer} - */ - @Produces - public GrpcServer server() { - return server; - } - - /** - * Produce a {@link Supplier} that can supply the {@link GrpcServer}. - *

- * This could be useful where an injection point has the server injected - * before the {@link #startServer} method has actually started it. In that - * case a {@link Supplier Supplier<GrpcServer>} can be injected instead - * that will be able to lazily supply the server. - * - * @return a {@link Supplier} that can supply the {@link GrpcServer} - */ - @Produces - public Supplier supply() { - return this::server; - } - - /** - * Produces an in-process {@link Channel} to connect to the - * running gRPC server. - * - * @return an in-process {@link Channel} to connect to the - * running gRPC server - */ - @Produces - @InProcessGrpcChannel - public Channel channel() { - String name = server.configuration().name(); - return InProcessChannelBuilder.forName(name) - .usePlaintext() - .build(); - } - - /** - * Produces an in-process {@link InProcessChannelBuilder} to - * connect to the running gRPC server. - * - * @return an in-process {@link InProcessChannelBuilder} to - * connect to the running gRPC server - */ - @Produces - @InProcessGrpcChannel - public InProcessChannelBuilder channelBuilder() { - String name = server.configuration().name(); - return InProcessChannelBuilder.forName(name); - } - - void server(GrpcServer server) { - this.server = server; - } - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java deleted file mode 100644 index 6672e62797c..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerConfiguration.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * 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.server; - -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -import io.helidon.common.context.Context; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; - -/** - * The configuration for a gRPC server. - */ -public interface GrpcServerConfiguration { - /** - * The default server name. - */ - String DEFAULT_NAME = "grpc.server"; - - /** - * The default grpc port. - */ - int DEFAULT_PORT = 1408; - - /** - * The default number of worker threads that will be used if not explicitly set. - */ - int DEFAULT_WORKER_COUNT = Runtime.getRuntime().availableProcessors(); - - /** - * Get the server name. - * - * @return the server name - */ - String name(); - - /** - * Get the server port. - * - * @return the server port - */ - int port(); - - /** - * The top level {@link Context} to be used by the server. - * - * @return a context instance with registered application scoped instances - */ - Context context(); - - /** - * Determine whether use native transport if possible. - *

- * If native transport support is enabled, gRPC server will use epoll on - * Linux, or kqueue on OS X. Otherwise, the standard NIO transport will - * be used. - * - * @return {@code true} if native transport should be used - */ - boolean useNativeTransport(); - - /** - * Returns a count of threads in s pool used to process gRPC requests. - *

- * Default value is {@code CPU_COUNT * 2}. - * - * @return a workers count - */ - int workers(); - - /** - * Returns a SslConfiguration to use with the server socket. If not {@code null} then - * the server enforces an SSL communication. - * - * @return a TLS configuration to use - */ - GrpcTlsDescriptor tlsConfig(); - - /** - * Returns the period for counting rapid resets (stream RST sent by client before any data have been sent by server). - * - * @return the period for counting rapid resets - */ - Duration rapidResetCheckPeriod(); - - /** - * Returns the maximum allowed number of rapid resets (stream RST sent by client before any data have been sent by server). - * When reached within {@link #rapidResetCheckPeriod()}, GOAWAY is sent to client and connection is closed. - * - * @return the maximum allowed number of rapid resets - */ - int maxRapidResets(); - - /** - * Creates new instance with default values for all configuration properties. - * - * @return a new instance - */ - static GrpcServerConfiguration create() { - return builder().build(); - } - - /** - * Creates new instance with values from external configuration. - * - * @param config the externalized configuration - * @return a new instance - */ - static GrpcServerConfiguration create(Config config) { - return builder(config).build(); - } - - /** - * Creates new instance of a {@link Builder server configuration builder}. - * - * @return a new builder instance - */ - static Builder builder() { - return new Builder(); - } - - /** - * Creates new instance of a {@link Builder server configuration builder} with defaults from external configuration source. - * - * @param config the externalized configuration - * @return a new builder instance - */ - static Builder builder(Config config) { - return new Builder().config(config); - } - - /** - * A {@link GrpcServerConfiguration} builder. - */ - @Configured - final class Builder implements io.helidon.common.Builder { - private static final AtomicInteger GRPC_SERVER_COUNTER = new AtomicInteger(1); - - private String name = DEFAULT_NAME; - - private int port = DEFAULT_PORT; - - private boolean useNativeTransport; - - private int workers; - - private GrpcTlsDescriptor tlsConfig = null; - - private Context context; - - private int maxRapidResets = 200; - - private Duration rapidResetCheckPeriod = Duration.ofSeconds(30); - - private Builder() { - } - - /** - * Update the builder from configuration. - * - * @param config configuration instance - * @return updated builder - */ - @ConfiguredOption(key = "native", - type = Boolean.class, - value = "false", - description = "Specify if native transport should be used.") - public Builder config(Config config) { - if (config == null) { - return this; - } - - name = config.get("name").asString().orElse(DEFAULT_NAME); - port = config.get("port").asInt().orElse(DEFAULT_PORT); - maxRapidResets = config.get("max-rapid-resets").asInt().orElse(200); - rapidResetCheckPeriod = config.get("rapid-reset-check-period").as(Duration.class).orElse(Duration.ofSeconds(30)); - useNativeTransport = config.get("native").asBoolean().orElse(false); - config.get("workers").asInt().ifPresent(this::workersCount); - - return this; - } - - /** - * Set the name of the gRPC server. - *

- * Configuration key: {@code name} - * - * @param name the name of the gRPC server - * @return an updated builder - */ - @ConfiguredOption(key = "name", value = DEFAULT_NAME) - public Builder name(String name) { - this.name = name == null ? null : name.trim(); - return this; - } - - /** - * Sets server port. If port is {@code 0} or less then any available ephemeral port will be used. - *

- * Configuration key: {@code port} - * - * @param port the server port - * @return an updated builder - */ - @ConfiguredOption(value = "" + DEFAULT_PORT) - public Builder port(int port) { - this.port = port < 0 ? 0 : port; - return this; - } - - /** - * Period for counting rapid resets(stream RST sent by client before any data have been sent by server). - * Default value is {@code PT30S}. - * - * @param rapidResetCheckPeriod duration - * @return updated builder - * @see ISO_8601 Durations - * @see #maxRapidResets() - */ - @ConfiguredOption("PT30S") - public Builder rapidResetCheckPeriod(Duration rapidResetCheckPeriod) { - Objects.requireNonNull(rapidResetCheckPeriod); - this.rapidResetCheckPeriod = rapidResetCheckPeriod; - return this; - } - - /** - * Maximum number of rapid resets(stream RST sent by client before any data have been sent by server). - * When reached within {@link #rapidResetCheckPeriod()}, GOAWAY is sent to client and connection is closed. - * Default value is {@code 200}. - * - * @param maxRapidResets maximum number of rapid resets - * @return updated builder - * @see #rapidResetCheckPeriod() - */ - @ConfiguredOption("200") - public Builder maxRapidResets(int maxRapidResets) { - this.maxRapidResets = maxRapidResets; - return this; - } - - /** - * Configure the application scoped context to be used as a parent for webserver request contexts. - * - * @param context top level context - * @return an updated builder - */ - public Builder context(Context context) { - this.context = context; - return this; - } - - /** - * Sets a count of threads in pool used to process HTTP requests. - * Default value is {@code CPU_COUNT * 2}. - *

- * Configuration key: {@code workers} - * - * @param workers a workers count - * @return an updated builder - */ - @ConfiguredOption(key = "workers", value = "Number of processors available to the JVM") - public Builder workersCount(int workers) { - this.workers = workers; - return this; - } - - /** - * Configures TLS configuration to use with the server socket. If not {@code null} then - * the server enforces an TLS communication. - * - * @param tlsConfig a TLS configuration to use - * @return this builder - */ - public Builder tlsConfig(GrpcTlsDescriptor tlsConfig) { - this.tlsConfig = tlsConfig; - return this; - } - - String name() { - return name; - } - - int port() { - return port; - } - - /** - * Current Helidon {@link Context}. - * - * @return current context - */ - public Context context() { - return context; - } - - GrpcTlsDescriptor tlsConfig() { - return tlsConfig; - } - - boolean useNativeTransport() { - return useNativeTransport; - } - - int workers() { - return workers; - } - - int maxRapidResets() { - return maxRapidResets; - } - - Duration rapidResetCheckPeriod() { - return rapidResetCheckPeriod; - } - - @Override - public GrpcServerConfiguration build() { - if (name == null || name.isEmpty()) { - name = DEFAULT_NAME; - } - - if (port < 0) { - port = 0; - } - - if (context == null) { - context = Context.builder() - .id("grpc-" + GRPC_SERVER_COUNTER.getAndIncrement()) - .build(); - } - - if (workers <= 0) { - workers = DEFAULT_WORKER_COUNT; - } - - return GrpcServerBasicConfig.create(this); - } - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java deleted file mode 100644 index 3aa7a151e7b..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcServerImpl.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.server; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -import io.helidon.common.context.Context; -import io.helidon.grpc.core.WeightedBag; -import io.helidon.webserver.grpc.ServiceDescriptor; - -import io.grpc.ServerInterceptor; -import org.eclipse.microprofile.health.HealthCheck; - -class GrpcServerImpl implements GrpcServer { - - static GrpcServerImpl create() { - return new GrpcServerImpl(); - } - - static GrpcServerImpl create(GrpcServerConfiguration config) { - return new GrpcServerImpl(); // TODO - } - - @Override - public GrpcServerConfiguration configuration() { - return null; - } - - @Override - public Context context() { - return null; - } - - @Override - public CompletionStage start() { - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletionStage whenShutdown() { - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletionStage shutdown() { - return CompletableFuture.completedFuture(null); - } - - @Override - public HealthCheck[] healthChecks() { - return new HealthCheck[0]; - } - - @Override - public Map services() { - return null; - } - - @Override - public boolean isRunning() { - return false; - } - - @Override - public int port() { - return 0; - } - - public void deploy(ServiceDescriptor service, WeightedBag interceptors) { - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java deleted file mode 100644 index d75c0ce05e8..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcTlsDescriptor.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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.server; - -import io.helidon.common.configurable.Resource; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.config.objectmapping.Value; - -/** - * GrpcTlsDescriptor contains details about configuring TLS of a {@link io.grpc.Channel}. - */ -public class GrpcTlsDescriptor { - private final boolean enabled; - private final boolean jdkSSL; - private final Resource tlsCert; - private final Resource tlsKey; - private final Resource tlsCaCert; - - private GrpcTlsDescriptor(boolean enabled, boolean jdkSSL, Resource tlsCert, Resource tlsKey, Resource tlsCaCert) { - this.enabled = enabled; - this.jdkSSL = jdkSSL; - this.tlsCert = tlsCert; - this.tlsKey = tlsKey; - this.tlsCaCert = tlsCaCert; - } - - /** - * Return a new instance of {@link Builder}. - * - * @return a new instance of {@link Builder} - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Return an instance of builder based on the specified external config. - * - * @param config external config - * @return an instance of builder - */ - public static Builder builder(Config config) { - return new Builder(config); - } - - /** - * Create an instance of a TLS configuration from external configuration source. - * - * @param config external config - * @return an instance of a TLS configuration - */ - public static GrpcTlsDescriptor create(Config config) { - return builder(config).build(); - } - - /** - * Check if TLS is enabled. If this is false, then none of the other configuration values are used. - * - * @return true if TLS is enabled; false otherwise - */ - public boolean isEnabled() { - return enabled; - } - - /** - * Check if JDK SSL has be used. Only used for TLS enabled server channels.A Ignored by client channel. - * - * @return true if JDK ssl has to be used; false otherwise - */ - public boolean isJdkSSL() { - return jdkSSL; - } - - /** - * Get the tlsCert path. Can be either client or server cert. - * - * @return the path to tls certificate - */ - public Resource tlsCert() { - return tlsCert; - } - - /** - * Get the client private key path. Can be either client or server private key. - * - * @return the path to tls private key - */ - public Resource tlsKey() { - return tlsKey; - } - - /** - * Get the CA (certificate authority) certificate path. - * - * @return the path to CA certificate - */ - public Resource tlsCaCert() { - return tlsCaCert; - } - - /** - * Builder to build a new instance of {@link GrpcTlsDescriptor}. - */ - @Configured - public static class Builder implements io.helidon.common.Builder { - - private boolean enabled = true; - private boolean jdkSSL; - private Resource tlsCert; - private Resource tlsKey; - private Resource tlsCaCert; - - private Builder() { - - } - - private Builder(Config config) { - if (config == null) { - return; - } - - config.get("tls-cert.resource").as(Resource::create).ifPresent(this::tlsCert); - config.get("tls-key.resource").as(Resource::create).ifPresent(this::tlsKey); - config.get("tls-ca-cert.resource").as(Resource::create).ifPresent(this::tlsCaCert); - - this.jdkSSL = config.get("jdk-ssl").asBoolean().orElse(false); - this.enabled = config.get("enabled").asBoolean().orElse(true); - } - - /** - * Enable or disable TLS. If enabled is false, then the rest of the TLS configuration properties are ignored. - * - * @param enabled true to enable, false otherwise - * @return this instance for fluent API - */ - @ConfiguredOption(value = "true") - @Value(withDefault = "true") - public Builder enabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - /** - * Sets the type of SSL implementation to be used. - * - * @param jdkSSL true to use JDK based SSL, false otherwise - * @return this instance for fluent API - */ - @ConfiguredOption(key = "jdk-ssl", value = "false") - public Builder jdkSSL(boolean jdkSSL) { - this.jdkSSL = jdkSSL; - return this; - } - - /** - * Set the client tlsCert path. Required only if mutual auth is desired. - * - * @param tlsCert the path to client's certificate - * @return this instance for fluent API - */ - @ConfiguredOption - @Value(key = "tls-cert") - public Builder tlsCert(Resource tlsCert) { - this.tlsCert = tlsCert; - return this; - } - - /** - * Set the client private key path. Required only if mutual auth is desired. - * - * @param tlsKey the 's TLS private key - * @return this instance for fluent API - */ - @ConfiguredOption - @Value(key = "tls-key") - public Builder tlsKey(Resource tlsKey) { - this.tlsKey = tlsKey; - return this; - } - - /** - * Set the CA (certificate authority) certificate path. - * - * @param caCert the path to CA certificate - * @return this instance for fluent API - */ - @ConfiguredOption(key = "tls-ca-cert") - @Value(key = "tls-ca-cert") - public Builder tlsCaCert(Resource caCert) { - this.tlsCaCert = caCert; - return this; - } - - /** - * Create and return a new instance of {@link GrpcTlsDescriptor}. - * - * @return a new instance of {@link GrpcTlsDescriptor} - */ - public GrpcTlsDescriptor build() { - return new GrpcTlsDescriptor(enabled, jdkSSL, tlsCert, tlsKey, tlsCaCert); - } - } -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java deleted file mode 100644 index 13b48d70123..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.server.spi; - -import java.util.concurrent.CompletionStage; - -import io.helidon.config.Config; -import io.helidon.microprofile.grpc.server.GrpcServer; -import io.helidon.microprofile.grpc.server.GrpcServerConfiguration; -import io.helidon.webserver.grpc.GrpcRouting; - -import jakarta.enterprise.inject.spi.BeanManager; - -/** - * A context to allow a microprofile gRPC server extensions to configure additional - * services or components for the gRPC server or use the CDI bean manager. - */ -public interface GrpcMpContext { - - /** - * Obtain the Helidon configuration. - * - * @return the Helidon configuration - */ - Config config(); - - /** - * Obtain the {@link GrpcServerConfiguration}. - * - * @return the {@link GrpcServerConfiguration} - */ - GrpcServerConfiguration.Builder grpcServerConfiguration(); - - /** - * Obtain the {@link GrpcRouting.Builder} to allow modifications - * to be made to the routing before the server is configured. - * - * @return the {@link GrpcRouting.Builder} - */ - GrpcRouting.Builder routing(); - - /** - * Obtain the {@link jakarta.enterprise.inject.spi.BeanManager}. - * - * @return the {@link jakarta.enterprise.inject.spi.BeanManager} - */ - BeanManager beanManager(); - - /** - * Return a completion stage is completed when the gRPC server is started. - * - * @return a completion stage is completed when the gRPC server is started - */ - CompletionStage whenStarted(); - - /** - * Return a completion stage is completed when the gRPC server is shut down. - * - * @return a completion stage is completed when the gRPC server is shut down - */ - CompletionStage whenShutdown(); -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java deleted file mode 100644 index cf44c3252ed..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.server.spi; - -/** - * Microprofile service to extend features of the gRPC server. - */ -public interface GrpcMpExtension { - /** - * Allow the service to add configuration through the context. - * - * @param context context to obtain configuration objects - */ - void configure(GrpcMpContext context); -} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java deleted file mode 100644 index 5cf2f30af3d..00000000000 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -/** - * Microprofile gRPC server implementation. - */ -package io.helidon.microprofile.grpc.server.spi; diff --git a/microprofile/grpc/server/src/main/java/module-info.java b/microprofile/grpc/server/src/main/java/module-info.java index 298e1ad1c3b..c4b85ea5643 100644 --- a/microprofile/grpc/server/src/main/java/module-info.java +++ b/microprofile/grpc/server/src/main/java/module-info.java @@ -14,12 +14,13 @@ * limitations under the License. */ +import io.helidon.microprofile.grpc.server.GrpcMpCdiExtension; + /** * gRPC microprofile server module */ module io.helidon.microprofile.grpc.server { exports io.helidon.microprofile.grpc.server; - exports io.helidon.microprofile.grpc.server.spi; requires transitive io.helidon.webserver.grpc; requires transitive io.helidon.microprofile.grpc.core; @@ -43,12 +44,11 @@ requires io.helidon.config.metadata; requires io.helidon.common.context; - uses io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; - uses io.helidon.microprofile.grpc.server.GrpcServerCdiExtension; + uses GrpcMpCdiExtension; uses io.helidon.microprofile.grpc.server.AnnotatedServiceConfigurer; provides jakarta.enterprise.inject.spi.Extension - with io.helidon.microprofile.grpc.server.GrpcServerCdiExtension; + with GrpcMpCdiExtension; // needed when running with modules - to make private methods accessible opens io.helidon.microprofile.grpc.server to weld.core.impl, io.helidon.microprofile.cdi; diff --git a/microprofile/grpc/server/src/main/resources/META-INF/beans.xml b/microprofile/grpc/server/src/main/resources/META-INF/beans.xml deleted file mode 100644 index a0938bff7d4..00000000000 --- a/microprofile/grpc/server/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java deleted file mode 100644 index 0180267b819..00000000000 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.server; - -import io.grpc.stub.StreamObserver; -import io.helidon.grpc.server.test.Echo.EchoRequest; -import io.helidon.grpc.server.test.Echo.EchoResponse; -import io.helidon.microprofile.grpc.core.Grpc; -import io.helidon.microprofile.grpc.core.Unary; -import jakarta.enterprise.context.ApplicationScoped; - -import static io.helidon.grpc.core.ResponseHelper.complete; - -/** - * A simple test gRPC echo service. - */ -@Grpc -@ApplicationScoped -public class EchoService { - - /** - * Echo the message back to the caller. - * - * @param request the echo request containing the message to echo - * @param observer the call response - */ - @Unary - public void echo(EchoRequest request, StreamObserver observer) { - String message = request.getMessage(); - EchoResponse response = EchoResponse.newBuilder().setMessage(message).build(); - complete(observer, response); - } -} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index 9c88a54fe88..eeec93d2155 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -15,21 +15,86 @@ */ package io.helidon.microprofile.grpc.server; +import java.time.Duration; + +import io.grpc.stub.StreamObserver; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.Unary; +import io.helidon.microprofile.grpc.server.test.Echo; import io.helidon.microprofile.testing.junit5.AddExtension; import io.helidon.microprofile.testing.junit5.HelidonTest; -import jakarta.enterprise.inject.spi.CDI; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; import org.junit.jupiter.api.Test; +import static io.helidon.grpc.core.ResponseHelper.complete; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; @HelidonTest -@AddExtension(GrpcServerCdiExtension.class) +@AddExtension(GrpcMpCdiExtension.class) class EchoServiceTest { + private final GrpcClient grpcClient; + private final GrpcServiceDescriptor serviceDescriptor; + + @Inject + public EchoServiceTest(WebTarget webTarget) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.grpcClient = GrpcClient.builder() + .tls(clientTls) + .readTimeout(Duration.ofSeconds(300)) // debugging + .baseUri("https://localhost:" + webTarget.getUri().getPort()) + .build(); + + this.serviceDescriptor = GrpcServiceDescriptor.builder() + .serviceName("EchoService") + .putMethod("echo", + GrpcClientMethodDescriptor.unary("EchoService", "echo") + .requestType(Echo.EchoRequest.class) + .responseType(Echo.EchoResponse.class) + .build()) + .build(); + } + @Test void test() { - assertThat(CDI.current(), is(notNullValue())); + Echo.EchoResponse res = grpcClient.serviceClient(serviceDescriptor) + .unary("echo", fromString("Howdy")); + assertThat(res.getMessage(), is("Howdy")); + } + + private Echo.EchoRequest fromString(String value) { + return Echo.EchoRequest.newBuilder().setMessage(value).build(); + } + + @Grpc + @ApplicationScoped + public static class EchoService { + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + @Unary + 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/server/src/test/proto/echo.proto b/microprofile/grpc/server/src/test/proto/echo.proto index 739b442d005..af0d4c50d0d 100644 --- a/microprofile/grpc/server/src/test/proto/echo.proto +++ b/microprofile/grpc/server/src/test/proto/echo.proto @@ -15,7 +15,7 @@ */ syntax = "proto3"; -option java_package = "io.helidon.grpc.server.test"; +option java_package = "io.helidon.microprofile.grpc.server.test"; service EchoService { rpc Echo (EchoRequest) returns (EchoResponse) {} diff --git a/microprofile/grpc/server/src/test/resources/application.yaml b/microprofile/grpc/server/src/test/resources/application.yaml new file mode 100644 index 00000000000..00b3aef2e03 --- /dev/null +++ b/microprofile/grpc/server/src/test/resources/application.yaml @@ -0,0 +1,30 @@ +# +# 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" diff --git a/microprofile/grpc/server/src/test/resources/client.p12 b/microprofile/grpc/server/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/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java similarity index 96% rename from microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java rename to webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java index b7965ae9bc4..d71dcf860c4 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/BindableServiceImpl.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.microprofile.grpc.server; +package io.helidon.webserver.grpc; import java.util.LinkedHashSet; import java.util.List; @@ -22,12 +22,9 @@ import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; import java.util.function.Supplier; -import java.util.stream.Collectors; import io.helidon.grpc.core.InterceptorWeights; import io.helidon.grpc.core.WeightedBag; -import io.helidon.webserver.grpc.MethodDescriptor; -import io.helidon.webserver.grpc.ServiceDescriptor; import io.grpc.BindableService; import io.grpc.Metadata; @@ -99,7 +96,7 @@ private ServerCallHandler wrapCallHandler(MethodDescr priorityServerInterceptors.addAll(globalInterceptors); priorityServerInterceptors.addAll(descriptor.interceptors()); priorityServerInterceptors.addAll(method.interceptors()); - List interceptors = priorityServerInterceptors.stream().collect(Collectors.toList()); + List interceptors = priorityServerInterceptors.stream().toList(); if (!interceptors.isEmpty()) { LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); @@ -125,7 +122,7 @@ static Supplier createSupplier(Callable callable) { } static class CallableSupplier implements Supplier { - private Callable callable; + private final Callable callable; CallableSupplier(Callable callable) { this.callable = callable; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index 7ed1caedd9f..30be63b6ec7 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -50,7 +50,6 @@ static Grpc unary(Descriptors.FileDescriptor proto, String serviceName, String methodName, ServerCalls.UnaryMethod method) { - return grpc(proto, serviceName, methodName, ServerCalls.asyncUnaryCall(method)); } @@ -58,7 +57,6 @@ static Grpc bidi(Descriptors.FileDescriptor proto, String serviceName, String methodName, ServerCalls.BidiStreamingMethod method) { - return grpc(proto, serviceName, methodName, ServerCalls.asyncBidiStreamingCall(method)); } @@ -66,7 +64,6 @@ static Grpc serverStream(Descriptors.FileDescriptor pro String serviceName, String methodName, ServerCalls.ServerStreamingMethod method) { - return grpc(proto, serviceName, methodName, ServerCalls.asyncServerStreamingCall(method)); } @@ -74,7 +71,6 @@ static Grpc clientStream(Descriptors.FileDescriptor pro String serviceName, String methodName, ServerCalls.ClientStreamingMethod method) { - return grpc(proto, serviceName, methodName, ServerCalls.asyncClientStreamingCall(method)); } @@ -82,19 +78,27 @@ static Grpc clientStream(Descriptors.FileDescriptor pro * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link io.grpc.ServerMethodDefinition}. * * @param definition the {@link io.grpc.ServerMethodDefinition} representing the method to execute - * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} - * containing the service definition - * @param the request type - * @param the response type - * + * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} + * containing the service definition + * @param the request type + * @param the response type * @return a {@link io.helidon.webserver.grpc.Grpc gRPC route} created - * from the {@link io.grpc.ServerMethodDefinition} + * from the {@link io.grpc.ServerMethodDefinition} */ static Grpc methodDefinition(ServerMethodDefinition definition, Descriptors.FileDescriptor proto) { return grpc(definition.getMethodDescriptor(), definition.getServerCallHandler(), proto); } + public static Grpc unary(ServiceDescriptor service, io.helidon.webserver.grpc.MethodDescriptor method) { + String path = service.fullName() + "/" + method.name(); + return new Grpc<>((MethodDescriptor) method.descriptor(), + PathMatchers.exact(path), + (Class) method.requestType(), + (Class) method.responseType(), + method.callHandler()); + } + @Override Grpc toGrpc(HttpPrologue grpcPrologue) { return this; @@ -153,15 +157,14 @@ private static Grpc grpc(Descriptors.FileDescriptor pro /** * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link io.grpc.MethodDescriptor}. * - * @param grpcDesc the {@link io.grpc.MethodDescriptor} describing the method to execute + * @param grpcDesc the {@link io.grpc.MethodDescriptor} describing the method to execute * @param callHandler the {@link io.grpc.ServerCallHandler} that will execute the method - * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} containing - * the service definition - * @param the request type - * @param the response type - * + * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} containing + * the service definition + * @param the request type + * @param the response type * @return a {@link io.helidon.webserver.grpc.Grpc gRPC route} created - * from the {@link io.grpc.ServerMethodDefinition} + * from the {@link io.grpc.ServerMethodDefinition} */ private static Grpc grpc(MethodDescriptor grpcDesc, ServerCallHandler callHandler, diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index e39d914015d..7b7ff4a0b19 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -23,17 +23,18 @@ import java.util.List; import java.util.Map; +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.WeightedBag; +import io.helidon.http.HttpPrologue; +import io.helidon.http.PathMatchers; +import io.helidon.webserver.Routing; + import com.google.protobuf.Descriptors; import io.grpc.BindableService; import io.grpc.ServerInterceptor; import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; import io.grpc.stub.ServerCalls; -import io.helidon.grpc.core.InterceptorWeights; -import io.helidon.grpc.core.WeightedBag; -import io.helidon.http.HttpPrologue; -import io.helidon.http.PathMatchers; -import io.helidon.webserver.Routing; /** * GRPC specific routing. @@ -102,7 +103,7 @@ public WeightedBag interceptors() { * contained in this {@link GrpcRouting}. * * @return a {@link List} of the {@link ServiceDescriptor} instances - * contained in this {@link GrpcRouting} + * contained in this {@link GrpcRouting} */ public List services() { return services; @@ -152,7 +153,7 @@ public Builder service(GrpcService service) { * @return updated builder */ public Builder service(BindableService service) { - throw new UnsupportedOperationException("Not implemented"); // TODO + return route(GrpcServiceRoute.create(service)); } /** @@ -167,7 +168,7 @@ public Builder service(ServiceDescriptor service) { throw new IllegalArgumentException("Attempted to register service name " + name + " multiple times"); } services.put(name, service); - return this; + return route(GrpcServiceRoute.create(service)); } /** @@ -205,12 +206,12 @@ public Builder intercept(int weight, ServerInterceptor... interceptors) { /** * Unary route. * - * @param proto proto descriptor + * @param proto proto descriptor * @param serviceName service name - * @param methodName method name - * @param method method to handle this route - * @param request type - * @param response type + * @param methodName method name + * @param method method to handle this route + * @param request type + * @param response type * @return updated builder */ public Builder unary(Descriptors.FileDescriptor proto, @@ -223,12 +224,12 @@ public Builder unary(Descriptors.FileDescriptor proto, /** * Bidirectional route. * - * @param proto proto descriptor + * @param proto proto descriptor * @param serviceName service name - * @param methodName method name - * @param method method to handle this route - * @param request type - * @param response type + * @param methodName method name + * @param method method to handle this route + * @param request type + * @param response type * @return updated builder */ public Builder bidi(Descriptors.FileDescriptor proto, @@ -241,12 +242,12 @@ public Builder bidi(Descriptors.FileDescriptor proto, /** * Server streaming route. * - * @param proto proto descriptor + * @param proto proto descriptor * @param serviceName service name - * @param methodName method name - * @param method method to handle this route - * @param request type - * @param response type + * @param methodName method name + * @param method method to handle this route + * @param request type + * @param response type * @return updated builder */ public Builder serverStream(Descriptors.FileDescriptor proto, @@ -259,12 +260,12 @@ public Builder serverStream(Descriptors.FileDescriptor proto, /** * Client streaming route. * - * @param proto proto descriptor + * @param proto proto descriptor * @param serviceName service name - * @param methodName method name - * @param method method to handle this route - * @param request type - * @param response type + * @param methodName method name + * @param method method to handle this route + * @param request type + * @param response type * @return updated builder */ public Builder clientStream(Descriptors.FileDescriptor proto, @@ -277,9 +278,8 @@ public Builder clientStream(Descriptors.FileDescriptor proto, /** * Add all the routes for a {@link BindableService} service. * - * @param proto the proto descriptor - * @param service the {@link BindableService} to add routes for - * + * @param proto the proto descriptor + * @param service the {@link BindableService} to add routes for * @return updated builder */ public Builder service(Descriptors.FileDescriptor proto, BindableService service) { @@ -292,8 +292,7 @@ public Builder service(Descriptors.FileDescriptor proto, BindableService service /** * Add all the routes for the {@link ServerServiceDefinition} service. * - * @param service the {@link ServerServiceDefinition} to add routes for - * + * @param service the {@link ServerServiceDefinition} to add routes for * @return updated builder */ public Builder service(ServerServiceDefinition service) { diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index fdd0d91e76b..d4a8fd7aa3c 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -23,6 +23,7 @@ import io.helidon.http.PathMatchers; import com.google.protobuf.Descriptors; +import io.grpc.BindableService; import io.grpc.stub.ServerCalls; class GrpcServiceRoute extends GrpcRoute { @@ -40,6 +41,35 @@ static GrpcRoute create(GrpcService service) { return svcRouter.build(); } + static GrpcRoute create(BindableService service) { + throw new UnsupportedOperationException("Not implemented"); + } + + static GrpcRoute create(ServiceDescriptor service) { + String serviceName = service.name(); + List> routes = new LinkedList<>(); + + service.methods().forEach(method -> { + io.grpc.MethodDescriptor descriptor = method.descriptor(); + switch (descriptor.getType()) { + case UNARY -> routes.add(Grpc.unary(service, method)); + /* + case CLIENT_STREAMING -> + routes.add(Grpc.clientStream(service, method)); + case SERVER_STREAMING -> + routes.add(Grpc.serverStream(service, method)); + case BIDI_STREAMING -> + routes.add(Grpc.bidi(service, method)); + */ + case UNKNOWN -> throw new IllegalArgumentException("gRPC method of type " + + descriptor.getType() + " not supported"); + default -> throw new IllegalStateException("Invalid gRPC method type"); + } + }); + + return new GrpcServiceRoute(serviceName, routes); + } + @Override Grpc toGrpc(HttpPrologue prologue) { for (Grpc route : routes) { diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java index 8660e65d1ab..4903ecf2d32 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java @@ -42,14 +42,20 @@ public class MethodDescriptor { private final ServerCallHandler callHandler; private final Map, Object> context; private final WeightedBag interceptors; + private final Class requestType; + private final Class responseType; private MethodDescriptor(String name, io.grpc.MethodDescriptor descriptor, + Class requestType, + Class responseType, ServerCallHandler callHandler, Map, Object> context, WeightedBag interceptors) { this.name = name; this.descriptor = descriptor; + this.requestType = requestType; + this.responseType = responseType; this.callHandler = callHandler; this.context = context; this.interceptors = interceptors.copyMe(); @@ -82,6 +88,24 @@ public ServerCallHandler callHandler() { return callHandler; } + /** + * Return the method's request type. + * + * @return request type + */ + public Class requestType() { + return requestType; + } + + /** + * Return the method's response type. + * + * @return response type + */ + public Class responseType() { + return responseType; + } + /** * Obtain the {@link Map} of {@link Context.Key}s and values to add to the * call context when this method is invoked. @@ -328,6 +352,8 @@ public MethodDescriptor build() { return new MethodDescriptor<>(name, descriptor.build(), + (Class) requestType, + (Class) requestType, callHandler, context, interceptors); From 29b7a7e7f821382231a199f067c29417aabc439c Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 11 Jun 2024 14:23:41 -0400 Subject: [PATCH 05/30] Additional testing with different method types. Signed-off-by: Santiago Pericas-Geertsen --- .../server/AnnotatedServiceConfigurer.java | 10 +- .../ContextSettingServerInterceptor.java | 16 +- .../grpc/server/GrpcServiceBuilder.java | 39 ++-- .../grpc/server/BaseServiceTest.java | 49 +++++ .../grpc/server/EchoServiceTest.java | 39 +--- .../grpc/server/GrpcServiceBuilderTest.java | 22 +-- .../grpc/server/StringServiceTest.java | 174 ++++++++++++++++++ .../grpc/server/src/test/proto/strings.proto | 31 ++++ .../webserver/grpc/BindableServiceImpl.java | 32 ++-- .../java/io/helidon/webserver/grpc/Grpc.java | 18 +- ...criptor.java => GrpcMethodDescriptor.java} | 53 +++--- .../helidon/webserver/grpc/GrpcRouting.java | 12 +- ...riptor.java => GrpcServiceDescriptor.java} | 96 +++++----- .../webserver/grpc/GrpcServiceRoute.java | 22 +-- 14 files changed, 417 insertions(+), 196 deletions(-) create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/BaseServiceTest.java create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java create mode 100644 microprofile/grpc/server/src/test/proto/strings.proto rename webserver/grpc/src/main/java/io/helidon/webserver/grpc/{MethodDescriptor.java => GrpcMethodDescriptor.java} (85%) rename webserver/grpc/src/main/java/io/helidon/webserver/grpc/{ServiceDescriptor.java => GrpcServiceDescriptor.java} (86%) diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java index 0a2fdd8868b..77931538b79 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/AnnotatedServiceConfigurer.java @@ -16,14 +16,14 @@ package io.helidon.microprofile.grpc.server; -import io.helidon.webserver.grpc.ServiceDescriptor; +import io.helidon.webserver.grpc.GrpcServiceDescriptor; /** - * A class that may apply modifications to a {@link ServiceDescriptor.Builder} + * A class that may apply modifications to a {@link GrpcServiceDescriptor.Builder} * for an annotated gRPC service class. *

* Implementations of this class are called by the {@link GrpcServiceBuilder} when - * it builds a {@link ServiceDescriptor} from an annotated class. Instances of + * it builds a {@link GrpcServiceDescriptor} from an annotated class. Instances of * {@link AnnotatedServiceConfigurer} are discovered using the * {@link io.helidon.common.HelidonServiceLoader}. This service * loader supports ordering of configurers. @@ -31,11 +31,11 @@ @FunctionalInterface public interface AnnotatedServiceConfigurer { /** - * Apply modifications to a {@link ServiceDescriptor.Builder}. + * Apply modifications to a {@link GrpcServiceDescriptor.Builder}. * * @param serviceClass the annotated gRPC service class * @param annotatedClass the class with the {@link io.helidon.microprofile.grpc.core.Grpc} annotation * @param builder the builder to modify */ - void accept(Class serviceClass, Class annotatedClass, ServiceDescriptor.Builder builder); + void accept(Class serviceClass, Class annotatedClass, GrpcServiceDescriptor.Builder builder); } diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java index 61f6ac1cc5d..70327ca94b9 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java @@ -21,8 +21,8 @@ import io.helidon.common.Weight; import io.helidon.grpc.core.InterceptorWeights; -import io.helidon.webserver.grpc.MethodDescriptor; -import io.helidon.webserver.grpc.ServiceDescriptor; +import io.helidon.webserver.grpc.GrpcMethodDescriptor; +import io.helidon.webserver.grpc.GrpcServiceDescriptor; import io.grpc.Context; import io.grpc.Contexts; @@ -37,12 +37,12 @@ * A {@link ServerInterceptor} that sets values into the gRPC call context. */ @Weight(InterceptorWeights.CONTEXT) -class ContextSettingServerInterceptor implements ServerInterceptor, ServiceDescriptor.Aware { +class ContextSettingServerInterceptor implements ServerInterceptor, GrpcServiceDescriptor.Aware { /** - * The {@link ServiceDescriptor} for the service being intercepted. + * The {@link GrpcServiceDescriptor} for the service being intercepted. */ - private ServiceDescriptor serviceDescriptor; + private GrpcServiceDescriptor serviceDescriptor; private ContextSettingServerInterceptor() { } @@ -65,14 +65,14 @@ public ServerCall.Listener interceptCall(ServerCall, Object> contextMap = new HashMap<>(); // apply context keys from the service followed by the method // so that the method can override any service keys contextMap.putAll(serviceDescriptor.context()); contextMap.putAll(methodDescriptor.context()); - contextMap.put(ServiceDescriptor.SERVICE_DESCRIPTOR_KEY, serviceDescriptor); + contextMap.put(GrpcServiceDescriptor.SERVICE_DESCRIPTOR_KEY, serviceDescriptor); if (!contextMap.isEmpty()) { for (Map.Entry, Object> entry : contextMap.entrySet()) { @@ -85,7 +85,7 @@ public ServerCall.Listener interceptCall(ServerCall { + implements Builder { private static final Logger LOGGER = Logger.getLogger(GrpcServiceBuilder.class.getName()); @@ -117,19 +117,19 @@ public static GrpcServiceBuilder create(Class serviceClass, Supplier insta } /** - * Create a {@link ServiceDescriptor.Builder} introspected class. + * Create a {@link GrpcServiceDescriptor.Builder} introspected class. * - * @return a {@link ServiceDescriptor.Builder} for the introspected class. + * @return a {@link GrpcServiceDescriptor.Builder} for the introspected class. */ @Override - public ServiceDescriptor build() { + public GrpcServiceDescriptor build() { checkForNonPublicMethodIssues(); Class annotatedServiceClass = annotatedServiceClass(); AnnotatedMethodList methodList = AnnotatedMethodList.create(annotatedServiceClass); String name = determineServiceName(annotatedServiceClass); - ServiceDescriptor.Builder builder = ServiceDescriptor.builder(serviceClass(), name) + GrpcServiceDescriptor.Builder builder = GrpcServiceDescriptor.builder(serviceClass(), name) .marshallerSupplier(getMarshallerSupplier()); addServiceMethods(builder, methodList, beanManager); @@ -144,14 +144,15 @@ public ServiceDescriptor build() { } /** - * Add methods to the {@link ServiceDescriptor.Builder}. + * Add methods to the {@link GrpcServiceDescriptor.Builder}. * - * @param builder the {@link ServiceDescriptor.Builder} to add the method to + * @param builder the {@link GrpcServiceDescriptor.Builder} to add the method to * @param methodList the list of methods to add * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use * to look-up CDI beans. */ - private void addServiceMethods(ServiceDescriptor.Builder builder, AnnotatedMethodList methodList, BeanManager beanManager) { + private void addServiceMethods(GrpcServiceDescriptor.Builder builder, AnnotatedMethodList methodList, + BeanManager beanManager) { for (AnnotatedMethod am : methodList.withAnnotation(GrpcMethod.class)) { addServiceMethod(builder, am, beanManager); } @@ -161,18 +162,18 @@ private void addServiceMethods(ServiceDescriptor.Builder builder, AnnotatedMetho } /** - * Add a method to the {@link ServiceDescriptor.Builder}. + * Add a method to the {@link GrpcServiceDescriptor.Builder}. *

* The method configuration will be determined by the annotations present on the * method and the method signature. * - * @param builder the {@link ServiceDescriptor.Builder} to add the method to + * @param builder the {@link GrpcServiceDescriptor.Builder} to add the method to * @param method the {@link AnnotatedMethod} representing the method to add * @param beanManager the {@link jakarta.enterprise.inject.spi.BeanManager} to use * to look-up CDI beans. */ @SuppressWarnings("unchecked") - private void addServiceMethod(ServiceDescriptor.Builder builder, AnnotatedMethod method, BeanManager beanManager) { + private void addServiceMethod(GrpcServiceDescriptor.Builder builder, AnnotatedMethod method, BeanManager beanManager) { GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(GrpcMethod.class); String name = determineMethodName(method, annotation); Supplier instanceSupplier = instanceSupplier(); @@ -222,7 +223,7 @@ private void addServiceMethod(ServiceDescriptor.Builder builder, AnnotatedMethod } } - private void configureServiceInterceptors(ServiceDescriptor.Builder builder, BeanManager beanManager) { + private void configureServiceInterceptors(GrpcServiceDescriptor.Builder builder, BeanManager beanManager) { if (beanManager != null) { Class serviceClass = serviceClass(); Class annotatedClass = annotatedServiceClass(); @@ -235,7 +236,7 @@ private void configureServiceInterceptors(ServiceDescriptor.Builder builder, Bea } } - private void configureServiceInterceptors(ServiceDescriptor.Builder builder, BeanManager beanManager, Class cls) { + private void configureServiceInterceptors(GrpcServiceDescriptor.Builder builder, BeanManager beanManager, Class cls) { if (beanManager != null) { for (Annotation annotation : cls.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(GrpcInterceptorBinding.class)) { @@ -327,12 +328,12 @@ private Class getClass(Object o) { } /** - * A {@link Consumer} of {@link MethodDescriptor.Rules} that + * A {@link Consumer} of {@link GrpcMethodDescriptor.Rules} that * applies configuration changes based on annotations present * on the gRPC method. */ private static class AnnotatedMethodConfigurer - implements MethodDescriptor.Configurer { + implements GrpcMethodDescriptor.Configurer { private final AnnotatedMethod method; private final Class requestType; @@ -350,7 +351,7 @@ private AnnotatedMethodConfigurer(AnnotatedMethod method, } @Override - public void configure(MethodDescriptor.Rules rules) { + public void configure(GrpcMethodDescriptor.Rules rules) { rules.addContextValue(ContextKeys.SERVICE_METHOD, method.declaredMethod()) .requestType(requestType) .responseType(responseType); diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/BaseServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/BaseServiceTest.java new file mode 100644 index 00000000000..3ba455c9fd6 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/BaseServiceTest.java @@ -0,0 +1,49 @@ +/* + * 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.server; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.webclient.grpc.GrpcClient; +import jakarta.ws.rs.client.WebTarget; + +@HelidonTest +@AddExtension(GrpcMpCdiExtension.class) +class BaseServiceTest { + + private final GrpcClient grpcClient; + + public BaseServiceTest(WebTarget webTarget) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + webTarget.getUri().getPort()) + .build(); + } + + protected GrpcClient grpcClient() { + return grpcClient; + } +} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index eeec93d2155..4c3d757bed1 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -13,19 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.grpc.server; -import java.time.Duration; +package io.helidon.microprofile.grpc.server; import io.grpc.stub.StreamObserver; -import io.helidon.common.configurable.Resource; -import io.helidon.common.tls.Tls; import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.Unary; import io.helidon.microprofile.grpc.server.test.Echo; -import io.helidon.microprofile.testing.junit5.AddExtension; -import io.helidon.microprofile.testing.junit5.HelidonTest; -import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; import jakarta.enterprise.context.ApplicationScoped; @@ -37,32 +31,17 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -@HelidonTest -@AddExtension(GrpcMpCdiExtension.class) -class EchoServiceTest { +class EchoServiceTest extends BaseServiceTest { - private final GrpcClient grpcClient; private final GrpcServiceDescriptor serviceDescriptor; @Inject public EchoServiceTest(WebTarget webTarget) { - Tls clientTls = Tls.builder() - .trust(trust -> trust - .keystore(store -> store - .passphrase("password") - .trustStore(true) - .keystore(Resource.create("client.p12")))) - .build(); - this.grpcClient = GrpcClient.builder() - .tls(clientTls) - .readTimeout(Duration.ofSeconds(300)) // debugging - .baseUri("https://localhost:" + webTarget.getUri().getPort()) - .build(); - + super(webTarget); this.serviceDescriptor = GrpcServiceDescriptor.builder() .serviceName("EchoService") - .putMethod("echo", - GrpcClientMethodDescriptor.unary("EchoService", "echo") + .putMethod("Echo", + GrpcClientMethodDescriptor.unary("EchoService", "Echo") .requestType(Echo.EchoRequest.class) .responseType(Echo.EchoResponse.class) .build()) @@ -70,9 +49,9 @@ public EchoServiceTest(WebTarget webTarget) { } @Test - void test() { - Echo.EchoResponse res = grpcClient.serviceClient(serviceDescriptor) - .unary("echo", fromString("Howdy")); + void testEcho() { + Echo.EchoResponse res = grpcClient().serviceClient(serviceDescriptor) + .unary("Echo", fromString("Howdy")); assertThat(res.getMessage(), is("Howdy")); } @@ -90,7 +69,7 @@ public static class EchoService { * @param request the echo request containing the message to echo * @param observer the call response */ - @Unary + @Unary(name = "Echo") public void echo(Echo.EchoRequest request, StreamObserver observer) { String message = request.getMessage(); Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java index 2548d3d1346..43a2c5833c2 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcServiceBuilderTest.java @@ -27,8 +27,8 @@ import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.GrpcMarshaller; import io.helidon.microprofile.grpc.core.GrpcMethod; -import io.helidon.webserver.grpc.MethodDescriptor; -import io.helidon.webserver.grpc.ServiceDescriptor; +import io.helidon.webserver.grpc.GrpcMethodDescriptor; +import io.helidon.webserver.grpc.GrpcServiceDescriptor; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.inject.Singleton; @@ -61,7 +61,7 @@ public void setup() { public void shouldUseServiceNameFromAnnotation() { ServiceOne service = new ServiceOne(); GrpcServiceBuilder modeller = GrpcServiceBuilder.create(service, beanManager); - ServiceDescriptor descriptor = modeller.build(); + GrpcServiceDescriptor descriptor = modeller.build(); assertThat(descriptor.name(), is("ServiceOne/foo")); } @@ -69,7 +69,7 @@ public void shouldUseServiceNameFromAnnotation() { @Test public void shouldCreateDescriptorFoServiceWithNestedGenericParameters() { GrpcServiceBuilder modeller = GrpcServiceBuilder.create(ServiceSix.class, beanManager); - ServiceDescriptor descriptor = modeller.build(); + GrpcServiceDescriptor descriptor = modeller.build(); assertThat(descriptor.name(), is(ServiceSix.class.getSimpleName())); } @@ -77,7 +77,7 @@ public void shouldCreateDescriptorFoServiceWithNestedGenericParameters() { public void shouldUseDefaultServiceName() { ServiceTwo service = new ServiceTwo(); GrpcServiceBuilder modeller = GrpcServiceBuilder.create(service, beanManager); - ServiceDescriptor descriptor = modeller.build(); + GrpcServiceDescriptor descriptor = modeller.build(); assertThat(descriptor.name(), is("ServiceTwo")); } @@ -99,11 +99,11 @@ public void shouldCreateServiceFromClassWithoutBeanManager() { } public void assertServiceOne(GrpcServiceBuilder builder) { - ServiceDescriptor descriptor = builder.build(); + GrpcServiceDescriptor descriptor = builder.build(); assertThat(descriptor.name(), is("ServiceOne/foo")); assertThat(descriptor.methods().size(), is(4)); - MethodDescriptor methodDescriptor; + GrpcMethodDescriptor methodDescriptor; io.grpc.MethodDescriptor grpcDescriptor; methodDescriptor = descriptor.method("unary"); @@ -160,11 +160,11 @@ public void shouldCreateServiceWithMethodNamesFromAnnotation() { ServiceTwo service = new ServiceTwo(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(service, beanManager); - ServiceDescriptor descriptor = builder.build(); + GrpcServiceDescriptor descriptor = builder.build(); assertThat(descriptor.name(), is("ServiceTwo")); assertThat(descriptor.methods().size(), is(4)); - MethodDescriptor methodDescriptor; + GrpcMethodDescriptor methodDescriptor; io.grpc.MethodDescriptor grpcDescriptor; methodDescriptor = descriptor.method("One"); @@ -218,9 +218,9 @@ public void shouldCreateServiceWithMethodNamesFromAnnotation() { @SuppressWarnings("unchecked") public void assertSingleton(GrpcServiceBuilder builder) { - ServiceDescriptor descriptor = builder.build(); + GrpcServiceDescriptor descriptor = builder.build(); - MethodDescriptor methodDescriptor = descriptor.method("unary"); + GrpcMethodDescriptor methodDescriptor = descriptor.method("unary"); ServerCallHandler handler = methodDescriptor.callHandler(); ServerCall callOne = mock(ServerCall.class); diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java new file mode 100644 index 00000000000..f89739fb793 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java @@ -0,0 +1,174 @@ +/* + * 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.server; + +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.grpc.stub.StreamObserver; +import io.helidon.grpc.examples.common.StringServiceGrpc; +import io.helidon.grpc.examples.common.Strings; +import io.helidon.microprofile.grpc.core.Bidirectional; +import io.helidon.microprofile.grpc.core.ClientStreaming; +import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.ServerStreaming; +import io.helidon.microprofile.grpc.core.Unary; +import io.helidon.webserver.grpc.CollectingObserver; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.Test; + +import static io.helidon.grpc.core.ResponseHelper.complete; +import static io.helidon.grpc.core.ResponseHelper.stream; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class StringServiceTest extends BaseServiceTest { + private static final long TIMEOUT_SECONDS = 10; + + @Inject + public StringServiceTest(WebTarget webTarget) { + super(webTarget); + } + + @Test + void testUnaryUpper() { + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient().channel()); + Strings.StringMessage res = service.upper(newStringMessage("hello")); + assertThat(res.getText(), is("HELLO")); + } + + @Test + void testUnaryLower() { + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient().channel()); + Strings.StringMessage res = service.lower(newStringMessage("HELLO")); + assertThat(res.getText(), is("hello")); + } + + @Test + void testServerStreamingSplit() { + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient().channel()); + Iterator res = service.split(newStringMessage("hello world")); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + @Test + void testClientStreamingJoinAsync() throws ExecutionException, InterruptedException, TimeoutException { + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient().channel()); + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = service.join(singleStreamObserver(future)); + req.onNext(newStringMessage("hello")); + req.onNext(newStringMessage("world")); + req.onCompleted(); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("hello world")); + } + + @Test + void testBidirectionalEcho() throws ExecutionException, InterruptedException, TimeoutException { + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient().channel()); + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = service.echo(singleStreamObserver(future)); + req.onNext(newStringMessage("hello")); + req.onCompleted(); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("hello")); + } + + Strings.StringMessage newStringMessage(String data) { + return Strings.StringMessage.newBuilder().setText(data).build(); + } + + @Grpc + @ApplicationScoped + public static class StringService { + + @Unary(name = "Upper") + public void upper(Strings.StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toUpperCase())); + } + + @Unary(name = "Lower") + public void lower(Strings.StringMessage request, StreamObserver observer) { + complete(observer, response(request.getText().toLowerCase())); + } + + @ServerStreaming(name = "Split") + public void split(Strings.StringMessage request, StreamObserver observer) { + String[] parts = request.getText().split(" "); + stream(observer, Stream.of(parts).map(this::response)); + } + + @ClientStreaming(name = "Join") + public StreamObserver join(StreamObserver observer) { + return new CollectingObserver<>( + Collectors.joining(" "), + observer, + Strings.StringMessage::getText, + this::response); + } + + @Bidirectional(name = "Echo") + public StreamObserver echo(StreamObserver observer) { + return new StreamObserver<>() { + public void onNext(Strings.StringMessage value) { + observer.onNext(value); + } + + public void onError(Throwable t) { + } + + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + private Strings.StringMessage response(String text) { + return Strings.StringMessage.newBuilder().setText(text).build(); + } + } + + static StreamObserver singleStreamObserver(CompletableFuture future) { + return new StreamObserver<>() { + private ReqT value; + + @Override + public void onNext(ReqT value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + }; + } +} diff --git a/microprofile/grpc/server/src/test/proto/strings.proto b/microprofile/grpc/server/src/test/proto/strings.proto new file mode 100644 index 00000000000..c09c0efa5b4 --- /dev/null +++ b/microprofile/grpc/server/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.grpc.examples.common"; + +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/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java index d71dcf860c4..832ca9d3d3d 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java @@ -28,29 +28,31 @@ import io.grpc.BindableService; import io.grpc.Metadata; +import io.grpc.MethodDescriptor; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; import io.grpc.protobuf.ProtoFileDescriptorSupplier; import io.grpc.stub.StreamObserver; /** * A {@link BindableService} implementation that creates {@link ServerServiceDefinition} - * from a {@link ServiceDescriptor}. + * from a {@link GrpcServiceDescriptor}. */ class BindableServiceImpl implements BindableService { /** * The descriptor of this service. */ - private final ServiceDescriptor descriptor; + private final GrpcServiceDescriptor descriptor; /** * The global interceptors to apply. */ private final WeightedBag globalInterceptors; - private BindableServiceImpl(ServiceDescriptor descriptor, WeightedBag interceptors) { + private BindableServiceImpl(GrpcServiceDescriptor descriptor, WeightedBag interceptors) { this.descriptor = descriptor; this.globalInterceptors = interceptors.copyMe(); } @@ -62,7 +64,7 @@ private BindableServiceImpl(ServiceDescriptor descriptor, WeightedBag interceptors) { + static BindableServiceImpl create(GrpcServiceDescriptor descriptor, WeightedBag interceptors) { return new BindableServiceImpl(descriptor, interceptors); } @@ -71,8 +73,8 @@ static BindableServiceImpl create(ServiceDescriptor descriptor, WeightedBag builder.addMethod((io.grpc.MethodDescriptor) method.descriptor(), - wrapCallHandler(method))); + .forEach(method -> builder.addMethod((MethodDescriptor) method.descriptor(), + wrapCallHandler(method))); return builder.build(); } // ---- helpers --------------------------------------------------------- - private ServerCallHandler wrapCallHandler(MethodDescriptor method) { + private ServerCallHandler wrapCallHandler(GrpcMethodDescriptor method) { ServerCallHandler handler = method.callHandler(); WeightedBag priorityServerInterceptors = WeightedBag.create(InterceptorWeights.USER); @@ -173,8 +175,8 @@ public void accept(T result, Throwable error) { * A {@link ServerCallHandler} that wraps a {@link ServerCallHandler} with * a {@link ServerInterceptor}. *

- * If the wrapped {@link ServerInterceptor} implements {@link ServiceDescriptor.Aware} - * then the {@link ServiceDescriptor.Aware#setServiceDescriptor(ServiceDescriptor)} method + * If the wrapped {@link ServerInterceptor} implements {@link GrpcServiceDescriptor.Aware} + * then the {@link GrpcServiceDescriptor.Aware#setServiceDescriptor(GrpcServiceDescriptor)} method * will be called before calling {@link ServerInterceptor#interceptCall(ServerCall, * Metadata, ServerCallHandler)}. * @@ -182,11 +184,11 @@ public void accept(T result, Throwable error) { * @param the response type */ static final class InterceptingCallHandler implements ServerCallHandler { - private final ServiceDescriptor serviceDefinition; + private final GrpcServiceDescriptor serviceDefinition; private final ServerInterceptor interceptor; private final ServerCallHandler callHandler; - private InterceptingCallHandler(ServiceDescriptor serviceDefinition, + private InterceptingCallHandler(GrpcServiceDescriptor serviceDefinition, ServerInterceptor interceptor, ServerCallHandler callHandler) { this.serviceDefinition = serviceDefinition; @@ -198,8 +200,8 @@ private InterceptingCallHandler(ServiceDescriptor serviceDefinition, public ServerCall.Listener startCall( ServerCall call, Metadata headers) { - if (interceptor instanceof ServiceDescriptor.Aware) { - ((ServiceDescriptor.Aware) interceptor).setServiceDescriptor(serviceDefinition); + if (interceptor instanceof GrpcServiceDescriptor.Aware) { + ((GrpcServiceDescriptor.Aware) interceptor).setServiceDescriptor(serviceDefinition); } return interceptor.interceptCall(call, headers, callHandler); } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index 30be63b6ec7..0022a0f6c57 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -75,22 +75,22 @@ static Grpc clientStream(Descriptors.FileDescriptor pro } /** - * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link io.grpc.ServerMethodDefinition}. + * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link ServerMethodDefinition}. * - * @param definition the {@link io.grpc.ServerMethodDefinition} representing the method to execute + * @param definition the {@link ServerMethodDefinition} representing the method to execute * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} * containing the service definition * @param the request type * @param the response type * @return a {@link io.helidon.webserver.grpc.Grpc gRPC route} created - * from the {@link io.grpc.ServerMethodDefinition} + * from the {@link ServerMethodDefinition} */ static Grpc methodDefinition(ServerMethodDefinition definition, Descriptors.FileDescriptor proto) { return grpc(definition.getMethodDescriptor(), definition.getServerCallHandler(), proto); } - public static Grpc unary(ServiceDescriptor service, io.helidon.webserver.grpc.MethodDescriptor method) { + public static Grpc method(GrpcServiceDescriptor service, GrpcMethodDescriptor method) { String path = service.fullName() + "/" + method.name(); return new Grpc<>((MethodDescriptor) method.descriptor(), PathMatchers.exact(path), @@ -145,8 +145,8 @@ private static Grpc grpc(Descriptors.FileDescriptor pro MethodDescriptor.Marshaller reqMarshaller = ProtoMarshaller.get(requestType); MethodDescriptor.Marshaller resMarshaller = ProtoMarshaller.get(responseType); - io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() - .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(serviceName, methodName)) + MethodDescriptor.Builder grpcDesc = MethodDescriptor.newBuilder() + .setFullMethodName(MethodDescriptor.generateFullMethodName(serviceName, methodName)) .setType(getMethodType(mtd)).setFullMethodName(path).setRequestMarshaller(reqMarshaller) .setResponseMarshaller(resMarshaller).setSampledToLocalTracing(true); @@ -155,9 +155,9 @@ private static Grpc grpc(Descriptors.FileDescriptor pro /** - * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link io.grpc.MethodDescriptor}. + * Create a {@link io.helidon.webserver.grpc.Grpc gRPC route} from a {@link MethodDescriptor}. * - * @param grpcDesc the {@link io.grpc.MethodDescriptor} describing the method to execute + * @param grpcDesc the {@link MethodDescriptor} describing the method to execute * @param callHandler the {@link io.grpc.ServerCallHandler} that will execute the method * @param proto an optional protocol buffer {@link com.google.protobuf.Descriptors.FileDescriptor} containing * the service definition @@ -236,7 +236,7 @@ private static String getOuterClassFromFileName(String name) { return sb.toString(); } - private static io.grpc.MethodDescriptor.MethodType getMethodType(Descriptors.MethodDescriptor mtd) { + private static MethodDescriptor.MethodType getMethodType(Descriptors.MethodDescriptor mtd) { if (mtd.isClientStreaming()) { if (mtd.isServerStreaming()) { return MethodDescriptor.MethodType.BIDI_STREAMING; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcMethodDescriptor.java similarity index 85% rename from webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java rename to webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcMethodDescriptor.java index 4903ecf2d32..4edf4eb3abb 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/MethodDescriptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcMethodDescriptor.java @@ -27,6 +27,7 @@ import io.helidon.grpc.core.WeightedBag; import io.grpc.Context; +import io.grpc.MethodDescriptor; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; @@ -36,22 +37,22 @@ * @param request type * @param response type */ -public class MethodDescriptor { +public class GrpcMethodDescriptor { private final String name; - private final io.grpc.MethodDescriptor descriptor; + private final MethodDescriptor descriptor; private final ServerCallHandler callHandler; private final Map, Object> context; private final WeightedBag interceptors; private final Class requestType; private final Class responseType; - private MethodDescriptor(String name, - io.grpc.MethodDescriptor descriptor, - Class requestType, - Class responseType, - ServerCallHandler callHandler, - Map, Object> context, - WeightedBag interceptors) { + private GrpcMethodDescriptor(String name, + MethodDescriptor descriptor, + Class requestType, + Class responseType, + ServerCallHandler callHandler, + Map, Object> context, + WeightedBag interceptors) { this.name = name; this.descriptor = descriptor; this.requestType = requestType; @@ -75,7 +76,7 @@ public String name() { * * @return gRPC method descriptor */ - public io.grpc.MethodDescriptor descriptor() { + public MethodDescriptor descriptor() { return descriptor; } @@ -128,15 +129,15 @@ public WeightedBag interceptors() { static Builder builder(String serviceName, String name, - io.grpc.MethodDescriptor.Builder descriptor, + MethodDescriptor.Builder descriptor, ServerCallHandler callHandler) { return new Builder<>(serviceName, name, descriptor, callHandler); } - static MethodDescriptor create(String serviceName, - String name, - io.grpc.MethodDescriptor.Builder descriptor, - ServerCallHandler callHandler) { + static GrpcMethodDescriptor create(String serviceName, + String name, + MethodDescriptor.Builder descriptor, + ServerCallHandler callHandler) { return builder(serviceName, name, descriptor, callHandler).build(); } @@ -193,7 +194,7 @@ public interface Rules { * If not set the default {@link MarshallerSupplier} from the service will be used. * * @param marshallerSupplier the {@link MarshallerSupplier} for the service - * @return this {@link ServiceDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcServiceDescriptor.Rules} instance for fluent call chaining */ Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); @@ -207,7 +208,7 @@ public interface Rules { * * @param requestType the type of the request message * @param the type of the request message - * @return this {@link ServiceDescriptor.Rules} instance + * @return this {@link GrpcServiceDescriptor.Rules} instance * for fluent call chaining */ Rules requestType(Class requestType); @@ -222,7 +223,7 @@ public interface Rules { * * @param responseType the type of the request message * @param the type of the request message - * @return this {@link ServiceDescriptor.Rules} instance + * @return this {@link GrpcServiceDescriptor.Rules} instance * for fluent call chaining */ Rules responseType(Class responseType); @@ -246,16 +247,16 @@ public interface Configurer { } /** - * {@link MethodDescriptor} builder implementation. + * {@link GrpcMethodDescriptor} builder implementation. * * @param request type * @param response type */ static final class Builder implements Rules, - io.helidon.common.Builder, MethodDescriptor> { + io.helidon.common.Builder, GrpcMethodDescriptor> { private final String name; - private final io.grpc.MethodDescriptor.Builder descriptor; + private final MethodDescriptor.Builder descriptor; private final ServerCallHandler callHandler; private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); @@ -271,7 +272,7 @@ static final class Builder Builder(String serviceName, String name, - io.grpc.MethodDescriptor.Builder descriptor, + MethodDescriptor.Builder descriptor, ServerCallHandler callHandler) { this.name = name; @@ -335,7 +336,7 @@ public Builder responseType(Class responseType) { @Override @SuppressWarnings("unchecked") - public MethodDescriptor build() { + public GrpcMethodDescriptor build() { MarshallerSupplier supplier = this.marshallerSupplier; if (supplier == null) { @@ -343,14 +344,14 @@ public MethodDescriptor build() { } if (requestType != null) { - descriptor.setRequestMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(requestType)); + descriptor.setRequestMarshaller((MethodDescriptor.Marshaller) supplier.get(requestType)); } if (responseType != null) { - descriptor.setResponseMarshaller((io.grpc.MethodDescriptor.Marshaller) supplier.get(responseType)); + descriptor.setResponseMarshaller((MethodDescriptor.Marshaller) supplier.get(responseType)); } - return new MethodDescriptor<>(name, + return new GrpcMethodDescriptor<>(name, descriptor.build(), (Class) requestType, (Class) requestType, diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index 7b7ff4a0b19..d19256ba9cb 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -44,7 +44,7 @@ public class GrpcRouting implements Routing { private final ArrayList routes; private final WeightedBag interceptors; - private final ArrayList services; + private final ArrayList services; private GrpcRouting(Builder builder) { this.routes = new ArrayList<>(builder.routes); @@ -99,13 +99,13 @@ public WeightedBag interceptors() { } /** - * Obtain a {@link List} of the {@link ServiceDescriptor} instances + * Obtain a {@link List} of the {@link GrpcServiceDescriptor} instances * contained in this {@link GrpcRouting}. * - * @return a {@link List} of the {@link ServiceDescriptor} instances + * @return a {@link List} of the {@link GrpcServiceDescriptor} instances * contained in this {@link GrpcRouting} */ - public List services() { + public List services() { return services; } @@ -126,7 +126,7 @@ public List services() { public static class Builder implements io.helidon.common.Builder { private final List routes = new LinkedList<>(); private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); - private final Map services = new LinkedHashMap<>(); + private final Map services = new LinkedHashMap<>(); private Builder() { } @@ -162,7 +162,7 @@ public Builder service(BindableService service) { * @param service service to add * @return updated builder */ - public Builder service(ServiceDescriptor service) { + public Builder service(GrpcServiceDescriptor service) { String name = service.name(); if (services.containsKey(name)) { throw new IllegalArgumentException("Attempted to register service name " + name + " multiple times"); diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceDescriptor.java similarity index 86% rename from webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java rename to webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceDescriptor.java index 0722385e121..38925c1ec6b 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServiceDescriptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceDescriptor.java @@ -32,6 +32,7 @@ import com.google.protobuf.Descriptors; import io.grpc.BindableService; import io.grpc.Context; +import io.grpc.MethodDescriptor; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.ServerMethodDefinition; @@ -44,26 +45,26 @@ /** * Encapsulates all metadata necessary to create and deploy a gRPC service. */ -public class ServiceDescriptor { +public class GrpcServiceDescriptor { /** * The {@link io.grpc.Context.Key} to use to obtain the {@link io.grpc.ServiceDescriptor}. */ - public static final Context.Key SERVICE_DESCRIPTOR_KEY = + public static final Context.Key SERVICE_DESCRIPTOR_KEY = Context.key("Helidon.ServiceDescriptor"); private final String name; private final String fullName; private final String packageName; - private final Map> methods; + private final Map> methods; private final WeightedBag interceptors; private final Map, Object> context; private final Descriptors.FileDescriptor proto; - private ServiceDescriptor(String name, - Map> methods, - WeightedBag interceptors, - Map, Object> context, - Descriptors.FileDescriptor proto) { + private GrpcServiceDescriptor(String name, + Map> methods, + WeightedBag interceptors, + Map, Object> context, + Descriptors.FileDescriptor proto) { String assignedName = Objects.requireNonNull(name); this.methods = methods; this.context = Collections.unmodifiableMap(context); @@ -112,12 +113,12 @@ public String packageName() { } /** - * Return {@link MethodDescriptor} for a specified method name. + * Return {@link GrpcMethodDescriptor} for a specified method name. * * @param name method name * @return method descriptor for the specified name */ - public MethodDescriptor method(String name) { + public GrpcMethodDescriptor method(String name) { return methods.get(name); } @@ -126,7 +127,7 @@ public String packageName() { * * @return service methods */ - public Collection> methods() { + public Collection> methods() { return Collections.unmodifiableCollection(methods.values()); } @@ -170,7 +171,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ServiceDescriptor that = (ServiceDescriptor) o; + GrpcServiceDescriptor that = (GrpcServiceDescriptor) o; return fullName.equals(that.fullName); } @@ -213,7 +214,7 @@ public static Builder builder(BindableService service) { // ---- inner interface: Config ----------------------------------------- /** - * Fluent configuration interface for the {@link ServiceDescriptor}. + * Fluent configuration interface for the {@link GrpcServiceDescriptor}. */ public interface Rules { /** @@ -336,7 +337,7 @@ public interface Rules { */ Rules unary(String name, ServerCalls.UnaryMethod method, - MethodDescriptor.Configurer configurer); + GrpcMethodDescriptor.Configurer configurer); /** * Register server streaming method for the service. @@ -361,7 +362,7 @@ Rules unary(String name, */ Rules serverStreaming(String name, ServerCalls.ServerStreamingMethod method, - MethodDescriptor.Configurer configurer); + GrpcMethodDescriptor.Configurer configurer); /** * Register client streaming method for the service. @@ -386,7 +387,7 @@ Rules serverStreaming(String name, */ Rules clientStreaming(String name, ServerCalls.ClientStreamingMethod method, - MethodDescriptor.Configurer configurer); + GrpcMethodDescriptor.Configurer configurer); /** * Register bi-directional streaming method for the service. @@ -411,7 +412,7 @@ Rules clientStreaming(String name, */ Rules bidirectional(String name, ServerCalls.BidiStreamingMethod method, - MethodDescriptor.Configurer configurer); + GrpcMethodDescriptor.Configurer configurer); } // ---- inner class: Configurer ----------------------------------------- @@ -434,7 +435,7 @@ public interface Configurer { /** * Allows users to specify that they would like to have access to a - * {@link ServiceDescriptor} within their {@link io.grpc.ServerInterceptor} + * {@link GrpcServiceDescriptor} within their {@link io.grpc.ServerInterceptor} * implementation. */ public interface Aware { @@ -443,21 +444,21 @@ public interface Aware { * * @param descriptor service descriptor instance */ - void setServiceDescriptor(ServiceDescriptor descriptor); + void setServiceDescriptor(GrpcServiceDescriptor descriptor); } // ---- inner class: Builder -------------------------------------------- /** - * A {@link ServiceDescriptor} builder. + * A {@link GrpcServiceDescriptor} builder. */ - public static final class Builder implements Rules, io.helidon.common.Builder { + public static final class Builder implements Rules, io.helidon.common.Builder { private final Class serviceClass; private String name; private Descriptors.FileDescriptor proto; private MarshallerSupplier marshallerSupplier = MarshallerSupplier.create(); - private final Map> methodBuilders = new LinkedHashMap<>(); + private final Map> methodBuilders = new LinkedHashMap<>(); private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); private final Map, Object> context = new HashMap<>(); @@ -486,11 +487,11 @@ public static final class Builder implements Rules, io.helidon.common.Builder smd : def.getMethods()) { - io.grpc.MethodDescriptor md = smd.getMethodDescriptor(); + MethodDescriptor md = smd.getMethodDescriptor(); ServerCallHandler handler = smd.getServerCallHandler(); String methodName = extractMethodName(md.getFullMethodName()); - MethodDescriptor.Builder descriptor = MethodDescriptor.builder(this.name, methodName, - (io.grpc.MethodDescriptor.Builder) md.toBuilder(), handler) + GrpcMethodDescriptor.Builder descriptor = GrpcMethodDescriptor.builder(this.name, methodName, + (MethodDescriptor.Builder) md.toBuilder(), handler) .marshallerSupplier(marshallerSupplier); methodBuilders.put(methodName, descriptor); @@ -513,7 +514,7 @@ public Builder name(String name) { } this.name = name.trim(); - for (Map.Entry> entry : methodBuilders.entrySet()) { + for (Map.Entry> entry : methodBuilders.entrySet()) { entry.getValue().fullname(name + "/" + entry.getKey()); } return this; @@ -539,9 +540,9 @@ public Builder unary(String name, ServerCalls.UnaryMethod Builder unary(String name, ServerCalls.UnaryMethod method, - MethodDescriptor.Configurer configurer) { + GrpcMethodDescriptor.Configurer configurer) { methodBuilders.put(name, createMethodDescriptor(name, - io.grpc.MethodDescriptor.MethodType.UNARY, + MethodDescriptor.MethodType.UNARY, ServerCalls.asyncUnaryCall(method), configurer)); return this; @@ -555,10 +556,10 @@ public Builder serverStreaming(String name, ServerCalls.ServerStrea @Override public Builder serverStreaming(String name, ServerCalls.ServerStreamingMethod method, - MethodDescriptor.Configurer configurer) { + GrpcMethodDescriptor.Configurer configurer) { methodBuilders.put(name, createMethodDescriptor(name, - io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING, + MethodDescriptor.MethodType.SERVER_STREAMING, ServerCalls.asyncServerStreamingCall(method), configurer)); return this; @@ -572,10 +573,10 @@ public Builder clientStreaming(String name, ServerCalls.ClientStrea @Override public Builder clientStreaming(String name, ServerCalls.ClientStreamingMethod method, - MethodDescriptor.Configurer configurer) { + GrpcMethodDescriptor.Configurer configurer) { methodBuilders.put(name, createMethodDescriptor(name, - io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING, + MethodDescriptor.MethodType.CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall(method), configurer)); return this; @@ -589,10 +590,10 @@ public Builder bidirectional(String name, ServerCalls.BidiStreaming @Override public Builder bidirectional(String name, ServerCalls.BidiStreamingMethod method, - MethodDescriptor.Configurer configurer) { + GrpcMethodDescriptor.Configurer configurer) { methodBuilders.put(name, createMethodDescriptor(name, - io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING, + MethodDescriptor.MethodType.BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall(method), configurer)); return this; @@ -612,7 +613,7 @@ public Builder intercept(int priority, ServerInterceptor... interceptors) { @Override public Builder intercept(String methodName, ServerInterceptor... interceptors) { - MethodDescriptor.Builder method = methodBuilders.get(methodName); + GrpcMethodDescriptor.Builder method = methodBuilders.get(methodName); if (method == null) { throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); @@ -624,7 +625,7 @@ public Builder intercept(String methodName, ServerInterceptor... interceptors) { @Override public Builder intercept(String methodName, int priority, ServerInterceptor... interceptors) { - MethodDescriptor.Builder method = methodBuilders.get(methodName); + GrpcMethodDescriptor.Builder method = methodBuilders.get(methodName); if (method == null) { throw new IllegalArgumentException("No method exists with name '" + methodName + "'"); @@ -641,15 +642,15 @@ public Builder addContextValue(Context.Key key, V value) { } @Override - public ServiceDescriptor build() { - Map> methods = new LinkedHashMap<>(); + public GrpcServiceDescriptor build() { + Map> methods = new LinkedHashMap<>(); String fullName = getFullName(); - for (Map.Entry> entry : methodBuilders.entrySet()) { + for (Map.Entry> entry : methodBuilders.entrySet()) { String methodName = entry.getKey(); - String fullMethodName = io.grpc.MethodDescriptor.generateFullMethodName(fullName, methodName); + String fullMethodName = MethodDescriptor.generateFullMethodName(fullName, methodName); methods.put(methodName, entry.getValue().fullname(fullMethodName).build()); } - return new ServiceDescriptor(name, methods, interceptors, context, proto); + return new GrpcServiceDescriptor(name, methods, interceptors, context, proto); } @Override @@ -659,21 +660,22 @@ public String toString() { // ---- helpers ----------------------------------------------------- - private MethodDescriptor.Builder createMethodDescriptor( + private GrpcMethodDescriptor.Builder createMethodDescriptor( String methodName, - io.grpc.MethodDescriptor.MethodType methodType, + MethodDescriptor.MethodType methodType, ServerCallHandler callHandler, - MethodDescriptor.Configurer configurer) { + GrpcMethodDescriptor.Configurer configurer) { - io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() - .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(getFullName(), methodName)) + MethodDescriptor.Builder grpcDesc = MethodDescriptor.newBuilder() + .setFullMethodName(MethodDescriptor.generateFullMethodName(getFullName(), methodName)) .setType(methodType) .setSampledToLocalTracing(true); Class requestType = getTypeFromMethodDescriptor(methodName, true); Class responseType = getTypeFromMethodDescriptor(methodName, false); - MethodDescriptor.Builder builder = MethodDescriptor.builder(this.name, methodName, grpcDesc, callHandler) + GrpcMethodDescriptor.Builder builder = + GrpcMethodDescriptor.builder(this.name, methodName, grpcDesc, callHandler) .defaultMarshallerSupplier(marshallerSupplier) .requestType(requestType) .responseType(responseType) diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index d4a8fd7aa3c..809d8368d44 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -45,28 +45,10 @@ static GrpcRoute create(BindableService service) { throw new UnsupportedOperationException("Not implemented"); } - static GrpcRoute create(ServiceDescriptor service) { + static GrpcRoute create(GrpcServiceDescriptor service) { String serviceName = service.name(); List> routes = new LinkedList<>(); - - service.methods().forEach(method -> { - io.grpc.MethodDescriptor descriptor = method.descriptor(); - switch (descriptor.getType()) { - case UNARY -> routes.add(Grpc.unary(service, method)); - /* - case CLIENT_STREAMING -> - routes.add(Grpc.clientStream(service, method)); - case SERVER_STREAMING -> - routes.add(Grpc.serverStream(service, method)); - case BIDI_STREAMING -> - routes.add(Grpc.bidi(service, method)); - */ - case UNKNOWN -> throw new IllegalArgumentException("gRPC method of type " - + descriptor.getType() + " not supported"); - default -> throw new IllegalStateException("Invalid gRPC method type"); - } - }); - + service.methods().forEach(method -> routes.add(Grpc.method(service, method))); return new GrpcServiceRoute(serviceName, routes); } From b21099dad70ad2206ac53f17f6dd69ee769ce9cc Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 11 Jun 2024 15:50:21 -0400 Subject: [PATCH 06/30] New test that implements GrpcService instead of using annotations. Signed-off-by: Santiago Pericas-Geertsen --- .../helidon/microprofile/grpc/core/Grpc.java | 2 +- .../grpc/server/HashServiceTest.java | 65 +++++++++++++++++++ .../grpc/server/StringServiceTest.java | 4 +- .../grpc/server/src/test/proto/hash.proto | 31 +++++++++ .../grpc/server/src/test/proto/strings.proto | 2 +- 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java create mode 100644 microprofile/grpc/server/src/test/proto/hash.proto diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java index 661c98a323f..305e61cb00d 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java @@ -30,7 +30,7 @@ * An annotation used to mark a class as representing a gRPC service. */ @Qualifier -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java new file mode 100644 index 00000000000..ee9ce322024 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java @@ -0,0 +1,65 @@ +/* + * 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.server; + +import io.helidon.microprofile.grpc.server.test.Hash; +import io.helidon.microprofile.grpc.server.test.HashServiceGrpc; +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.Test; + +import static io.helidon.grpc.core.ResponseHelper.complete; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +class HashServiceTest extends BaseServiceTest { + + @Inject + public HashServiceTest(WebTarget webTarget) { + super(webTarget); + } + + @Test + void testHash() { + HashServiceGrpc.HashServiceBlockingStub service = HashServiceGrpc.newBlockingStub(grpcClient().channel()); + Hash.Value res = service.hash(Hash.Message.newBuilder().setText("hello world").build()); + assertThat(res.getText(), is(String.valueOf("hello world".hashCode()))); + } + + @ApplicationScoped + public static class HashService implements GrpcService { + @Override + public Descriptors.FileDescriptor proto() { + return Hash.getDescriptor(); + } + + @Override + public void update(Routing routing) { + routing.unary("Hash", this::hash); + } + + public void hash(Hash.Message request, StreamObserver observer) { + complete(observer, Hash.Value.newBuilder().setText( + String.valueOf(request.getText().hashCode())).build()); + } + } +} diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java index f89739fb793..4aa1d241399 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java @@ -25,13 +25,13 @@ import java.util.stream.Stream; import io.grpc.stub.StreamObserver; -import io.helidon.grpc.examples.common.StringServiceGrpc; -import io.helidon.grpc.examples.common.Strings; import io.helidon.microprofile.grpc.core.Bidirectional; import io.helidon.microprofile.grpc.core.ClientStreaming; import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.ServerStreaming; import io.helidon.microprofile.grpc.core.Unary; +import io.helidon.microprofile.grpc.server.test.StringServiceGrpc; +import io.helidon.microprofile.grpc.server.test.Strings; import io.helidon.webserver.grpc.CollectingObserver; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; diff --git a/microprofile/grpc/server/src/test/proto/hash.proto b/microprofile/grpc/server/src/test/proto/hash.proto new file mode 100644 index 00000000000..8e823ec2948 --- /dev/null +++ b/microprofile/grpc/server/src/test/proto/hash.proto @@ -0,0 +1,31 @@ +/* + * 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. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.microprofile.grpc.server.test"; + +service HashService { + rpc Hash (Message) returns (Value) {} +} + +message Message { + string text = 1; +} + +message Value { + string text = 1; +} diff --git a/microprofile/grpc/server/src/test/proto/strings.proto b/microprofile/grpc/server/src/test/proto/strings.proto index c09c0efa5b4..35caba68766 100644 --- a/microprofile/grpc/server/src/test/proto/strings.proto +++ b/microprofile/grpc/server/src/test/proto/strings.proto @@ -16,7 +16,7 @@ syntax = "proto3"; -option java_package = "io.helidon.grpc.examples.common"; +option java_package = "io.helidon.microprofile.grpc.server.test"; service StringService { rpc Upper (StringMessage) returns (StringMessage) {} From 3021ba90b4b00edb698b8ede96c4e034478e4f3c Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 12 Jun 2024 09:59:53 -0400 Subject: [PATCH 07/30] Adds support for services that implement the BindableService interface. New test. --- .../grpc/server/EchoServiceTest.java | 4 ++ .../grpc/server/HashServiceTest.java | 4 ++ .../grpc/server/RandomServiceTest.java | 67 +++++++++++++++++++ .../grpc/server/src/test/proto/random.proto | 31 +++++++++ .../webserver/grpc/GrpcServiceRoute.java | 2 +- 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/RandomServiceTest.java create mode 100644 microprofile/grpc/server/src/test/proto/random.proto diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index 4c3d757bed1..8d694bb90ad 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -59,6 +59,10 @@ private Echo.EchoRequest fromString(String value) { return Echo.EchoRequest.newBuilder().setMessage(value).build(); } + /** + * A service that is annotated by {@link Grpc}. Should be discovered by + * {@link GrpcMpCdiExtension}. + */ @Grpc @ApplicationScoped public static class EchoService { diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java index ee9ce322024..e318c8895e7 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/HashServiceTest.java @@ -45,6 +45,10 @@ void testHash() { assertThat(res.getText(), is(String.valueOf("hello world".hashCode()))); } + /** + * A service that implements the {@link GrpcService} interface. Should be + * discovered by {@link GrpcMpCdiExtension}. + */ @ApplicationScoped public static class HashService implements GrpcService { @Override diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/RandomServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/RandomServiceTest.java new file mode 100644 index 00000000000..e378dcd5378 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/RandomServiceTest.java @@ -0,0 +1,67 @@ +/* + * 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.server; + +import io.grpc.stub.StreamObserver; +import io.helidon.microprofile.grpc.server.test.Random; +import io.helidon.microprofile.grpc.server.test.RandomServiceGrpc; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.Test; + +import static io.helidon.grpc.core.ResponseHelper.complete; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.MatcherAssert.assertThat; + +class RandomServiceTest extends BaseServiceTest { + + @Inject + public RandomServiceTest(WebTarget webTarget) { + super(webTarget); + } + + @Test + void testRandom() { + RandomServiceGrpc.RandomServiceBlockingStub service = RandomServiceGrpc.newBlockingStub(grpcClient().channel()); + int seed = (int) System.currentTimeMillis(); + Random.IntValue res = service.random(Random.Seed.newBuilder().setN(seed).build());; + assertThat(res.getN(), is(lessThan(1000))); + } + + /** + * A service that implements the {@link io.grpc.BindableService} interface. Should be + * discovered by {@link GrpcMpCdiExtension}. + */ + @ApplicationScoped + public static class RandomService implements io.grpc.BindableService, RandomServiceGrpc.AsyncService { + + @Override + public io.grpc.ServerServiceDefinition bindService() { + return RandomServiceGrpc.bindService(this); + } + + @Override + public void random(Random.Seed request, StreamObserver observer) { + int seed = request.getN(); + java.util.Random random = new java.util.Random(); + random.setSeed(seed); + complete(observer, Random.IntValue.newBuilder().setN(random.nextInt(1000)).build()); + } + } +} diff --git a/microprofile/grpc/server/src/test/proto/random.proto b/microprofile/grpc/server/src/test/proto/random.proto new file mode 100644 index 00000000000..e1cfb50eb65 --- /dev/null +++ b/microprofile/grpc/server/src/test/proto/random.proto @@ -0,0 +1,31 @@ +/* + * 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. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.microprofile.grpc.server.test"; + +service RandomService { + rpc Random (Seed) returns (IntValue) {} +} + +message IntValue { + int32 n = 1; +} + +message Seed { + int32 n = 1; +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index 809d8368d44..ddc0898e9f8 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -42,7 +42,7 @@ static GrpcRoute create(GrpcService service) { } static GrpcRoute create(BindableService service) { - throw new UnsupportedOperationException("Not implemented"); + return create(GrpcServiceDescriptor.builder(service).build()); } static GrpcRoute create(GrpcServiceDescriptor service) { From 4ed225a4b6ed204f1af7b3108bbd8a9f2c56fa10 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 12 Jun 2024 14:36:13 -0400 Subject: [PATCH 08/30] Initial support for server interceptors. --- .../grpc/core/GrpcInterceptors.java | 2 +- .../grpc/server/EchoServiceTest.java | 60 ++++++++++++++++++- .../java/io/helidon/webserver/grpc/Grpc.java | 51 ++++------------ .../webserver/grpc/GrpcServiceRoute.java | 16 +++-- 4 files changed, 82 insertions(+), 47 deletions(-) diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java index 9621c68a7f4..9a504562375 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptors.java @@ -47,5 +47,5 @@ * * @return the ordered list of interceptors */ - Class[] value(); + Class[] value(); } diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index 8d694bb90ad..775671b63a0 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -16,23 +16,42 @@ package io.helidon.microprofile.grpc.server; -import io.grpc.stub.StreamObserver; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.HashSet; +import java.util.Set; + import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.core.GrpcInterceptor; +import io.helidon.microprofile.grpc.core.GrpcInterceptorBinding; +import io.helidon.microprofile.grpc.core.GrpcInterceptors; import io.helidon.microprofile.grpc.core.Unary; import io.helidon.microprofile.grpc.server.test.Echo; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; import org.junit.jupiter.api.Test; import static io.helidon.grpc.core.ResponseHelper.complete; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; class EchoServiceTest extends BaseServiceTest { + private static final Set> INTERCEPTORS_CALLED = new HashSet<>(); + private final GrpcServiceDescriptor serviceDescriptor; @Inject @@ -53,18 +72,28 @@ void testEcho() { Echo.EchoResponse res = grpcClient().serviceClient(serviceDescriptor) .unary("Echo", fromString("Howdy")); assertThat(res.getMessage(), is("Howdy")); + assertThat(INTERCEPTORS_CALLED, hasItems(EchoInterceptor1.class, EchoInterceptor2.class)); } private Echo.EchoRequest fromString(String value) { return Echo.EchoRequest.newBuilder().setMessage(value).build(); } + @GrpcInterceptorBinding + @Retention(RUNTIME) + @Target({TYPE, METHOD}) + public @interface EchoInterceptorBinding { + } + /** * A service that is annotated by {@link Grpc}. Should be discovered by - * {@link GrpcMpCdiExtension}. + * {@link GrpcMpCdiExtension}. References two interceptors, one directly + * and one via an interceptor binding. */ @Grpc @ApplicationScoped + @EchoInterceptorBinding + @GrpcInterceptors(EchoInterceptor1.class) public static class EchoService { /** @@ -80,4 +109,31 @@ public void echo(Echo.EchoRequest request, StreamObserver obs complete(observer, response); } } + + @GrpcInterceptor + @ApplicationScoped + public static class EchoInterceptor1 implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + INTERCEPTORS_CALLED.add(getClass()); + return next.startCall(call, headers); + } + } + + @GrpcInterceptor + @EchoInterceptorBinding + @ApplicationScoped + public static class EchoInterceptor2 implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + INTERCEPTORS_CALLED.add(getClass()); + return next.startCall(call, headers); + } + } } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index 0022a0f6c57..2af5334b42e 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -22,27 +22,23 @@ import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; +import io.grpc.BindableService; import io.grpc.MethodDescriptor; import io.grpc.ServerCallHandler; import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; import io.grpc.stub.ServerCalls; class Grpc extends GrpcRoute { private final MethodDescriptor method; private final PathMatcher pathMatcher; - private final Class requestType; - private final Class responseType; private final ServerCallHandler callHandler; private Grpc(MethodDescriptor method, PathMatcher pathMatcher, - Class requestType, - Class responseType, ServerCallHandler callHandler) { this.method = method; this.pathMatcher = pathMatcher; - this.requestType = requestType; - this.responseType = responseType; this.callHandler = callHandler; } @@ -90,13 +86,15 @@ static Grpc methodDefinition(ServerMethodDefinition method(GrpcServiceDescriptor service, GrpcMethodDescriptor method) { - String path = service.fullName() + "/" + method.name(); - return new Grpc<>((MethodDescriptor) method.descriptor(), + @SuppressWarnings("unchecked") + public static Grpc bindableMethod(BindableService service, + ServerMethodDefinition method) { + ServerServiceDefinition definition = service.bindService(); + String path = definition.getServiceDescriptor().getName() + "/" + + method.getMethodDescriptor().getBareMethodName(); + return new Grpc<>((MethodDescriptor) method.getMethodDescriptor(), PathMatchers.exact(path), - (Class) method.requestType(), - (Class) method.responseType(), - method.callHandler()); + (ServerCallHandler) method.getServerCallHandler()); } @Override @@ -112,14 +110,6 @@ MethodDescriptor method() { return method; } - Class requestType() { - return requestType; - } - - Class responseType() { - return responseType; - } - ServerCallHandler callHandler() { return callHandler; } @@ -128,7 +118,6 @@ private static Grpc grpc(Descriptors.FileDescriptor pro String serviceName, String methodName, ServerCallHandler callHandler) { - Descriptors.ServiceDescriptor svc = proto.findServiceByName(serviceName); Descriptors.MethodDescriptor mtd = svc.findMethodByName(methodName); @@ -150,7 +139,7 @@ private static Grpc grpc(Descriptors.FileDescriptor pro .setType(getMethodType(mtd)).setFullMethodName(path).setRequestMarshaller(reqMarshaller) .setResponseMarshaller(resMarshaller).setSampledToLocalTracing(true); - return new Grpc<>(grpcDesc.build(), PathMatchers.exact(path), requestType, responseType, callHandler); + return new Grpc<>(grpcDesc.build(), PathMatchers.exact(path), callHandler); } @@ -169,23 +158,7 @@ private static Grpc grpc(Descriptors.FileDescriptor pro private static Grpc grpc(MethodDescriptor grpcDesc, ServerCallHandler callHandler, Descriptors.FileDescriptor proto) { - - Class requestType = null; - Class responsetype = null; - String serviceName = grpcDesc.getServiceName(); - - if (proto != null && serviceName != null) { - Descriptors.ServiceDescriptor svc = proto.findServiceByName(serviceName); - Descriptors.MethodDescriptor mtd = svc.findMethodByName(grpcDesc.getBareMethodName()); - /* - We have to use reflection here - - to load the class - - to invoke a static method on it - */ - requestType = load(getClassName(mtd.getInputType())); - responsetype = load(getClassName(mtd.getOutputType())); - } - return new Grpc<>(grpcDesc, PathMatchers.exact(grpcDesc.getFullMethodName()), requestType, responsetype, callHandler); + return new Grpc<>(grpcDesc, PathMatchers.exact(grpcDesc.getFullMethodName()), callHandler); } private static String getClassName(Descriptors.Descriptor descriptor) { diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index ddc0898e9f8..15f8b21723f 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -19,11 +19,14 @@ import java.util.LinkedList; import java.util.List; +import io.helidon.grpc.core.WeightedBag; import io.helidon.http.HttpPrologue; import io.helidon.http.PathMatchers; import com.google.protobuf.Descriptors; import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; import io.grpc.stub.ServerCalls; class GrpcServiceRoute extends GrpcRoute { @@ -42,14 +45,17 @@ static GrpcRoute create(GrpcService service) { } static GrpcRoute create(BindableService service) { - return create(GrpcServiceDescriptor.builder(service).build()); + ServerServiceDefinition definition = service.bindService(); + String serviceName = definition.getServiceDescriptor().getName(); + List> routes = new LinkedList<>(); + service.bindService().getMethods().forEach( + method -> routes.add(Grpc.bindableMethod(service, method))); + return new GrpcServiceRoute(serviceName, routes); } static GrpcRoute create(GrpcServiceDescriptor service) { - String serviceName = service.name(); - List> routes = new LinkedList<>(); - service.methods().forEach(method -> routes.add(Grpc.method(service, method))); - return new GrpcServiceRoute(serviceName, routes); + WeightedBag interceptors = WeightedBag.create(); // TODO + return create(BindableServiceImpl.create(service, interceptors)); } @Override From cf79308475f88743669437966d623de6c7687760 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 13 Jun 2024 10:00:01 -0400 Subject: [PATCH 09/30] Adds support for ContextSettingServerInterceptor. --- .../grpc/server/EchoServiceTest.java | 55 ++++++++++--------- .../ContextSettingServerInterceptor.java | 23 ++++---- .../webserver/grpc/GrpcServiceRoute.java | 23 +++++++- 3 files changed, 63 insertions(+), 38 deletions(-) rename {microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server => webserver/grpc/src/main/java/io/helidon/webserver/grpc}/ContextSettingServerInterceptor.java (81%) diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index 775671b63a0..aae6e646711 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -21,26 +21,26 @@ import java.util.HashSet; import java.util.Set; +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.GrpcInterceptor; import io.helidon.microprofile.grpc.core.GrpcInterceptorBinding; import io.helidon.microprofile.grpc.core.GrpcInterceptors; import io.helidon.microprofile.grpc.core.Unary; import io.helidon.microprofile.grpc.server.test.Echo; -import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; -import io.helidon.webclient.grpc.GrpcServiceDescriptor; - -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; -import io.grpc.ServerInterceptor; -import io.grpc.stub.StreamObserver; +import io.helidon.microprofile.grpc.server.test.EchoServiceGrpc; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; import org.junit.jupiter.api.Test; import static io.helidon.grpc.core.ResponseHelper.complete; +import static io.helidon.webserver.grpc.GrpcServiceDescriptor.SERVICE_DESCRIPTOR_KEY; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -52,25 +52,15 @@ class EchoServiceTest extends BaseServiceTest { private static final Set> INTERCEPTORS_CALLED = new HashSet<>(); - private final GrpcServiceDescriptor serviceDescriptor; - @Inject public EchoServiceTest(WebTarget webTarget) { super(webTarget); - this.serviceDescriptor = GrpcServiceDescriptor.builder() - .serviceName("EchoService") - .putMethod("Echo", - GrpcClientMethodDescriptor.unary("EchoService", "Echo") - .requestType(Echo.EchoRequest.class) - .responseType(Echo.EchoResponse.class) - .build()) - .build(); } @Test void testEcho() { - Echo.EchoResponse res = grpcClient().serviceClient(serviceDescriptor) - .unary("Echo", fromString("Howdy")); + EchoServiceGrpc.EchoServiceBlockingStub service = EchoServiceGrpc.newBlockingStub(grpcClient().channel()); + Echo.EchoResponse res = service.echo(fromString("Howdy")); assertThat(res.getMessage(), is("Howdy")); assertThat(INTERCEPTORS_CALLED, hasItems(EchoInterceptor1.class, EchoInterceptor2.class)); } @@ -99,15 +89,21 @@ public static class EchoService { /** * Echo the message back to the caller. * - * @param request the echo request containing the message to echo - * @param observer the call response + * @param request the echo request containing the message to echo + * @param observer the call response */ @Unary(name = "Echo") public void echo(Echo.EchoRequest request, StreamObserver observer) { - String message = request.getMessage(); - Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); - complete(observer, response); + try { + validateContext(); + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } catch (IllegalStateException e) { + observer.onError(e); + } } + } @GrpcInterceptor @@ -118,6 +114,7 @@ public static class EchoInterceptor1 implements ServerInterceptor { public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + validateContext(); INTERCEPTORS_CALLED.add(getClass()); return next.startCall(call, headers); } @@ -132,8 +129,16 @@ public static class EchoInterceptor2 implements ServerInterceptor { public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + validateContext(); INTERCEPTORS_CALLED.add(getClass()); return next.startCall(call, headers); } } + + private static void validateContext() { + Context context = Context.current(); + if (context == null || SERVICE_DESCRIPTOR_KEY.get() == null) { + throw new IllegalStateException("Invalid context"); + } + } } diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java similarity index 81% rename from microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java rename to webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java index 70327ca94b9..dca134939c5 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/ContextSettingServerInterceptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java @@ -14,15 +14,13 @@ * limitations under the License. */ -package io.helidon.microprofile.grpc.server; +package io.helidon.webserver.grpc; import java.util.HashMap; import java.util.Map; import io.helidon.common.Weight; import io.helidon.grpc.core.InterceptorWeights; -import io.helidon.webserver.grpc.GrpcMethodDescriptor; -import io.helidon.webserver.grpc.GrpcServiceDescriptor; import io.grpc.Context; import io.grpc.Contexts; @@ -58,14 +56,15 @@ static ContextSettingServerInterceptor create() { @Override @SuppressWarnings("unchecked") - public ServerCall.Listener interceptCall(ServerCall call, - Metadata headers, - ServerCallHandler next) { + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { Context context = Context.current(); String fullMethodName = call.getMethodDescriptor().getFullMethodName(); String methodName = extractMethodName(fullMethodName); - GrpcMethodDescriptor methodDescriptor = serviceDescriptor.method(methodName); + GrpcMethodDescriptor methodDescriptor = + (GrpcMethodDescriptor) serviceDescriptor.method(methodName); Map, Object> contextMap = new HashMap<>(); // apply context keys from the service followed by the method @@ -74,13 +73,13 @@ public ServerCall.Listener interceptCall(ServerCall, Object> entry : contextMap.entrySet()) { - Context.Key key = (Context.Key) entry.getKey(); - context = context.withValue(key, entry.getValue()); - } + // set up context from gRPC API + for (Map.Entry, Object> entry : contextMap.entrySet()) { + Context.Key key = (Context.Key) entry.getKey(); + context = context.withValue(key, entry.getValue()); } + // intercept with new context return Contexts.interceptCall(context, call, headers, next); } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index 15f8b21723f..054ffa04460 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -38,12 +38,26 @@ private GrpcServiceRoute(String serviceName, List> routes) { this.routes = routes; } + /** + * Creates a gRPC route for an instance of {@link GrpcService}. + * A server interceptor chain will not be automatically associated + * with calls to this service. + * + * @param service the service + * @return the route + */ static GrpcRoute create(GrpcService service) { Routing svcRouter = new Routing(service); service.update(svcRouter); return svcRouter.build(); } + /** + * Creates a gRPC route for an instance of {@link BindableServiceImpl}. + * + * @param service the service + * @return the route + */ static GrpcRoute create(BindableService service) { ServerServiceDefinition definition = service.bindService(); String serviceName = definition.getServiceDescriptor().getName(); @@ -53,8 +67,15 @@ static GrpcRoute create(BindableService service) { return new GrpcServiceRoute(serviceName, routes); } + /** + * Creates a gRPC route for an instance CDI bean annotated with {@link @Grpc}. + * + * @param service the service + * @return the route + */ static GrpcRoute create(GrpcServiceDescriptor service) { - WeightedBag interceptors = WeightedBag.create(); // TODO + WeightedBag interceptors = WeightedBag.create(); + interceptors.add(ContextSettingServerInterceptor.create()); return create(BindableServiceImpl.create(service, interceptors)); } From 67e99f604edfaee2edbc846ad89fb2d76b31fba9 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 13 Jun 2024 10:45:32 -0400 Subject: [PATCH 10/30] Adds support for a simplified SPI with support for GrpcMpExtension. --- .../grpc/server/GrpcMpCdiExtension.java | 45 +++++++++++++ .../grpc/server/spi/GrpcMpContext.java | 51 ++++++++++++++ .../grpc/server/spi/GrpcMpExtension.java | 29 ++++++++ .../grpc/server/spi/package-info.java | 20 ++++++ .../server/src/main/java/module-info.java | 29 +++++--- .../grpc/server/GrpcMpExtensionTest.java | 66 +++++++++++++++++++ 6 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java create mode 100644 microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java create mode 100644 microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcMpExtensionTest.java diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java index f4b72e23590..3463c0a656a 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java @@ -17,10 +17,16 @@ package io.helidon.microprofile.grpc.server; import java.lang.annotation.Annotation; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; import io.helidon.microprofile.grpc.core.Grpc; +import io.helidon.microprofile.grpc.server.spi.GrpcMpContext; +import io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.grpc.GrpcService; @@ -34,6 +40,7 @@ import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.Extension; +import org.eclipse.microprofile.config.ConfigProvider; /** * A CDI extension that will discover and register gRPC routes. @@ -44,6 +51,8 @@ public class GrpcMpCdiExtension implements Extension { private void discoverRoutes(@Observes @Initialized(ApplicationScoped.class) Object event, BeanManager beanManager) { GrpcRouting.Builder routingBuilder = discoverGrpcRouting(beanManager); + Config config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); + loadExtensions(beanManager, config, routingBuilder); ServerCdiExtension extension = beanManager.getExtension(ServerCdiExtension.class); extension.addRouting(routingBuilder); } @@ -116,4 +125,40 @@ private void register(Object service, GrpcRouting.Builder builder, Class cls, () -> "Discovered type is not a properly annotated gRPC service " + service.getClass()); } } + + /** + * Load any instances of {@link GrpcMpExtension} discovered by the {@link ServiceLoader} + * and allow them to further configure the gRPC server. + * + * @param beanManager the {@link BeanManager} + * @param config the Helidon configuration + * @param routingBuilder the {@link GrpcRouting.Builder} + */ + private void loadExtensions(BeanManager beanManager, + Config config, + GrpcRouting.Builder routingBuilder) { + GrpcMpContext context = new GrpcMpContext() { + @Override + public Config config() { + return config; + } + + @Override + public GrpcRouting.Builder routing() { + return routingBuilder; + } + + @Override + public BeanManager beanManager() { + return beanManager; + } + }; + + HelidonServiceLoader.create(ServiceLoader.load(GrpcMpExtension.class)) + .forEach(ext -> ext.configure(context)); + beanManager.createInstance() + .select(GrpcMpExtension.class) + .stream() + .forEach(ext -> ext.configure(context)); + } } diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java new file mode 100644 index 00000000000..ddacbba5db3 --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpContext.java @@ -0,0 +1,51 @@ +/* + * 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.server.spi; + +import io.helidon.config.Config; +import io.helidon.webserver.grpc.GrpcRouting; + +import jakarta.enterprise.inject.spi.BeanManager; + +/** + * A context to allow a microprofile gRPC server extensions to configure additional + * services or components for the gRPC server or use the CDI bean manager. + */ +public interface GrpcMpContext { + + /** + * Obtain the Helidon configuration. + * + * @return the Helidon configuration + */ + Config config(); + + /** + * Obtain the {@link GrpcRouting.Builder} to allow modifications + * to be made to the routing before the server is configured. + * + * @return the {@link GrpcRouting.Builder} + */ + GrpcRouting.Builder routing(); + + /** + * Obtain the {@link BeanManager}. + * + * @return the {@link BeanManager} + */ + BeanManager beanManager(); +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java new file mode 100644 index 00000000000..cf44c3252ed --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/GrpcMpExtension.java @@ -0,0 +1,29 @@ +/* + * 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.server.spi; + +/** + * Microprofile service to extend features of the gRPC server. + */ +public interface GrpcMpExtension { + /** + * Allow the service to add configuration through the context. + * + * @param context context to obtain configuration objects + */ + void configure(GrpcMpContext context); +} diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java new file mode 100644 index 00000000000..5cf2f30af3d --- /dev/null +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Microprofile gRPC server implementation. + */ +package io.helidon.microprofile.grpc.server.spi; diff --git a/microprofile/grpc/server/src/main/java/module-info.java b/microprofile/grpc/server/src/main/java/module-info.java index c4b85ea5643..27af88d469d 100644 --- a/microprofile/grpc/server/src/main/java/module-info.java +++ b/microprofile/grpc/server/src/main/java/module-info.java @@ -14,22 +14,37 @@ * limitations under the License. */ +import io.helidon.common.features.api.Aot; +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; import io.helidon.microprofile.grpc.server.GrpcMpCdiExtension; /** * gRPC microprofile server module */ +@Feature(value = "gRPC", + description = "Helidon MP gRPC implementation", + in = HelidonFlavor.MP, + path = "gRPC" +) +@Aot(false) module io.helidon.microprofile.grpc.server { exports io.helidon.microprofile.grpc.server; + exports io.helidon.microprofile.grpc.server.spi; + requires transitive io.helidon.grpc.core; requires transitive io.helidon.webserver.grpc; requires transitive io.helidon.microprofile.grpc.core; requires io.helidon.common; - requires io.helidon.grpc.core; - requires io.helidon.microprofile.server; + requires io.helidon.common.configurable; + requires io.helidon.common.context; + requires io.helidon.common.features.api; requires io.helidon.config.mp; requires io.helidon.config.objectmapping; + requires io.helidon.config; + requires io.helidon.config.metadata; + requires io.helidon.microprofile.server; requires io.grpc; requires io.grpc.inprocess; @@ -39,16 +54,10 @@ requires java.logging; requires microprofile.health.api; - requires io.helidon.common.configurable; - requires io.helidon.config; - requires io.helidon.config.metadata; - requires io.helidon.common.context; - uses GrpcMpCdiExtension; - uses io.helidon.microprofile.grpc.server.AnnotatedServiceConfigurer; + uses io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; - provides jakarta.enterprise.inject.spi.Extension - with GrpcMpCdiExtension; + provides jakarta.enterprise.inject.spi.Extension with GrpcMpCdiExtension; // needed when running with modules - to make private methods accessible opens io.helidon.microprofile.grpc.server to weld.core.impl, io.helidon.microprofile.cdi; diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcMpExtensionTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcMpExtensionTest.java new file mode 100644 index 00000000000..a4995093904 --- /dev/null +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/GrpcMpExtensionTest.java @@ -0,0 +1,66 @@ +/* + * 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.server; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.grpc.server.spi.GrpcMpContext; +import io.helidon.microprofile.grpc.server.spi.GrpcMpExtension; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; + +@HelidonTest +@AddExtension(GrpcMpCdiExtension.class) +public class GrpcMpExtensionTest { + + private static final Set> EXTENSIONS_LOADED = new HashSet<>(); + + @Test + void testExtensions() { + assertThat(EXTENSIONS_LOADED, hasItems(Extension1.class, Extension2.class)); + + } + + @ApplicationScoped + public static class Extension1 implements GrpcMpExtension { + + @Override + public void configure(GrpcMpContext context) { + if (context.beanManager() != null && context.routing() != null) { + EXTENSIONS_LOADED.add(getClass()); + } + } + } + + @ApplicationScoped + public static class Extension2 implements GrpcMpExtension { + + @Override + public void configure(GrpcMpContext context) { + if (context.beanManager() != null && context.routing() != null) { + EXTENSIONS_LOADED.add(getClass()); + } + } + } +} From 34facc9ae923d477f937d6e4c3db5d02403e0777 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 14 Jun 2024 14:25:55 -0400 Subject: [PATCH 11/30] Adds initial support for gRPC tracing. --- webserver/grpc/pom.xml | 4 + .../webserver/grpc/BindableServiceImpl.java | 73 +---- .../grpc/ContextSettingServerInterceptor.java | 1 - .../webserver/grpc/GrpcConfigBlueprint.java | 1 + .../webserver/grpc/GrpcServiceRoute.java | 7 + .../helidon/webserver/grpc/GrpcTracing.java | 292 ++++++++++++++++++ .../webserver/grpc/GrpcTracingConfig.java | 194 ++++++++++++ .../webserver/grpc/GrpcTracingContext.java | 46 +++ .../webserver/grpc/GrpcTracingName.java | 33 ++ .../grpc/ServerRequestAttribute.java | 47 +++ webserver/grpc/src/main/java/module-info.java | 1 + 11 files changed, 632 insertions(+), 67 deletions(-) create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingContext.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingName.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServerRequestAttribute.java diff --git a/webserver/grpc/pom.xml b/webserver/grpc/pom.xml index 7ff8a3c06fc..e22f30750bc 100644 --- a/webserver/grpc/pom.xml +++ b/webserver/grpc/pom.xml @@ -47,6 +47,10 @@ io.helidon.grpc helidon-grpc-core + + io.helidon.tracing + helidon-tracing + io.grpc grpc-core diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java index 832ca9d3d3d..4bbfa0fac99 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java @@ -18,10 +18,6 @@ import java.util.LinkedHashSet; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionException; -import java.util.function.BiConsumer; -import java.util.function.Supplier; import io.helidon.grpc.core.InterceptorWeights; import io.helidon.grpc.core.WeightedBag; @@ -35,7 +31,6 @@ import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; import io.grpc.protobuf.ProtoFileDescriptorSupplier; -import io.grpc.stub.StreamObserver; /** * A {@link BindableService} implementation that creates {@link ServerServiceDefinition} @@ -50,11 +45,11 @@ class BindableServiceImpl implements BindableService { /** * The global interceptors to apply. */ - private final WeightedBag globalInterceptors; + private final WeightedBag interceptors; private BindableServiceImpl(GrpcServiceDescriptor descriptor, WeightedBag interceptors) { this.descriptor = descriptor; - this.globalInterceptors = interceptors.copyMe(); + this.interceptors = interceptors.copyMe(); } /** @@ -73,18 +68,16 @@ static BindableServiceImpl create(GrpcServiceDescriptor descriptor, WeightedBag< @SuppressWarnings("unchecked") @Override public ServerServiceDefinition bindService() { - ServiceDescriptor.Builder serviceDescriptorBuilder = - ServiceDescriptor.newBuilder(descriptor.fullName()); + ServiceDescriptor.Builder serviceDescriptorBuilder = ServiceDescriptor.newBuilder(descriptor.fullName()); if (descriptor.proto() != null) { serviceDescriptorBuilder.setSchemaDescriptor((ProtoFileDescriptorSupplier) descriptor::proto); } - descriptor.methods() - .forEach(method -> serviceDescriptorBuilder.addMethod(method.descriptor())); + descriptor.methods().forEach(method -> serviceDescriptorBuilder.addMethod(method.descriptor())); ServerServiceDefinition.Builder builder = ServerServiceDefinition.builder(serviceDescriptorBuilder.build()); descriptor.methods() - .forEach(method -> builder.addMethod((MethodDescriptor) method.descriptor(), - wrapCallHandler(method))); + .forEach(method -> builder.addMethod((MethodDescriptor) method.descriptor(), + wrapCallHandler(method))); return builder.build(); } @@ -95,7 +88,7 @@ private ServerCallHandler wrapCallHandler(GrpcMethodD ServerCallHandler handler = method.callHandler(); WeightedBag priorityServerInterceptors = WeightedBag.create(InterceptorWeights.USER); - priorityServerInterceptors.addAll(globalInterceptors); + priorityServerInterceptors.addAll(interceptors); priorityServerInterceptors.addAll(descriptor.interceptors()); priorityServerInterceptors.addAll(method.interceptors()); List interceptors = priorityServerInterceptors.stream().toList(); @@ -119,58 +112,6 @@ private ServerCallHandler wrapCallHandler(GrpcMethodD return handler; } - static Supplier createSupplier(Callable callable) { - return new CallableSupplier<>(callable); - } - - static class CallableSupplier implements Supplier { - private final Callable callable; - - CallableSupplier(Callable callable) { - this.callable = callable; - } - - @Override - public T get() { - try { - return callable.call(); - } catch (Exception e) { - throw new CompletionException(e.getMessage(), e); - } - } - } - - static BiConsumer completeWithResult(StreamObserver observer) { - return new CompletionAction<>(observer, true); - } - - static BiConsumer completeWithoutResult(StreamObserver observer) { - return new CompletionAction<>(observer, false); - } - - static class CompletionAction implements BiConsumer { - private StreamObserver observer; - private boolean sendResult; - - CompletionAction(StreamObserver observer, boolean sendResult) { - this.observer = observer; - this.sendResult = sendResult; - } - - @Override - @SuppressWarnings("unchecked") - public void accept(T result, Throwable error) { - if (error != null) { - observer.onError(error); - } else { - if (sendResult) { - observer.onNext((U) result); - } - observer.onCompleted(); - } - } - } - /** * A {@link ServerCallHandler} that wraps a {@link ServerCallHandler} with * a {@link ServerInterceptor}. diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java index dca134939c5..0c187f61eda 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java @@ -59,7 +59,6 @@ static ContextSettingServerInterceptor create() { public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { - Context context = Context.current(); String fullMethodName = call.getMethodDescriptor().getFullMethodName(); String methodName = extractMethodName(fullMethodName); diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java index 286a8111bee..32cbb953fb1 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcConfigBlueprint.java @@ -23,6 +23,7 @@ @Prototype.Configured @Prototype.Provides(ProtocolConfig.class) interface GrpcConfigBlueprint extends ProtocolConfig { + /** * Protocol configuration type. * diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index 054ffa04460..e5dc87077fe 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -22,6 +22,7 @@ import io.helidon.grpc.core.WeightedBag; import io.helidon.http.HttpPrologue; import io.helidon.http.PathMatchers; +import io.helidon.tracing.Tracer; import com.google.protobuf.Descriptors; import io.grpc.BindableService; @@ -69,13 +70,19 @@ static GrpcRoute create(BindableService service) { /** * Creates a gRPC route for an instance CDI bean annotated with {@link @Grpc}. + * Registers global interceptors for the route. * * @param service the service * @return the route */ static GrpcRoute create(GrpcServiceDescriptor service) { WeightedBag interceptors = WeightedBag.create(); + + // set up context and tracing interceptors interceptors.add(ContextSettingServerInterceptor.create()); + Tracer tracer = Tracer.global(); + interceptors.add(GrpcTracing.create(tracer, GrpcTracingConfig.create())); + return create(BindableServiceImpl.create(service, interceptors)); } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java new file mode 100644 index 00000000000..d0452de41e4 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.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.webserver.grpc; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import io.helidon.common.Weight; +import io.helidon.grpc.core.ContextKeys; +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.tracing.HeaderProvider; +import io.helidon.tracing.Span; +import io.helidon.tracing.SpanContext; +import io.helidon.tracing.Tracer; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * A {@link ServerInterceptor} that adds tracing to gRPC service calls. + */ +@Weight(InterceptorWeights.TRACING) +public class GrpcTracing implements ServerInterceptor { + /** + * The Open Tracing {@link Tracer}. + */ + private final Tracer tracer; + + /* + * GRPC method name + */ + private final GrpcTracingName operationNameConstructor; + + /** + * + */ + private final boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private final boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private final Set tracedAttributes; + + private GrpcTracing(Tracer tracer, GrpcTracingConfig tracingConfig) { + this.tracer = tracer; + this.operationNameConstructor = tracingConfig.operationNameConstructor(); + this.streaming = tracingConfig.isStreaming(); + this.verbose = tracingConfig.isVerbose(); + this.tracedAttributes = tracingConfig.tracedAttributes(); + } + + /** + * Create a {@link GrpcTracing} interceptor. + * + * @param tracer the Open Tracing {@link Tracer} + * @param config the tracing configuration + * + * @return a {@link GrpcTracing} interceptor instance + */ + static GrpcTracing create(Tracer tracer, GrpcTracingConfig config) { + return new GrpcTracing(tracer, config); + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, + ServerCallHandler next) { + Map headerMap = new HashMap<>(); + + for (String key : headers.keys()) { + if (!key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + String value = headers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + headerMap.put(key, value); + } + } + + String operationName = operationNameConstructor.name(call.getMethodDescriptor()); + Span span = getSpanFromHeaders(headerMap, operationName); + + if (tracedAttributes.contains(ServerRequestAttribute.ALL)) { + span.tag("grpc.method_type", call.getMethodDescriptor().getType().toString()); + span.tag("grpc.method_name", call.getMethodDescriptor().getFullMethodName()); + span.tag("grpc.call_attributes", call.getAttributes().toString()); + addMetadata(headers, span); + } else { + for (ServerRequestAttribute attr : tracedAttributes) { + switch (attr) { + case METHOD_TYPE: + span.tag("grpc.method_type", call.getMethodDescriptor().getType().toString()); + break; + case METHOD_NAME: + span.tag("grpc.method_name", call.getMethodDescriptor().getFullMethodName()); + break; + case CALL_ATTRIBUTES: + span.tag("grpc.call_attributes", call.getAttributes().toString()); + break; + case HEADERS: + addMetadata(headers, span); + break; + default: + // ignored - should never happen + } + } + } + + Context grpcContext = Context.current(); + + updateContext(ContextKeys.HELIDON_CONTEXT.get(grpcContext), span); + io.helidon.common.context.Contexts.context().ifPresent(ctx -> updateContext(ctx, span)); + + Context ctxWithSpan = grpcContext.withValue(GrpcTracingContext.SPAN_KEY, span); + ServerCall.Listener listenerWithContext = Contexts.interceptCall(ctxWithSpan, call, headers, next); + + return new TracingListener<>(listenerWithContext, span); + } + + private void updateContext(io.helidon.common.context.Context context, Span span) { + if (context != null) { + if (!context.get(Tracer.class).isPresent()) { + context.register(tracer); + } + + context.register(span.context()); + } + } + + private void addMetadata(Metadata headers, Span span) { + // copy the headers and make sure that the AUTHORIZATION header + // is removed as we do not want auth details to appear in tracing logs + Metadata metadata = new Metadata(); + + metadata.merge(headers); + metadata.removeAll(ContextKeys.AUTHORIZATION); + + span.tag("grpc.headers", metadata.toString()); + } + + private Span getSpanFromHeaders(Map headers, String operationName) { + Span span; + + try { + SpanContext parentSpanCtx = tracer.extract(new MapHeaderProvider(headers)) + .orElse(null); + + if (parentSpanCtx == null) { + span = tracer.spanBuilder(operationName) + .start(); + } else { + span = tracer.spanBuilder(operationName) + .parent(parentSpanCtx) + .start(); + } + } catch (IllegalArgumentException iae) { + span = tracer.spanBuilder(operationName) + .tag("Error", "Extract failed and an IllegalArgumentException was thrown") + .start(); + } + + return span; + } + + private static boolean isCaseInsensitive(Map headers) { + return (headers instanceof TreeMap + && ((TreeMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER) + || (headers instanceof ConcurrentSkipListMap + && ((ConcurrentSkipListMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER); + } + + /** + * A {@link ServerCall.Listener} to apply details to a tracing {@link Span} at various points + * in a call lifecycle. + * + * @param the type of the request + */ + private class TracingListener + extends ForwardingServerCallListener.SimpleForwardingServerCallListener { + + private final Span span; + + private TracingListener(ServerCall.Listener delegate, Span span) { + super(delegate); + this.span = span; + } + + @Override + public void onMessage(ReqT message) { + if (streaming || verbose) { + span.addEvent("onMessage", Map.of("Message received", message)); + } + + delegate().onMessage(message); + } + + @Override + public void onHalfClose() { + if (streaming) { + span.addEvent("Client finished sending messages"); + } + + delegate().onHalfClose(); + } + + @Override + public void onCancel() { + span.addEvent("Call cancelled"); + + try { + delegate().onCancel(); + } finally { + span.end(); + } + } + + @Override + public void onComplete() { + if (verbose) { + span.addEvent("Call completed"); + } + + try { + delegate().onComplete(); + } finally { + span.end(); + } + } + } + + private static class MapHeaderProvider implements HeaderProvider { + private final Map headers; + + MapHeaderProvider(Map headers) { + if (isCaseInsensitive(headers)) { + this.headers = headers; + } else { + // headers is not updated, so TreeMap is OK--no need for concurrency. + this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + this.headers.putAll(headers); + } + } + + @Override + public Iterable keys() { + return headers.keySet(); + } + + @Override + public Optional get(String key) { + return Optional.ofNullable(headers.get(key)); + } + + @Override + public Iterable getAll(String key) { + // either map the value to list, or get empty list + return get(key).map(List::of) + .orElseGet(List::of); + } + + @Override + public boolean contains(String key) { + return headers.containsKey(key); + } + } +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java new file mode 100644 index 00000000000..9c3a2415e7e --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java @@ -0,0 +1,194 @@ +/* + * 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.webserver.grpc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import io.grpc.MethodDescriptor; + +/** + * The configuration for gRPC tracing. + */ +public class GrpcTracingConfig { + /** + * A flag indicating whether to log streaming. + */ + private final GrpcTracingName operationNameConstructor; + + /** + * A flag indicating verbose logging. + */ + private final boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private final boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private final Set tracedAttributes; + + /** + * Private constructor called by the {@link Builder}. + * + * @param operationNameConstructor the operation name constructor + * @param streaming flag indicating whether to log streaming + * @param verbose flag indicating verbose logging + * @param tracedAttributes the set of attributes to log in spans + */ + GrpcTracingConfig(GrpcTracingName operationNameConstructor, + Set tracedAttributes, + boolean streaming, + boolean verbose) { + this.operationNameConstructor = operationNameConstructor; + this.tracedAttributes = tracedAttributes; + this.streaming = streaming; + this.verbose = verbose; + } + + /** + * @return the configured verbose. + */ + public boolean isVerbose() { + return verbose; + } + + /** + * @return the configured streaming. + */ + public boolean isStreaming() { + return streaming; + } + + /** + * @return the set of configured tracedAttributes. + */ + public Set tracedAttributes() { + return tracedAttributes; + } + + /** + * @return the configured operationNameConstructor. + */ + public GrpcTracingName operationNameConstructor() { + return operationNameConstructor; + } + + /** + * Create a builder of {@link GrpcTracingConfig} instances. + * + * @return a builder of {@link GrpcTracingConfig} instances. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Create a default {@link GrpcTracingConfig} instances. + * + * @return a default {@link GrpcTracingConfig} instances. + */ + public static GrpcTracingConfig create() { + return builder().build(); + } + + /** + * Builds the configuration of a tracer. + */ + public static class Builder { + /** + * Creates a Builder with default configuration. + */ + Builder() { + operationNameConstructor = MethodDescriptor::getFullMethodName; + streaming = false; + verbose = false; + tracedAttributes = Collections.emptySet(); + } + + /** + * @param operationNameConstructor for all spans + * @return this Builder with configured operation name + */ + public Builder withOperationName(GrpcTracingName operationNameConstructor) { + this.operationNameConstructor = operationNameConstructor; + return this; + } + + /** + * @param attributes to set as tags on server spans + * @return this Builder configured to trace request + * attributes + */ + public Builder withTracedAttributes(ServerRequestAttribute... attributes) { + tracedAttributes = new HashSet<>(Arrays.asList(attributes)); + return this; + } + + /** + * Logs streaming events to server spans. + * + * @return this Builder configured to log streaming events + */ + public Builder withStreaming() { + streaming = true; + return this; + } + + /** + * Logs all request life-cycle events to server spans. + * + * @return this Builder configured to be verbose + */ + public Builder withVerbosity() { + verbose = true; + return this; + } + + /** + * @return a GrpcTracingConfig with this Builder's configuration + */ + public GrpcTracingConfig build() { + return new GrpcTracingConfig(operationNameConstructor, tracedAttributes, streaming, verbose); + } + + /** + * A flag indicating whether to log streaming. + */ + private GrpcTracingName operationNameConstructor; + + /** + * A flag indicating verbose logging. + */ + private boolean streaming; + + /** + * A flag indicating verbose logging. + */ + private boolean verbose; + + /** + * The set of attributes to log in spans. + */ + private Set tracedAttributes; + } +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingContext.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingContext.java new file mode 100644 index 00000000000..1056ef41065 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingContext.java @@ -0,0 +1,46 @@ +/* + * 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.webserver.grpc; + +import java.util.Optional; + +import io.helidon.tracing.Span; + +import io.grpc.Context; + +/** + * Contextual information related to Tracing. + */ +public final class GrpcTracingContext { + private static final String SPAN_KEY_NAME = "io.helidon.tracing.active-span"; + + /** + * Context key for Span instance. + */ + public static final Context.Key SPAN_KEY = Context.key(SPAN_KEY_NAME); + + /** + * Get the current active span associated with the context. + * + * @return span if one is in current context + */ + public static Optional activeSpan() { + return Optional.ofNullable(SPAN_KEY.get()); + } + + private GrpcTracingContext() { + } +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingName.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingName.java new file mode 100644 index 00000000000..0f330a51e17 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingName.java @@ -0,0 +1,33 @@ +/* + * 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.webserver.grpc; + +import io.grpc.MethodDescriptor; + +/** + * Name generator for span operation name. + */ +@FunctionalInterface +public interface GrpcTracingName { + + /** + * Constructs a span's operation name from the gRPC method. + * + * @param method method to extract a name from + * @return operation name + */ + String name(MethodDescriptor method); +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServerRequestAttribute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServerRequestAttribute.java new file mode 100644 index 00000000000..3172f37caa5 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ServerRequestAttribute.java @@ -0,0 +1,47 @@ +/* + * 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.webserver.grpc; + +/** + * An enum representing different types of gRPC request + * attribute that can be added to tracing logs. + */ +public enum ServerRequestAttribute { + /** + * Log the request headers. + */ + HEADERS, + + /** + * Log the method type. + */ + METHOD_TYPE, + + /** + * log the method name. + */ + METHOD_NAME, + + /** + * log the call attributes. + */ + CALL_ATTRIBUTES, + + /** + * log all attributes. + */ + ALL +} diff --git a/webserver/grpc/src/main/java/module-info.java b/webserver/grpc/src/main/java/module-info.java index 3a289cdab0e..d8768ec22ff 100644 --- a/webserver/grpc/src/main/java/module-info.java +++ b/webserver/grpc/src/main/java/module-info.java @@ -34,6 +34,7 @@ requires io.grpc.protobuf.lite; requires io.helidon.builder.api; requires io.helidon.webserver.http2; + requires io.helidon.tracing; requires static io.helidon.common.features.api; From 091c3a469ac24f0279ea6522be70d591f8014249 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 17 Jun 2024 14:01:47 -0400 Subject: [PATCH 12/30] Adds support for gRPC tracing configuration. --- .../helidon/grpc/core/InterceptorWeights.java | 2 +- .../io/helidon/grpc/core/ResponseHelper.java | 1 + .../helidon/grpc/core/SafeStreamObserver.java | 3 +- .../grpc/server/GrpcMpCdiExtension.java | 8 +- .../grpc/server/EchoServiceTest.java | 18 ++ .../src/test/resources/application.yaml | 4 + webserver/grpc/pom.xml | 4 + .../grpc/ContextSettingServerInterceptor.java | 15 +- .../webserver/grpc/GrpcProtocolSelector.java | 1 + .../helidon/webserver/grpc/GrpcRouting.java | 5 +- .../webserver/grpc/GrpcServiceRoute.java | 23 ++- .../webserver/grpc/GrpcTracingConfig.java | 194 ------------------ .../grpc/GrpcTracingConfigBlueprint.java | 62 ++++++ ...acing.java => GrpcTracingInterceptor.java} | 46 ++--- 14 files changed, 147 insertions(+), 239 deletions(-) delete mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfigBlueprint.java rename webserver/grpc/src/main/java/io/helidon/webserver/grpc/{GrpcTracing.java => GrpcTracingInterceptor.java} (87%) diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java index 2975789be85..29cbf7a48f2 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java @@ -36,7 +36,7 @@ public class InterceptorWeights { * interceptors so that they can trace and gather metrics on the whole call * stack of remaining interceptors. */ - public static final int TRACING = CONTEXT + 1; + public static final int TRACING = CONTEXT - 1; /** * Security authentication weight. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java index e95afb2a83a..c17035bc5bb 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java @@ -33,6 +33,7 @@ * A number of helper methods to handle sending responses to a {@link StreamObserver}. */ public final class ResponseHelper { + private ResponseHelper() { } diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java index 31cfb1db7ef..728403f94f8 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java @@ -27,8 +27,7 @@ * * @param the type of response expected */ -public class SafeStreamObserver - implements StreamObserver { +public class SafeStreamObserver implements StreamObserver { /** * Create a {@link SafeStreamObserver} that wraps diff --git a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java index 3463c0a656a..c2980b16dc5 100644 --- a/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java +++ b/microprofile/grpc/server/src/main/java/io/helidon/microprofile/grpc/server/GrpcMpCdiExtension.java @@ -30,6 +30,7 @@ import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.grpc.GrpcService; +import io.helidon.webserver.grpc.GrpcTracingConfig; import io.grpc.BindableService; import jakarta.enterprise.context.ApplicationScoped; @@ -49,9 +50,11 @@ public class GrpcMpCdiExtension implements Extension { private static final Logger LOGGER = Logger.getLogger(GrpcMpCdiExtension.class.getName()); + private Config config; + private void discoverRoutes(@Observes @Initialized(ApplicationScoped.class) Object event, BeanManager beanManager) { + config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); GrpcRouting.Builder routingBuilder = discoverGrpcRouting(beanManager); - Config config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); loadExtensions(beanManager, config, routingBuilder); ServerCdiExtension extension = beanManager.getExtension(ServerCdiExtension.class); extension.addRouting(routingBuilder); @@ -119,7 +122,8 @@ private boolean hasGrpcQualifier(Bean bean) { private void register(Object service, GrpcRouting.Builder builder, Class cls, BeanManager beanManager) { GrpcServiceBuilder serviceBuilder = GrpcServiceBuilder.create(cls, () -> service, beanManager); if (serviceBuilder.isAnnotatedService()) { - builder.service(serviceBuilder.build()); + GrpcTracingConfig tracingConfig = GrpcTracingConfig.create(config.get("tracing.grpc")); + builder.service(serviceBuilder.build(), tracingConfig); } else { LOGGER.log(Level.WARNING, () -> "Discovered type is not a properly annotated gRPC service " + service.getClass()); diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java index aae6e646711..5dfc01772c8 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/EchoServiceTest.java @@ -27,6 +27,7 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; +import io.helidon.grpc.core.ContextKeys; import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.GrpcInterceptor; import io.helidon.microprofile.grpc.core.GrpcInterceptorBinding; @@ -34,6 +35,7 @@ import io.helidon.microprofile.grpc.core.Unary; import io.helidon.microprofile.grpc.server.test.Echo; import io.helidon.microprofile.grpc.server.test.EchoServiceGrpc; +import io.helidon.tracing.Tracer; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; @@ -115,6 +117,7 @@ public ServerCall.Listener interceptCall(ServerCall next) { validateContext(); + validateTracing(); INTERCEPTORS_CALLED.add(getClass()); return next.startCall(call, headers); } @@ -130,15 +133,30 @@ public ServerCall.Listener interceptCall(ServerCall next) { validateContext(); + validateTracing(); INTERCEPTORS_CALLED.add(getClass()); return next.startCall(call, headers); } } + /** + * Validates that a gRPC context is present. + */ private static void validateContext() { Context context = Context.current(); if (context == null || SERVICE_DESCRIPTOR_KEY.get() == null) { throw new IllegalStateException("Invalid context"); } } + + /** + * Validates that tracing is enabled. See {@code resources/application.yaml}. + */ + private static void validateTracing() { + Context context = Context.current(); + io.helidon.common.context.Context helidonContext = ContextKeys.HELIDON_CONTEXT.get(context); + if (helidonContext == null || helidonContext.get(Tracer.class).isEmpty()) { + throw new IllegalStateException("Invalid tracing context"); + } + } } diff --git a/microprofile/grpc/server/src/test/resources/application.yaml b/microprofile/grpc/server/src/test/resources/application.yaml index 00b3aef2e03..7cda6180804 100644 --- a/microprofile/grpc/server/src/test/resources/application.yaml +++ b/microprofile/grpc/server/src/test/resources/application.yaml @@ -28,3 +28,7 @@ server: passphrase: "password" resource: resource-path: "server.p12" + +tracing: + grpc: + enabled: true diff --git a/webserver/grpc/pom.xml b/webserver/grpc/pom.xml index e22f30750bc..55c9c501f5b 100644 --- a/webserver/grpc/pom.xml +++ b/webserver/grpc/pom.xml @@ -82,6 +82,10 @@ hamcrest-all test + + io.helidon.codegen + helidon-codegen + diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java index 0c187f61eda..fb4955c4f6e 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java @@ -18,8 +18,10 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import io.helidon.common.Weight; +import io.helidon.grpc.core.ContextKeys; import io.helidon.grpc.core.InterceptorWeights; import io.grpc.Context; @@ -60,14 +62,21 @@ public ServerCall.Listener interceptCall(ServerCall next) { Context context = Context.current(); + + // set Helidon context into gRPC context + Optional helidonContext = + io.helidon.common.context.Contexts.context(); + context = Context.current().withValue(ContextKeys.HELIDON_CONTEXT, + helidonContext.orElseGet(io.helidon.common.context.Context::create)); + + // method info String fullMethodName = call.getMethodDescriptor().getFullMethodName(); String methodName = extractMethodName(fullMethodName); GrpcMethodDescriptor methodDescriptor = (GrpcMethodDescriptor) serviceDescriptor.method(methodName); - Map, Object> contextMap = new HashMap<>(); - // apply context keys from the service followed by the method - // so that the method can override any service keys + // apply context keys from the service followed by the method for overrides + Map, Object> contextMap = new HashMap<>(); contextMap.putAll(serviceDescriptor.context()); contextMap.putAll(methodDescriptor.context()); contextMap.put(GrpcServiceDescriptor.SERVICE_DESCRIPTOR_KEY, serviceDescriptor); diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java index 806e08bf767..b4be7eafbb6 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java @@ -34,6 +34,7 @@ * Sub-protocol selector for HTTP/2. */ public class GrpcProtocolSelector implements Http2SubProtocolSelector { + private GrpcProtocolSelector() { } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index d19256ba9cb..fd5acc057ff 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -160,15 +160,16 @@ public Builder service(BindableService service) { * Configure a service using a {@link io.grpc.ServiceDescriptor}. * * @param service service to add + * @param tracingConfig tracing configuration * @return updated builder */ - public Builder service(GrpcServiceDescriptor service) { + public Builder service(GrpcServiceDescriptor service, GrpcTracingConfig tracingConfig) { String name = service.name(); if (services.containsKey(name)) { throw new IllegalArgumentException("Attempted to register service name " + name + " multiple times"); } services.put(name, service); - return route(GrpcServiceRoute.create(service)); + return route(GrpcServiceRoute.create(service, tracingConfig)); } /** diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index e5dc87077fe..0c6ca7b2c07 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -31,6 +31,7 @@ import io.grpc.stub.ServerCalls; class GrpcServiceRoute extends GrpcRoute { + private final String serviceName; private final List> routes; @@ -54,7 +55,7 @@ static GrpcRoute create(GrpcService service) { } /** - * Creates a gRPC route for an instance of {@link BindableServiceImpl}. + * Creates a gRPC route for an instance of {@link BindableService}. * * @param service the service * @return the route @@ -70,19 +71,19 @@ static GrpcRoute create(BindableService service) { /** * Creates a gRPC route for an instance CDI bean annotated with {@link @Grpc}. - * Registers global interceptors for the route. + * Registers global interceptors for context and tracing on all the routes. * * @param service the service + * @param tracingConfig tracing configuration * @return the route */ - static GrpcRoute create(GrpcServiceDescriptor service) { + static GrpcRoute create(GrpcServiceDescriptor service, GrpcTracingConfig tracingConfig) { WeightedBag interceptors = WeightedBag.create(); - - // set up context and tracing interceptors interceptors.add(ContextSettingServerInterceptor.create()); - Tracer tracer = Tracer.global(); - interceptors.add(GrpcTracing.create(tracer, GrpcTracingConfig.create())); - + if (tracingConfig.enabled()) { + Tracer tracer = Tracer.global(); + interceptors.add(GrpcTracingInterceptor.create(tracer, tracingConfig)); + } return create(BindableServiceImpl.create(service, interceptors)); } @@ -94,11 +95,11 @@ static GrpcRoute create(GrpcServiceDescriptor service) { return route; } } - throw new IllegalStateException("GrpcServiceRoute(" + serviceName + ") accepted prologue, but cannot provide route: " - + prologue); + throw new IllegalStateException("GrpcServiceRoute(" + serviceName + + ") accepted prologue, but cannot provide route: " + prologue); } - public PathMatchers.MatchResult accepts(HttpPrologue prologue) { + PathMatchers.MatchResult accepts(HttpPrologue prologue) { for (Grpc route : routes) { PathMatchers.MatchResult accepts = route.accepts(prologue); if (accepts.accepted()) { diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java deleted file mode 100644 index 9c3a2415e7e..00000000000 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfig.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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.webserver.grpc; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import io.grpc.MethodDescriptor; - -/** - * The configuration for gRPC tracing. - */ -public class GrpcTracingConfig { - /** - * A flag indicating whether to log streaming. - */ - private final GrpcTracingName operationNameConstructor; - - /** - * A flag indicating verbose logging. - */ - private final boolean streaming; - - /** - * A flag indicating verbose logging. - */ - private final boolean verbose; - - /** - * The set of attributes to log in spans. - */ - private final Set tracedAttributes; - - /** - * Private constructor called by the {@link Builder}. - * - * @param operationNameConstructor the operation name constructor - * @param streaming flag indicating whether to log streaming - * @param verbose flag indicating verbose logging - * @param tracedAttributes the set of attributes to log in spans - */ - GrpcTracingConfig(GrpcTracingName operationNameConstructor, - Set tracedAttributes, - boolean streaming, - boolean verbose) { - this.operationNameConstructor = operationNameConstructor; - this.tracedAttributes = tracedAttributes; - this.streaming = streaming; - this.verbose = verbose; - } - - /** - * @return the configured verbose. - */ - public boolean isVerbose() { - return verbose; - } - - /** - * @return the configured streaming. - */ - public boolean isStreaming() { - return streaming; - } - - /** - * @return the set of configured tracedAttributes. - */ - public Set tracedAttributes() { - return tracedAttributes; - } - - /** - * @return the configured operationNameConstructor. - */ - public GrpcTracingName operationNameConstructor() { - return operationNameConstructor; - } - - /** - * Create a builder of {@link GrpcTracingConfig} instances. - * - * @return a builder of {@link GrpcTracingConfig} instances. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Create a default {@link GrpcTracingConfig} instances. - * - * @return a default {@link GrpcTracingConfig} instances. - */ - public static GrpcTracingConfig create() { - return builder().build(); - } - - /** - * Builds the configuration of a tracer. - */ - public static class Builder { - /** - * Creates a Builder with default configuration. - */ - Builder() { - operationNameConstructor = MethodDescriptor::getFullMethodName; - streaming = false; - verbose = false; - tracedAttributes = Collections.emptySet(); - } - - /** - * @param operationNameConstructor for all spans - * @return this Builder with configured operation name - */ - public Builder withOperationName(GrpcTracingName operationNameConstructor) { - this.operationNameConstructor = operationNameConstructor; - return this; - } - - /** - * @param attributes to set as tags on server spans - * @return this Builder configured to trace request - * attributes - */ - public Builder withTracedAttributes(ServerRequestAttribute... attributes) { - tracedAttributes = new HashSet<>(Arrays.asList(attributes)); - return this; - } - - /** - * Logs streaming events to server spans. - * - * @return this Builder configured to log streaming events - */ - public Builder withStreaming() { - streaming = true; - return this; - } - - /** - * Logs all request life-cycle events to server spans. - * - * @return this Builder configured to be verbose - */ - public Builder withVerbosity() { - verbose = true; - return this; - } - - /** - * @return a GrpcTracingConfig with this Builder's configuration - */ - public GrpcTracingConfig build() { - return new GrpcTracingConfig(operationNameConstructor, tracedAttributes, streaming, verbose); - } - - /** - * A flag indicating whether to log streaming. - */ - private GrpcTracingName operationNameConstructor; - - /** - * A flag indicating verbose logging. - */ - private boolean streaming; - - /** - * A flag indicating verbose logging. - */ - private boolean verbose; - - /** - * The set of attributes to log in spans. - */ - private Set tracedAttributes; - } -} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfigBlueprint.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfigBlueprint.java new file mode 100644 index 00000000000..928c9a73f25 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingConfigBlueprint.java @@ -0,0 +1,62 @@ +/* + * 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.webserver.grpc; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +@Prototype.Configured +interface GrpcTracingConfigBlueprint { + + /** + * A flag indicating if tracing is enabled. + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean enabled(); + + /** + * A flag indicating verbose logging. + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean verbose(); + + /** + * A flag indicating streaming logging. + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean streaming(); + + /** + * Operation name constructor. + * + * @return the tracing name + */ + Optional operationNameConstructor(); + + /** + * Set of attributes to trace. + * + * @return set of attributes to trace + */ + Set tracedAttributes(); +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingInterceptor.java similarity index 87% rename from webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java rename to webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingInterceptor.java index d0452de41e4..6e09917fd49 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracing.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcTracingInterceptor.java @@ -44,19 +44,20 @@ * A {@link ServerInterceptor} that adds tracing to gRPC service calls. */ @Weight(InterceptorWeights.TRACING) -public class GrpcTracing implements ServerInterceptor { +public class GrpcTracingInterceptor implements ServerInterceptor { + /** * The Open Tracing {@link Tracer}. */ private final Tracer tracer; - /* - * GRPC method name + /** + * GRPC method name. */ private final GrpcTracingName operationNameConstructor; /** - * + * A flag indicating streaming logging. */ private final boolean streaming; @@ -70,24 +71,23 @@ public class GrpcTracing implements ServerInterceptor { */ private final Set tracedAttributes; - private GrpcTracing(Tracer tracer, GrpcTracingConfig tracingConfig) { + private GrpcTracingInterceptor(Tracer tracer, GrpcTracingConfig tracingConfig) { this.tracer = tracer; - this.operationNameConstructor = tracingConfig.operationNameConstructor(); - this.streaming = tracingConfig.isStreaming(); - this.verbose = tracingConfig.isVerbose(); + this.operationNameConstructor = tracingConfig.operationNameConstructor().orElse(null); + this.streaming = tracingConfig.streaming(); + this.verbose = tracingConfig.verbose(); this.tracedAttributes = tracingConfig.tracedAttributes(); } /** - * Create a {@link GrpcTracing} interceptor. + * Create a {@link GrpcTracingInterceptor} interceptor. * * @param tracer the Open Tracing {@link Tracer} * @param config the tracing configuration - * - * @return a {@link GrpcTracing} interceptor instance + * @return a {@link GrpcTracingInterceptor} interceptor instance */ - static GrpcTracing create(Tracer tracer, GrpcTracingConfig config) { - return new GrpcTracing(tracer, config); + static GrpcTracingInterceptor create(Tracer tracer, GrpcTracingConfig config) { + return new GrpcTracingInterceptor(tracer, config); } @Override @@ -103,7 +103,9 @@ public ServerCall.Listener interceptCall(ServerCall ServerCall.Listener interceptCall(ServerCall updateContext(ctx, span)); - Context ctxWithSpan = grpcContext.withValue(GrpcTracingContext.SPAN_KEY, span); ServerCall.Listener listenerWithContext = Contexts.interceptCall(ctxWithSpan, call, headers, next); @@ -145,7 +145,7 @@ public ServerCall.Listener interceptCall(ServerCall headers, String operationNam private static boolean isCaseInsensitive(Map headers) { return (headers instanceof TreeMap - && ((TreeMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER) + && ((TreeMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER) || (headers instanceof ConcurrentSkipListMap - && ((ConcurrentSkipListMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER); + && ((ConcurrentSkipListMap) headers).comparator() == String.CASE_INSENSITIVE_ORDER); } /** * A {@link ServerCall.Listener} to apply details to a tracing {@link Span} at various points * in a call lifecycle. * - * @param the type of the request + * @param the type of the request */ private class TracingListener extends ForwardingServerCallListener.SimpleForwardingServerCallListener { @@ -254,10 +254,8 @@ public void onComplete() { } } - private static class MapHeaderProvider implements HeaderProvider { - private final Map headers; - - MapHeaderProvider(Map headers) { + private record MapHeaderProvider(Map headers) implements HeaderProvider { + private MapHeaderProvider(Map headers) { if (isCaseInsensitive(headers)) { this.headers = headers; } else { From bbcbcf21fff0d99a4ba8a5704e5cde9f83fffe2b Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 17 Jun 2024 15:32:32 -0400 Subject: [PATCH 13/30] Adds test for GrpcHelper. Signed-off-by: Santiago Pericas-Geertsen --- .../java/io/helidon/grpc/core/GrpcHelper.java | 9 +- .../io/helidon/grpc/core/ResponseHelper.java | 6 +- .../helidon/grpc/core/SafeStreamObserver.java | 2 +- .../io/helidon/grpc/core/GrpcHelperTest.java | 664 ++++++++++++++++++ 4 files changed, 671 insertions(+), 10 deletions(-) create mode 100644 grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java index dbba7cc24cf..dd6f7db9c00 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcHelper.java @@ -100,21 +100,19 @@ public static io.helidon.http.Status toHttpResponseStatus(StatusRuntimeException * @return the gRPC {@link Status} converted to a {@link io.helidon.http.Status} */ public static io.helidon.http.Status toHttpResponseStatus(Status status) { - io.helidon.http.Status httpStatus = switch (status.getCode()) { + return switch (status.getCode()) { case OK -> io.helidon.http.Status.create(200, status.getDescription()); - case INVALID_ARGUMENT -> io.helidon.http.Status.create(400, status.getDescription()); + case INVALID_ARGUMENT, OUT_OF_RANGE -> io.helidon.http.Status.create(400, status.getDescription()); case DEADLINE_EXCEEDED -> io.helidon.http.Status.create(408, status.getDescription()); case NOT_FOUND -> io.helidon.http.Status.create(404, status.getDescription()); case ALREADY_EXISTS -> io.helidon.http.Status.create(412, status.getDescription()); case PERMISSION_DENIED -> io.helidon.http.Status.create(403, status.getDescription()); case FAILED_PRECONDITION -> io.helidon.http.Status.create(412, status.getDescription()); - case OUT_OF_RANGE -> io.helidon.http.Status.create(400, status.getDescription()); case UNIMPLEMENTED -> io.helidon.http.Status.create(501, status.getDescription()); case UNAVAILABLE -> io.helidon.http.Status.create(503, status.getDescription()); case UNAUTHENTICATED -> io.helidon.http.Status.create(401, status.getDescription()); default -> io.helidon.http.Status.create(500, status.getDescription()); }; - return httpStatus; } /** @@ -147,8 +145,7 @@ public static Throwable ensureStatusException(Throwable thrown, Status status) { public static StatusRuntimeException ensureStatusRuntimeException(Throwable thrown, Status status) { if (thrown instanceof StatusRuntimeException) { return (StatusRuntimeException) thrown; - } else if (thrown instanceof StatusException) { - StatusException ex = (StatusException) thrown; + } else if (thrown instanceof StatusException ex) { return new StatusRuntimeException(ex.getStatus(), ex.getTrailers()); } else { return status.withCause(thrown).asRuntimeException(); diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java index c17035bc5bb..89f5eef4d14 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java @@ -411,8 +411,8 @@ public static Supplier createSupplier(Callable callable) { * @param the type of result expected by the {@link StreamObserver} */ private static class CompletionAction implements BiConsumer { - private StreamObserver observer; - private boolean sendResult; + private final StreamObserver observer; + private final boolean sendResult; CompletionAction(StreamObserver observer, boolean sendResult) { this.observer = observer; @@ -438,7 +438,7 @@ public void accept(T result, Throwable error) { * @param the type of result returned from the callable */ private static class CallableSupplier implements Supplier { - private Callable callable; + private final Callable callable; CallableSupplier(Callable callable) { this.callable = callable; diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java index 728403f94f8..ba3fdbf9577 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java @@ -159,7 +159,7 @@ public static StreamObserver ensureSafeObserver(StreamObserver observe /** * The actual StreamObserver. */ - private StreamObserver delegate; + private final StreamObserver delegate; /** * Indicates a terminal state. diff --git a/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java b/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java new file mode 100644 index 00000000000..b4f0d082c81 --- /dev/null +++ b/grpc/core/src/test/java/io/helidon/grpc/core/GrpcHelperTest.java @@ -0,0 +1,664 @@ +/* + * 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.grpc.core; + +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * {@link GrpcHelper} unit tests + */ +public class GrpcHelperTest { + + @Test + public void shouldExtractServiceName() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractServiceName(fullName), is("Foo")); + } + + @Test + public void shouldExtractMethodName() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractMethodName(fullName), is("Bar")); + } + + @Test + public void shouldExtractNamePrefix() { + String fullName = "Foo/1234/Bar"; + + assertThat(GrpcHelper.extractNamePrefix(fullName), is("Foo/1234")); + } + + @Test + public void shouldConvertAbortedStatusException() { + StatusException exception = Status.ABORTED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertAbortedStatusExceptionWithDescription() { + StatusException exception = Status.ABORTED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAlreadyExistsStatusException() { + StatusException exception = Status.ALREADY_EXISTS.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertAlreadyExistsStatusExceptionWithDescription() { + StatusException exception = Status.ALREADY_EXISTS.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOkStatusException() { + StatusException exception = Status.OK.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("OK")); + } + + @Test + public void shouldConvertOkStatusExceptionWithDescription() { + StatusException exception = Status.OK.withDescription("Good!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("Good!")); + } + + @Test + public void shouldConvertInvalidArgumentStatusException() { + StatusException exception = Status.INVALID_ARGUMENT.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertInvalidArgumentStatusExceptionWithDescription() { + StatusException exception = Status.INVALID_ARGUMENT.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDeadlineExceededStatusException() { + StatusException exception = Status.DEADLINE_EXCEEDED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Request Timeout")); + } + + @Test + public void shouldConvertDeadlineExceededStatusExceptionWithDescription() { + StatusException exception = Status.DEADLINE_EXCEEDED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertNotFoundStatusException() { + StatusException exception = Status.NOT_FOUND.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Not Found")); + } + + @Test + public void shouldConvertNotFoundStatusExceptionWithDescription() { + StatusException exception = Status.NOT_FOUND.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertPermissionDeniedStatusException() { + StatusException exception = Status.PERMISSION_DENIED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Forbidden")); + } + + @Test + public void shouldConvertPermissionDeniedStatusExceptionWithDescription() { + StatusException exception = Status.PERMISSION_DENIED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertFailedPreconditionStatusException() { + StatusException exception = Status.FAILED_PRECONDITION.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertFailedPreconditionStatusExceptionWithDescription() { + StatusException exception = Status.FAILED_PRECONDITION.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOutOfRangeStatusException() { + StatusException exception = Status.OUT_OF_RANGE.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertOutOfRangeStatusExceptionWithDescription() { + StatusException exception = Status.OUT_OF_RANGE.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnimplementedStatusException() { + StatusException exception = Status.UNIMPLEMENTED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Not Implemented")); + } + + @Test + public void shouldConvertUnimplementedStatusExceptionWithDescription() { + StatusException exception = Status.UNIMPLEMENTED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnavailableStatusException() { + StatusException exception = Status.UNAVAILABLE.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Service Unavailable")); + } + + @Test + public void shouldConvertUnavailableStatusExceptionWithDescription() { + StatusException exception = Status.UNAVAILABLE.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnauthenticatedStatusException() { + StatusException exception = Status.UNAUTHENTICATED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Unauthorized")); + } + + @Test + public void shouldConvertUnauthenticatedStatusExceptionWithDescription() { + StatusException exception = Status.UNAUTHENTICATED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertCancelledStatusException() { + StatusException exception = Status.CANCELLED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertCancelledStatusExceptionWithDescription() { + StatusException exception = Status.CANCELLED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDataLossStatusException() { + StatusException exception = Status.DATA_LOSS.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertDataLossStatusExceptionWithDescription() { + StatusException exception = Status.DATA_LOSS.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertInternalStatusException() { + StatusException exception = Status.INTERNAL.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertInternalStatusExceptionWithDescription() { + StatusException exception = Status.INTERNAL.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertResourceExhaustedStatusException() { + StatusException exception = Status.RESOURCE_EXHAUSTED.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertResourceExhaustedStatusExceptionWithDescription() { + StatusException exception = Status.RESOURCE_EXHAUSTED.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnknownStatusException() { + StatusException exception = Status.UNKNOWN.asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertUnknownStatusExceptionWithDescription() { + StatusException exception = Status.UNKNOWN.withDescription("Oops!").asException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAbortedStatusRuntimeException() { + StatusRuntimeException exception = Status.ABORTED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertAbortedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.ABORTED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertAlreadyExistsStatusRuntimeException() { + StatusRuntimeException exception = Status.ALREADY_EXISTS.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertAlreadyExistsStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.ALREADY_EXISTS.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOkStatusRuntimeException() { + StatusRuntimeException exception = Status.OK.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("OK")); + } + + @Test + public void shouldConvertOkStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.OK.withDescription("Good!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(200)); + assertThat(status.reasonPhrase(), is("Good!")); + } + + @Test + public void shouldConvertInvalidArgumentStatusRuntimeException() { + StatusRuntimeException exception = Status.INVALID_ARGUMENT.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertInvalidArgumentStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.INVALID_ARGUMENT.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDeadlineExceededStatusRuntimeException() { + StatusRuntimeException exception = Status.DEADLINE_EXCEEDED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Request Timeout")); + } + + @Test + public void shouldConvertDeadlineExceededStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.DEADLINE_EXCEEDED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(408)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertNotFoundStatusRuntimeException() { + StatusRuntimeException exception = Status.NOT_FOUND.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Not Found")); + } + + @Test + public void shouldConvertNotFoundStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.NOT_FOUND.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(404)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertPermissionDeniedStatusRuntimeException() { + StatusRuntimeException exception = Status.PERMISSION_DENIED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Forbidden")); + } + + @Test + public void shouldConvertPermissionDeniedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.PERMISSION_DENIED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(403)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertFailedPreconditionStatusRuntimeException() { + StatusRuntimeException exception = Status.FAILED_PRECONDITION.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Precondition Failed")); + } + + @Test + public void shouldConvertFailedPreconditionStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.FAILED_PRECONDITION.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(412)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertOutOfRangeStatusRuntimeException() { + StatusRuntimeException exception = Status.OUT_OF_RANGE.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Bad Request")); + } + + @Test + public void shouldConvertOutOfRangeStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.OUT_OF_RANGE.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(400)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnimplementedStatusRuntimeException() { + StatusRuntimeException exception = Status.UNIMPLEMENTED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Not Implemented")); + } + + @Test + public void shouldConvertUnimplementedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNIMPLEMENTED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(501)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnavailableStatusRuntimeException() { + StatusRuntimeException exception = Status.UNAVAILABLE.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Service Unavailable")); + } + + @Test + public void shouldConvertUnavailableStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNAVAILABLE.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(503)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnauthenticatedStatusRuntimeException() { + StatusRuntimeException exception = Status.UNAUTHENTICATED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Unauthorized")); + } + + @Test + public void shouldConvertUnauthenticatedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNAUTHENTICATED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(401)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertCancelledStatusRuntimeException() { + StatusRuntimeException exception = Status.CANCELLED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertCancelledStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.CANCELLED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertDataLossStatusRuntimeException() { + StatusRuntimeException exception = Status.DATA_LOSS.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertDataLossStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.DATA_LOSS.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertInternalStatusRuntimeException() { + StatusRuntimeException exception = Status.INTERNAL.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertInternalStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.INTERNAL.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertResourceExhaustedStatusRuntimeException() { + StatusRuntimeException exception = Status.RESOURCE_EXHAUSTED.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertResourceExhaustedStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.RESOURCE_EXHAUSTED.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } + + @Test + public void shouldConvertUnknownStatusRuntimeException() { + StatusRuntimeException exception = Status.UNKNOWN.asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Internal Server Error")); + } + + @Test + public void shouldConvertUnknownStatusRuntimeExceptionWithDescription() { + StatusRuntimeException exception = Status.UNKNOWN.withDescription("Oops!").asRuntimeException(); + io.helidon.http.Status status = GrpcHelper.toHttpResponseStatus(exception); + + assertThat(status.code(), is(500)); + assertThat(status.reasonPhrase(), is("Oops!")); + } +} From 3e45b8f1eb90c92e82d416e5fbba5c104e248a81 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 18 Jun 2024 10:02:50 -0400 Subject: [PATCH 14/30] Cleans up some modules and poms. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/grpc/core/pom.xml | 15 +++---- .../grpc/core/src/main/java/module-info.java | 1 - microprofile/grpc/pom.xml | 43 ++----------------- microprofile/grpc/server/pom.xml | 6 ++- 4 files changed, 12 insertions(+), 53 deletions(-) diff --git a/microprofile/grpc/core/pom.xml b/microprofile/grpc/core/pom.xml index 4bdc1cdf2fd..1a59ac4c350 100644 --- a/microprofile/grpc/core/pom.xml +++ b/microprofile/grpc/core/pom.xml @@ -27,22 +27,12 @@ helidon-microprofile-grpc-core Helidon Microprofile gRPC Core - Core microprofile gRPC io.helidon.grpc helidon-grpc-core - - io.helidon.webclient - helidon-webclient-grpc - - - jakarta.enterprise - jakarta.enterprise.cdi-api - provided - io.helidon.common helidon-common @@ -51,6 +41,11 @@ io.helidon.microprofile.config helidon-microprofile-config + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + org.junit.jupiter junit-jupiter-api diff --git a/microprofile/grpc/core/src/main/java/module-info.java b/microprofile/grpc/core/src/main/java/module-info.java index b6a32f9cc81..62e103a1b4e 100644 --- a/microprofile/grpc/core/src/main/java/module-info.java +++ b/microprofile/grpc/core/src/main/java/module-info.java @@ -22,7 +22,6 @@ requires io.helidon.common; requires transitive io.helidon.grpc.core; - requires transitive io.helidon.webclient.grpc; requires transitive io.helidon.microprofile.config; requires transitive jakarta.cdi; diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml index cf839094aeb..40fababf24c 100644 --- a/microprofile/grpc/pom.xml +++ b/microprofile/grpc/pom.xml @@ -26,35 +26,9 @@ io.helidon.microprofile.grpc - helidon-microprofile-grpc - Helidon Microprofile gRPC - - - io.grpc - grpc-api - - - io.helidon.grpc - helidon-grpc-core - - - io.helidon.health - helidon-health - - - io.helidon.webserver - helidon-webserver-grpc - - - io.helidon.config - helidon-config-metadata - - - org.eclipse.microprofile.health - microprofile-health-api - 4.0.1 - - + helidon-microprofile-grpc-project + Helidon Microprofile gRPC Project + pom @@ -64,7 +38,6 @@ - org.apache.maven.plugins maven-surefire-plugin @@ -72,7 +45,6 @@ -Dgrpc.marshaller.java.enabled=true - org.apache.maven.plugins maven-failsafe-plugin @@ -94,15 +66,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 7 - 7 - - - diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml index 2d9a63013e4..8455b0cb5dd 100644 --- a/microprofile/grpc/server/pom.xml +++ b/microprofile/grpc/server/pom.xml @@ -27,7 +27,6 @@ helidon-microprofile-grpc-server Helidon Microprofile gRPC Server - The microprofile gRPC Server implementation @@ -46,6 +45,10 @@ io.helidon.common helidon-common + + io.helidon.config + helidon-config-metadata + io.helidon.config helidon-config-yaml-mp @@ -143,7 +146,6 @@ ${version.plugin.os} - org.xolstice.maven.plugins From dbd72ce274bff84f734cb6cdb3e731a892817acd Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 18 Jun 2024 10:23:22 -0400 Subject: [PATCH 15/30] Fixes a number of compiler warnings. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/grpc/core/pom.xml | 2 +- .../core/AbstractMethodHandlerSupplier.java | 13 ++--- .../grpc/core/AbstractServiceBuilder.java | 2 +- .../grpc/core/AnnotatedMethod.java | 14 +++--- .../BidirectionalMethodHandlerSupplier.java | 3 +- .../grpc/core/ClientStreaming.java | 3 +- .../ClientStreamingMethodHandlerSupplier.java | 45 +++++------------ .../helidon/microprofile/grpc/core/Grpc.java | 6 ++- .../grpc/core/GrpcInterceptor.java | 7 ++- .../grpc/core/InProcessGrpcChannel.java | 12 ++--- .../microprofile/grpc/core/Instance.java | 10 ++-- .../microprofile/grpc/core/ModelHelper.java | 50 ++++++++----------- .../microprofile/grpc/core/RequestType.java | 2 +- .../microprofile/grpc/core/ResponseType.java | 2 +- .../grpc/core/ServerStreaming.java | 3 +- .../ServerStreamingMethodHandlerSupplier.java | 7 ++- .../helidon/microprofile/grpc/core/Unary.java | 3 +- .../grpc/core/UnaryMethodHandlerSupplier.java | 3 +- microprofile/grpc/server/pom.xml | 2 +- 19 files changed, 80 insertions(+), 109 deletions(-) diff --git a/microprofile/grpc/core/pom.xml b/microprofile/grpc/core/pom.xml index 1a59ac4c350..d7e4564f8e6 100644 --- a/microprofile/grpc/core/pom.xml +++ b/microprofile/grpc/core/pom.xml @@ -21,7 +21,7 @@ 4.0.0 io.helidon.microprofile.grpc - helidon-microprofile-grpc + helidon-microprofile-grpc-project 4.1.0-SNAPSHOT diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java index 8d43437b9c0..5131a4ddcc9 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractMethodHandlerSupplier.java @@ -34,8 +34,7 @@ /** * A base class for {@link MethodHandlerSupplier} implementations. */ -abstract class AbstractMethodHandlerSupplier - implements MethodHandlerSupplier { +abstract class AbstractMethodHandlerSupplier implements MethodHandlerSupplier { private final MethodDescriptor.MethodType methodType; @@ -77,8 +76,7 @@ boolean isRequiredMethodType(AnnotatedMethod method) { * @param the request type * @param the response type */ - public abstract static class AbstractHandler - implements MethodHandler { + public abstract static class AbstractHandler implements MethodHandler { private final String methodName; private final AnnotatedMethod method; @@ -263,10 +261,9 @@ protected Class getGenericResponseType(Type type) { * * @param the type of the response */ - private static class NullHandlingResponseObserver - implements StreamObserver { + private static class NullHandlingResponseObserver implements StreamObserver { - private final StreamObserver delegate; + private final StreamObserver delegate; private NullHandlingResponseObserver(StreamObserver delegate) { this.delegate = delegate; @@ -276,7 +273,7 @@ private NullHandlingResponseObserver(StreamObserver delegate) { @SuppressWarnings("unchecked") public void onNext(V value) { if (value == null) { - delegate.onNext(Empty.getDefaultInstance()); + delegate.onNext((V) Empty.getDefaultInstance()); } delegate.onNext(value); } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java index a5bdab576d3..23ad8655344 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AbstractServiceBuilder.java @@ -151,7 +151,7 @@ protected Supplier instanceSupplier() { */ protected List getAllDeclaredMethods(Class clazz) { List result = new LinkedList<>(); - Class current = clazz; + Class current = clazz; while (current != Object.class && current != null) { result.addAll(Arrays.asList(current.getDeclaredMethods())); current = current.getSuperclass(); diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java index 53bc1f61375..32a83544f81 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/AnnotatedMethod.java @@ -143,7 +143,7 @@ public Annotation[][] parameterAnnotations() { /** * Get method parameter types. - * + *

* See also {@link Method#getParameterTypes()}. * * @return method parameter types. @@ -154,7 +154,7 @@ public Class[] parameterTypes() { /** * Get method type parameters. - * + *

* See also {@link Method#getTypeParameters()}. * * @return method type parameters. @@ -165,7 +165,7 @@ public TypeVariable[] typeParameters() { /** * Get generic method parameter types. - * + *

* See also {@link Method#getGenericParameterTypes()}. * * @return generic method parameter types. @@ -176,8 +176,8 @@ public Type[] genericParameterTypes() { /** * Get generic method return type. - * - * See also {@link Method#getGenericReturnType()} ()}. + *

+ * See also {@link Method#getGenericReturnType()}. * * @return generic method return types. */ @@ -187,8 +187,8 @@ public Type genericReturnType() { /** * Get method return type. - * - * See also {@link Method#getReturnType()} ()} ()}. + *

+ * See also {@link Method#getReturnType()}. * * @return method return types. */ diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java index 36c01525b1f..44885964cf8 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/BidirectionalMethodHandlerSupplier.java @@ -31,8 +31,7 @@ /** * A supplier of {@link MethodHandler}s for bi-directional streaming gRPC methods. */ -public class BidirectionalMethodHandlerSupplier - extends AbstractMethodHandlerSupplier { +public class BidirectionalMethodHandlerSupplier extends AbstractMethodHandlerSupplier { /** * Create a supplier of handlers for bi-directional streaming methods. diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java index 45822c54560..2a2e453810b 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreaming.java @@ -26,8 +26,7 @@ import io.grpc.MethodDescriptor.MethodType; /** - * An annotation to mark a class as representing a - * client streaming gRPC method. + * An annotation to mark a class as representing a client streaming gRPC method. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java index f9add37e532..d1e49a169cf 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ClientStreamingMethodHandlerSupplier.java @@ -34,8 +34,7 @@ /** * A supplier of {@link MethodHandler}s for client streaming gRPC methods. */ -public class ClientStreamingMethodHandlerSupplier - extends AbstractMethodHandlerSupplier { +public class ClientStreamingMethodHandlerSupplier extends AbstractMethodHandlerSupplier { /** * Create a supplier of handlers for client streaming methods. @@ -58,26 +57,13 @@ public MethodHandler get(String methodName, Annotated } CallType type = determineCallType(method); - MethodHandler handler; - - switch (type) { - case clientStreaming: - handler = new ClientStreaming<>(methodName, method, instance); - break; - case futureResponse: - handler = new FutureResponse<>(methodName, method, instance); - break; - case clientStreamingIterable: - handler = new ClientStreamingIterable(methodName, method, instance); - break; - case clientStreamingStream: - handler = new ClientStreamingStream(methodName, method, instance); - break; - case unknown: - default: - throw new IllegalArgumentException("Not a supported client streaming method signature: " + method); - } - return handler; + return switch (type) { + case clientStreaming -> (MethodHandler) new ClientStreaming<>(methodName, method, instance); + case futureResponse -> new FutureResponse<>(methodName, method, instance); + case clientStreamingIterable -> new ClientStreamingIterable<>(methodName, method, instance); + case clientStreamingStream -> new ClientStreamingStream<>(methodName, method, instance); + default -> throw new IllegalArgumentException("Not a supported client streaming method signature: " + method); + }; } private CallType determineCallType(AnnotatedMethod method) { @@ -218,7 +204,7 @@ protected StreamObserver invoke(Method method, Object instance, StreamObse @Override @SuppressWarnings("unchecked") public Object clientStreaming(Object[] args, ClientStreaming client) { - return client.clientStreaming(methodName(), (StreamObserver) args[0]); + return client.clientStreaming(methodName(), (StreamObserver) args[0]); } } @@ -272,8 +258,7 @@ public Object clientStreaming(Object[] args, ClientStreaming client) { * @param the request type * @param the response type */ - public static class ClientStreamingIterable - extends AbstractClientStreamingHandler { + public static class ClientStreamingIterable extends AbstractClientStreamingHandler { ClientStreamingIterable(String methodName, AnnotatedMethod method, Supplier instance) { super(methodName, method, instance); @@ -313,7 +298,6 @@ public Object clientStreaming(Object[] args, ClientStreaming client) { } } - // ----- ClientStreamingIterable call handler --------------------------- /** @@ -326,8 +310,7 @@ public Object clientStreaming(Object[] args, ClientStreaming client) { * @param the request type * @param the response type */ - public static class ClientStreamingStream - extends AbstractClientStreamingHandler { + public static class ClientStreamingStream extends AbstractClientStreamingHandler { ClientStreamingStream(String methodName, AnnotatedMethod method, Supplier instance) { super(methodName, method, instance); @@ -364,17 +347,15 @@ public Object clientStreaming(Object[] args, ClientStreaming client) { } } - /** * A {@link StreamObserver} that completes a {@link CompletableFuture} * with its received result. * * @param the result type */ - private static class FutureObserver - implements StreamObserver { + private static class FutureObserver implements StreamObserver { - private CompletableFuture future; + private final CompletableFuture future; private T value; private FutureObserver(CompletableFuture future) { diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java index 305e61cb00d..ec1909ad2d6 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Grpc.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.grpc.core; +import java.io.Serial; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -54,13 +55,14 @@ */ class Literal extends AnnotationLiteral implements Grpc { + @Serial + private static final long serialVersionUID = 1L; + /** * The singleton instance of {@link Literal}. */ public static final Literal INSTANCE = new Literal(); - private static final long serialVersionUID = 1L; - @Override public String name() { return ""; diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java index 0aa9efd805a..8b03f945c2d 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/GrpcInterceptor.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.grpc.core; +import java.io.Serial; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -66,11 +67,13 @@ * An {@link AnnotationLiteral} for the {@link GrpcInterceptor} annotation. */ class Literal extends AnnotationLiteral implements GrpcInterceptor { + + @Serial + private static final long serialVersionUID = 1L; + /** * The singleton instance of {@link GrpcInterceptor.Literal}. */ public static final Literal INSTANCE = new Literal(); - - private static final long serialVersionUID = 1L; } } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java index 299678351dd..929f120a18e 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/InProcessGrpcChannel.java @@ -16,6 +16,7 @@ package io.helidon.microprofile.grpc.core; +import java.io.Serial; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -25,7 +26,7 @@ import jakarta.inject.Qualifier; /** - * An qualifier annotation to specify that an in-process {@link io.grpc.Channel} + * A qualifier annotation to specify that an in-process {@link io.grpc.Channel} * should be injected. *

* For example: @@ -43,15 +44,14 @@ /** * An {@link AnnotationLiteral} for the {@link InProcessGrpcChannel} annotation. */ - class Literal - extends AnnotationLiteral - implements InProcessGrpcChannel { + class Literal extends AnnotationLiteral implements InProcessGrpcChannel { + + @Serial + private static final long serialVersionUID = 1L; /** * The singleton instance of {@link Literal}. */ public static final Literal INSTANCE = new Literal(); - - private static final long serialVersionUID = 1L; } } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java index a51e0d53808..8bff3e2c611 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Instance.java @@ -96,7 +96,7 @@ protected NewInstance(Class instanceClass) { @Override public T get() { try { - return instanceClass.newInstance(); + return instanceClass.getDeclaredConstructor().newInstance(); } catch (Throwable e) { throw Status.INTERNAL.withCause(e).asRuntimeException(); } @@ -110,8 +110,7 @@ public T get() { * * @param the type of instance supplied */ - class SingletonInstance - implements Supplier { + class SingletonInstance implements Supplier { private final Class instanceClass; @@ -137,7 +136,8 @@ private T ensureInstance() { return accessInstance(() -> { if (instance == null) { try { - instance = instanceClass.newInstance(); + assert instanceClass != null; + instance = instanceClass.getDeclaredConstructor().newInstance(); } catch (Throwable e) { throw Status.INTERNAL.withCause(e).asRuntimeException(); } @@ -146,7 +146,7 @@ private T ensureInstance() { }); } - private T accessInstance(Supplier operation) { + private T accessInstance(Supplier operation) { instanceAccess.lock(); try { return operation.get(); diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java index 1b585fe569b..216db0c6912 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ModelHelper.java @@ -70,7 +70,7 @@ public static Class getAnnotatedResourceClass(Class resourceClass, Class i : cls.getInterfaces()) { + for (Class i : cls.getInterfaces()) { if (i.getDeclaredAnnotation(annotation) != null) { // store the interface reference in case no annotation will be found in the super-classes foundInterface = i; @@ -94,7 +94,7 @@ public static Class getAnnotatedResourceClass(Class resourceClass, Class getDeclaredMethods(final Class clazz) { + public static Collection getDeclaredMethods(Class clazz) { return Arrays.asList(clazz.getDeclaredMethods()); } @@ -113,11 +113,11 @@ public static Collection getDeclaredMethods(final Class cla * @param m the method to find * @return public method found. */ - public static Method findMethodOnClass(final Class cls, final Method m) { + public static Method findMethodOnClass(Class cls, Method m) { try { return cls.getMethod(m.getName(), m.getParameterTypes()); - } catch (final NoSuchMethodException e) { - for (final Method method : cls.getMethods()) { + } catch (NoSuchMethodException e) { + for (Method method : cls.getMethods()) { if (method.getName().equals(m.getName()) && method.getParameterTypes().length == m.getParameterTypes().length) { if (compareParameterTypes(m.getGenericParameterTypes(), @@ -138,7 +138,7 @@ public static Method findMethodOnClass(final Class cls, final Method m) { * @return {@code true} if the given types are understood to be equal, {@code false} otherwise. * @see #compareParameterTypes(java.lang.reflect.Type, java.lang.reflect.Type) */ - private static boolean compareParameterTypes(final Type[] first, final Type[] second) { + private static boolean compareParameterTypes(Type[] first, Type[] second) { for (int i = 0; i < first.length; i++) { if (!first[i].equals(second[i])) { if (!compareParameterTypes(first[i], second[i])) { @@ -157,24 +157,22 @@ private static boolean compareParameterTypes(final Type[] first, final Type[] se * @return {@code true} if the given types are understood to be equal, {@code false} otherwise. */ @SuppressWarnings("unchecked") - private static boolean compareParameterTypes(final Type first, final Type second) { - if (first instanceof Class) { - final Class clazz = (Class) first; - + private static boolean compareParameterTypes(Type first, Type second) { + if (first instanceof Class clazz) { if (second instanceof Class) { - return ((Class) second).isAssignableFrom(clazz); + return ((Class) second).isAssignableFrom(clazz); } else if (second instanceof TypeVariable) { - return checkTypeBounds(clazz, ((TypeVariable) second).getBounds()); + return checkTypeBounds(clazz, ((TypeVariable) second).getBounds()); } } return second instanceof TypeVariable; } @SuppressWarnings("unchecked") - private static boolean checkTypeBounds(final Class type, final Type[] bounds) { - for (final Type bound : bounds) { + private static boolean checkTypeBounds(Class type, Type[] bounds) { + for (Type bound : bounds) { if (bound instanceof Class) { - if (!((Class) bound).isAssignableFrom(type)) { + if (!((Class) bound).isAssignableFrom(type)) { return false; } } @@ -189,15 +187,13 @@ private static boolean checkTypeBounds(final Class type, final Type[] bounds) { * @return array component type. * @throws IllegalArgumentException in case the type is not an array type. */ - public static Type getArrayComponentType(final Type type) { - if (type instanceof Class) { - final Class c = (Class) type; + public static Type getArrayComponentType(Type type) { + if (type instanceof Class c) { return c.getComponentType(); } if (type instanceof GenericArrayType) { return ((GenericArrayType) type).getGenericComponentType(); } - throw new IllegalArgumentException(); } @@ -207,11 +203,11 @@ public static Type getArrayComponentType(final Type type) { * @param c the component class of the array * @return the array class. */ - public static Class getArrayForComponentType(final Class c) { + public static Class getArrayForComponentType(Class c) { try { - final Object o = Array.newInstance(c, 0); + Object o = Array.newInstance(c, 0); return o.getClass(); - } catch (final Exception e) { + } catch (Exception e) { throw new IllegalArgumentException(e); } } @@ -263,8 +259,7 @@ private static boolean hasName(MarshallerSupplier supplier, String name) { public static Class getGenericType(Type type) { if (type instanceof Class) { return (Class) type; - } else if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) type; + } else if (type instanceof ParameterizedType parameterizedType) { if (parameterizedType.getRawType() instanceof Class) { Type t = parameterizedType.getActualTypeArguments()[0]; if (t instanceof Class) { @@ -278,16 +273,15 @@ public static Class getGenericType(Type type) { + "parameterized type whose raw type is a class"); } } - } else if (type instanceof GenericArrayType) { - GenericArrayType array = (GenericArrayType) type; - final Class componentRawType = getGenericType(array.getGenericComponentType()); + } else if (type instanceof GenericArrayType array) { + Class componentRawType = getGenericType(array.getGenericComponentType()); return getArrayClass(componentRawType); } throw new IllegalArgumentException("Type parameter " + type.toString() + " not a class or " + "parameterized type whose raw type is a class"); } - private static Class getArrayClass(Class c) { + private static Class getArrayClass(Class c) { try { Object o = Array.newInstance(c, 0); return o.getClass(); diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java index 2d615662228..047a9f6f645 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/RequestType.java @@ -36,5 +36,5 @@ * * @return the gRPC request type */ - Class value(); + Class value(); } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java index d59738c3dae..94ed6049604 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ResponseType.java @@ -36,5 +36,5 @@ * * @return the gRPC response type */ - Class value(); + Class value(); } diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java index 6f4c9f85a9b..6f97df1c251 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreaming.java @@ -26,8 +26,7 @@ import io.grpc.MethodDescriptor.MethodType; /** - * An annotation to mark a class as representing a - * server streaming gRPC method. + * An annotation to mark a class as representing a server streaming gRPC method. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java index bc517a11905..ef5c8e0d535 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/ServerStreamingMethodHandlerSupplier.java @@ -38,8 +38,7 @@ /** * A supplier of {@link MethodHandler}s for server streaming gRPC methods. */ -public class ServerStreamingMethodHandlerSupplier - extends AbstractMethodHandlerSupplier { +public class ServerStreamingMethodHandlerSupplier extends AbstractMethodHandlerSupplier { /** * Create a supplier of handlers for server streaming methods. @@ -360,8 +359,8 @@ public Object serverStreaming(Object[] args, ServerStreamingClient client) { private static class Observer implements StreamObserver { - private CompletableFuture> future = new CompletableFuture<>(); - private List list = new ArrayList<>(); + private final CompletableFuture> future = new CompletableFuture<>(); + private final List list = new ArrayList<>(); private CompletableFuture> future() { return future; diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java index eaba07e0543..92fdb746366 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/Unary.java @@ -26,8 +26,7 @@ import io.grpc.MethodDescriptor.MethodType; /** - * An annotation to mark a method as representing a - * unary gRPC method. + * An annotation to mark a method as representing a unary gRPC method. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java index 4151f3f37c5..30ae5dccd6b 100644 --- a/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java +++ b/microprofile/grpc/core/src/main/java/io/helidon/microprofile/grpc/core/UnaryMethodHandlerSupplier.java @@ -34,8 +34,7 @@ /** * A supplier of {@link MethodHandler}s for unary gRPC methods. */ -public class UnaryMethodHandlerSupplier - extends AbstractMethodHandlerSupplier { +public class UnaryMethodHandlerSupplier extends AbstractMethodHandlerSupplier { /** * Create a supplier of handlers for server streaming methods. diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml index 8455b0cb5dd..4724adb32e0 100644 --- a/microprofile/grpc/server/pom.xml +++ b/microprofile/grpc/server/pom.xml @@ -21,7 +21,7 @@ 4.0.0 io.helidon.microprofile.grpc - helidon-microprofile-grpc + helidon-microprofile-grpc-project 4.1.0-SNAPSHOT From dc5b70f7630f226712549cc60c3045a05ccd133d Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 18 Jun 2024 12:42:12 -0400 Subject: [PATCH 16/30] Relocates class CollectingObserver to grpc-core. Signed-off-by: Santiago Pericas-Geertsen --- .../grpc/core}/CollectingObserver.java | 69 +++++++++---------- .../grpc/server/StringServiceTest.java | 2 +- .../webserver/grpc/BindableServiceImpl.java | 2 +- webserver/grpc/src/main/java/module-info.java | 4 +- 4 files changed, 37 insertions(+), 40 deletions(-) rename {webserver/grpc/src/main/java/io/helidon/webserver/grpc => grpc/core/src/main/java/io/helidon/grpc/core}/CollectingObserver.java (66%) diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/CollectingObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java similarity index 66% rename from webserver/grpc/src/main/java/io/helidon/webserver/grpc/CollectingObserver.java rename to grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java index da99a666c5f..b1ab322c97c 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/CollectingObserver.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.webserver.grpc; +package io.helidon.grpc.core; import java.util.Objects; import java.util.Optional; @@ -25,69 +25,69 @@ import io.grpc.stub.StreamObserver; /** - * A {@link StreamObserver}. + * Utility {@link StreamObserver} mostly used for testing. * - * @param ToDo: Add JavaDoc - * @param ToDo: Add JavaDoc - * @param ToDo: Add JavaDoc - * @param ToDo: Add JavaDoc - * @param ToDo: Add JavaDoc + * @param the type of input elements to the reduction operation + * @param the mutable accumulation type of the reduction operation + * @param the result type of the reduction operation + * @param the type of values observed + * @param the request type before conversion */ public class CollectingObserver implements StreamObserver { + private final Collector collector; private final StreamObserver responseObserver; private final Function requestConverter; private final Function responseConverter; private final Consumer errorHandler; - private final A accumulator; /** - * ToDo: Add JavaDoc. + * Constructor * - * @param collector ToDo: Add JavaDoc - * @param responseObserver ToDo: Add JavaDoc + * @param collector the collector + * @param observer the observer */ - public CollectingObserver(Collector collector, StreamObserver responseObserver) { - this(collector, responseObserver, null, null, null); + public CollectingObserver(Collector collector, StreamObserver observer) { + this(collector, observer, null, null, null); } /** - * ToDo: Add JavaDoc. + * Constructor * - * @param collector ToDo: Add JavaDoc - * @param responseObserver ToDo: Add JavaDoc - * @param errorHandler ToDo: Add JavaDoc + * @param collector the collector + * @param observer the observer + * @param errorHandler the error handler */ public CollectingObserver(Collector collector, - StreamObserver responseObserver, + StreamObserver observer, Consumer errorHandler) { - this(collector, responseObserver, null, null, errorHandler); + this(collector, observer, null, null, errorHandler); } /** - * ToDo: Add JavaDoc. + * Constructor * - * @param collector ToDo: Add JavaDoc - * @param responseObserver ToDo: Add JavaDoc - * @param requestConverter ToDo: Add JavaDoc - * @param responseConverter ToDo: Add JavaDoc + * @param collector the collector + * @param observer the observer + * @param requestConverter the request converter + * @param responseConverter the response converterr */ public CollectingObserver(Collector collector, - StreamObserver responseObserver, + StreamObserver observer, Function requestConverter, Function responseConverter) { - this(collector, responseObserver, requestConverter, responseConverter, null); + this(collector, observer, requestConverter, responseConverter, null); } /** - * ToDo: Add JavaDoc. + * Constructor * - * @param collector ToDo: Add JavaDoc - * @param observer ToDo: Add JavaDoc - * @param requestConverter ToDo: Add JavaDoc - * @param responseConverter ToDo: Add JavaDoc - * @param errorHandler ToDo: Add JavaDoc + * @param collector the collector + * @param observer the observer + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param errorHandler the error handler */ @SuppressWarnings("unchecked") public CollectingObserver(Collector collector, @@ -99,8 +99,7 @@ public CollectingObserver(Collector collector, this.responseObserver = Objects.requireNonNull(observer, "The observer parameter cannot be null"); this.requestConverter = Optional.ofNullable(requestConverter).orElse(v -> (T) v); this.responseConverter = Optional.ofNullable(responseConverter).orElse(r -> (U) r); - this.errorHandler = Optional.ofNullable(errorHandler).orElse(t -> { - }); + this.errorHandler = Optional.ofNullable(errorHandler).orElse(t -> {}); this.accumulator = collector.supplier().get(); } diff --git a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java index 4aa1d241399..e12b2c91d6a 100644 --- a/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java +++ b/microprofile/grpc/server/src/test/java/io/helidon/microprofile/grpc/server/StringServiceTest.java @@ -32,7 +32,7 @@ import io.helidon.microprofile.grpc.core.Unary; import io.helidon.microprofile.grpc.server.test.StringServiceGrpc; import io.helidon.microprofile.grpc.server.test.Strings; -import io.helidon.webserver.grpc.CollectingObserver; +import io.helidon.grpc.core.CollectingObserver; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java index 4bbfa0fac99..75ff46fda43 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/BindableServiceImpl.java @@ -96,7 +96,7 @@ private ServerCallHandler wrapCallHandler(GrpcMethodD if (!interceptors.isEmpty()) { LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); - // iterate the interceptors in reverse order so that the handler chain is in the correct order + // iterate the interceptors in reverse order to set up handler chain for (int i = interceptors.size() - 1; i >= 0; i--) { ServerInterceptor interceptor = interceptors.get(i); if (!uniqueInterceptors.contains(interceptor)) { diff --git a/webserver/grpc/src/main/java/module-info.java b/webserver/grpc/src/main/java/module-info.java index d8768ec22ff..043114886d4 100644 --- a/webserver/grpc/src/main/java/module-info.java +++ b/webserver/grpc/src/main/java/module-info.java @@ -18,7 +18,6 @@ import io.helidon.common.features.api.HelidonFlavor; import io.helidon.common.features.api.Preview; - /** * Helidon WebServer gRPC Support. */ @@ -35,6 +34,7 @@ requires io.helidon.builder.api; requires io.helidon.webserver.http2; requires io.helidon.tracing; + requires io.helidon.grpc.core; requires static io.helidon.common.features.api; @@ -42,7 +42,6 @@ requires transitive io.grpc; requires transitive io.grpc.stub; requires transitive io.helidon.common.config; - requires io.helidon.grpc.core; exports io.helidon.webserver.grpc; @@ -50,5 +49,4 @@ with io.helidon.webserver.grpc.GrpcProtocolProvider; provides io.helidon.webserver.spi.ProtocolConfigProvider with io.helidon.webserver.grpc.GrpcProtocolConfigProvider; - } From 79f881941444e8a0a15b985f6f6b86300cf57cd2 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 18 Jun 2024 12:51:20 -0400 Subject: [PATCH 17/30] Fixes a few additional warnings. Signed-off-by: Santiago Pericas-Geertsen --- .../java/io/helidon/grpc/core/CollectingObserver.java | 8 ++++---- .../grpc/ContextSettingServerInterceptor.java | 2 +- .../src/main/java/io/helidon/webserver/grpc/Grpc.java | 1 + .../io/helidon/webserver/grpc/GrpcHeadersUtil.java | 1 + .../helidon/webserver/grpc/GrpcProtocolSelector.java | 2 +- .../io/helidon/webserver/grpc/SafeStreamObserver.java | 11 +++++------ .../webserver/grpc/ServerRequestAttribute.java | 5 +++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java index b1ab322c97c..ad3d06ed46b 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/CollectingObserver.java @@ -43,7 +43,7 @@ public class CollectingObserver implements StreamObserver { private final A accumulator; /** - * Constructor + * Constructor. * * @param collector the collector * @param observer the observer @@ -53,7 +53,7 @@ public CollectingObserver(Collector collector, StreamObserver observ } /** - * Constructor + * Constructor. * * @param collector the collector * @param observer the observer @@ -66,7 +66,7 @@ public CollectingObserver(Collector collector, } /** - * Constructor + * Constructor. * * @param collector the collector * @param observer the observer @@ -81,7 +81,7 @@ public CollectingObserver(Collector collector, } /** - * Constructor + * Constructor. * * @param collector the collector * @param observer the observer diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java index fb4955c4f6e..ba6e1fc8230 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/ContextSettingServerInterceptor.java @@ -66,7 +66,7 @@ public ServerCall.Listener interceptCall(ServerCall helidonContext = io.helidon.common.context.Contexts.context(); - context = Context.current().withValue(ContextKeys.HELIDON_CONTEXT, + context = context.withValue(ContextKeys.HELIDON_CONTEXT, helidonContext.orElseGet(io.helidon.common.context.Context::create)); // method info diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index 2af5334b42e..5fa7228832b 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -30,6 +30,7 @@ import io.grpc.stub.ServerCalls; class Grpc extends GrpcRoute { + private final MethodDescriptor method; private final PathMatcher pathMatcher; private final ServerCallHandler callHandler; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcHeadersUtil.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcHeadersUtil.java index 68b7570fe33..5016d0a65f3 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcHeadersUtil.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcHeadersUtil.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.helidon.webserver.grpc; import java.util.Base64; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java index b4be7eafbb6..aae1a90df32 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java @@ -78,7 +78,7 @@ public SubProtocolResult subProtocol(ConnectionContext ctx, new GrpcProtocolHandlerNotFound(streamWriter, streamId, currentStreamState)); } return new SubProtocolResult(true, - new GrpcProtocolHandler(prologue, + new GrpcProtocolHandler<>(prologue, headers, streamWriter, streamId, diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/SafeStreamObserver.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/SafeStreamObserver.java index c7e1a51934c..08a9e11aba4 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/SafeStreamObserver.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/SafeStreamObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * 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. @@ -27,14 +27,13 @@ * * @param the type of response expected */ -class SafeStreamObserver - implements StreamObserver { +class SafeStreamObserver implements StreamObserver { private static final System.Logger LOGGER = System.getLogger(SafeStreamObserver.class.getName()); /** * The actual StreamObserver. */ - private StreamObserver delegate; + private final StreamObserver delegate; /** * Indicates a terminal state. */ @@ -132,8 +131,8 @@ public StreamObserver delegate() { // ----- data members --------------------------------------------------- /** - * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These varieties are - * as follows: + * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. + * These varieties are as follows: *