diff --git a/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java b/extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscoding.java similarity index 84% rename from extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java rename to extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscoding.java index 7d410ebe8e105..7142ad2163c29 100644 --- a/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java +++ b/extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscoding.java @@ -2,7 +2,7 @@ import com.google.protobuf.Message; -public interface MutinyTranscodingService { +public interface GrpcTranscoding { String getGrpcServiceName(); diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java index cdefebfd64c12..513f9d9cea4ef 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java @@ -16,13 +16,13 @@ import io.quarkus.grpc.GlobalInterceptor; import io.quarkus.grpc.GrpcClient; import io.quarkus.grpc.GrpcService; +import io.quarkus.grpc.GrpcTranscoding; import io.quarkus.grpc.GrpcTranscodingMethod; import io.quarkus.grpc.MutinyBean; import io.quarkus.grpc.MutinyClient; import io.quarkus.grpc.MutinyGrpc; import io.quarkus.grpc.MutinyService; import io.quarkus.grpc.MutinyStub; -import io.quarkus.grpc.MutinyTranscodingService; import io.quarkus.grpc.RegisterClientInterceptor; import io.quarkus.grpc.RegisterInterceptor; import io.quarkus.grpc.RegisterInterceptors; @@ -51,7 +51,7 @@ public class GrpcDotNames { public static final DotName MUTINY_BEAN = DotName.createSimple(MutinyBean.class.getName()); public static final DotName MUTINY_SERVICE = DotName.createSimple(MutinyService.class.getName()); - public static final DotName MUTINY_TRANSCODING_SERVICE = DotName.createSimple(MutinyTranscodingService.class.getName()); + public static final DotName GRPC_TRANSCODING = DotName.createSimple(GrpcTranscoding.class.getName()); public static final DotName GRPC_METHOD = DotName.createSimple(GrpcTranscodingMethod.class.getName()); public static final DotName GLOBAL_INTERCEPTOR = DotName.createSimple(GlobalInterceptor.class.getName()); diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java index dd42eb2c7b2a3..677c243adb9e0 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java @@ -683,7 +683,6 @@ ServiceStartBuildItem initializeServer(GrpcServerRecorder recorder, List orderEnforcer, LaunchModeBuildItem launchModeBuildItem, VertxWebRouterBuildItem routerBuildItem, - GrpcTranscodingServerBuildItem transcodingServerBuildItem, VertxBuildItem vertx, Capabilities capabilities) { // Build the list of blocking methods per service implementation @@ -705,7 +704,6 @@ ServiceStartBuildItem initializeServer(GrpcServerRecorder recorder, //Uses mainrouter when the 'quarkus.http.root-path' is not '/' recorder.initializeGrpcServer(vertx.getVertx(), routerBuildItem.getMainRouter() != null ? routerBuildItem.getMainRouter() : routerBuildItem.getHttpRouter(), - transcodingServerBuildItem.getTranscodingServer(), config, shutdown, blocking, virtuals, launchModeBuildItem.getLaunchMode(), capabilities.isPresent(Capability.SECURITY)); return new ServiceStartBuildItem(GRPC_SERVER); diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMarshallingBuildItem.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingBuildItem.java similarity index 75% rename from extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMarshallingBuildItem.java rename to extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingBuildItem.java index a99146bc7f47c..c1013dd4e1fd9 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMarshallingBuildItem.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingBuildItem.java @@ -5,14 +5,14 @@ import org.jboss.jandex.DotName; import io.quarkus.builder.item.MultiBuildItem; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingMethod; +import io.quarkus.grpc.transcoding.GrpcTranscodingMethod; -public final class GrpcMarshallingBuildItem extends MultiBuildItem { +public final class GrpcTranscodingBuildItem extends MultiBuildItem { final DotName marshallingClass; final List transcodingMethods; - public GrpcMarshallingBuildItem(DotName marshallingClass, List transcodingMethods) { + public GrpcTranscodingBuildItem(DotName marshallingClass, List transcodingMethods) { this.marshallingClass = marshallingClass; this.transcodingMethods = transcodingMethods; } diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingProcessor.java index 72e408aa2e6ad..d9c0106c4bd62 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingProcessor.java @@ -30,10 +30,10 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; -import io.quarkus.grpc.runtime.GrpcTranscodingRecorder; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingContainer; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingMethod; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingServer; +import io.quarkus.grpc.transcoding.GrpcTranscodingContainer; +import io.quarkus.grpc.transcoding.GrpcTranscodingMethod; +import io.quarkus.grpc.transcoding.GrpcTranscodingRecorder; +import io.quarkus.grpc.transcoding.GrpcTranscodingServer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.deployment.VertxBuildItem; import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem; @@ -54,20 +54,17 @@ FeatureBuildItem feature() { @BuildStep void processGeneratedBeans(CombinedIndexBuildItem index, BuildProducer transformers, - BuildProducer marshallings, + BuildProducer marshallings, BuildProducer delegatingBeans) { Set generatedBeans = new HashSet<>(); - Map> methods = new HashMap<>(); - for (ClassInfo generatedBean : index.getIndex().getAllKnownImplementors(GrpcDotNames.MUTINY_TRANSCODING_SERVICE)) { + for (ClassInfo generatedBean : index.getIndex().getAllKnownImplementors(GrpcDotNames.GRPC_TRANSCODING)) { DotName generatedBeanName = generatedBean.name(); generatedBeans.add(generatedBeanName); + // Extract gRPC transcoding configuration from methods and store the results List transcodingMethods = collectTranscodingMethods(generatedBean); - methods.put(generatedBeanName.toString(), transcodingMethods); - - marshallings.produce(new GrpcMarshallingBuildItem(generatedBeanName, transcodingMethods)); - log.info("Registering a delegating bean for {}", generatedBeanName); + marshallings.produce(new GrpcTranscodingBuildItem(generatedBeanName, transcodingMethods)); } delegatingBeans.produce(AdditionalBeanBuildItem.unremovableOf(GrpcTranscodingContainer.class)); @@ -80,6 +77,7 @@ public boolean appliesTo(AnnotationTarget.Kind kind) { @Override public void transform(TransformationContext context) { + // Check if the class is a generated gRPC transcoding bean if (generatedBeans.contains(context.getTarget().asClass().name())) { context.transform() .add(BuiltinScope.SINGLETON.getName()) @@ -95,20 +93,20 @@ public void transform(TransformationContext context) { GrpcTranscodingServerBuildItem buildTranscoding(GrpcTranscodingRecorder recorder, VertxBuildItem vertx, VertxWebRouterBuildItem routerBuildItem, - List marshallings, + List marshallings, Capabilities capabilities, ShutdownContextBuildItem shutdown) { + // Build a map to organize the collected gRPC transcoding methods by service name Map> methods = new HashMap<>(); - for (GrpcMarshallingBuildItem item : marshallings) { + for (GrpcTranscodingBuildItem item : marshallings) { String name = item.getMarshallingClass().toString().replace("Marshalling", ""); - log.info("Registering transcoding methods for {}", name); methods.put(name, item.getTranscodingMethods()); } + // Create and initialize the gRPC transcoding server RuntimeValue server = recorder.initializeMarshallingServer(vertx.getVertx(), routerBuildItem.getHttpRouter(), shutdown, methods, capabilities.isPresent(Capability.SECURITY)); return new GrpcTranscodingServerBuildItem(server); - //recorder.buildTranscoding(routerBuildItem.getHttpRouter(), methods, shutdown); } private static List collectTranscodingMethods(ClassInfo service) { diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingServerBuildItem.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingServerBuildItem.java index 4041cb7abdc45..83ef70aa598b2 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingServerBuildItem.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcTranscodingServerBuildItem.java @@ -1,7 +1,7 @@ package io.quarkus.grpc.deployment; import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingServer; +import io.quarkus.grpc.transcoding.GrpcTranscodingServer; import io.quarkus.runtime.RuntimeValue; public final class GrpcTranscodingServerBuildItem extends SimpleBuildItem { diff --git a/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache b/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache index a507ddd304a72..ee0b3244c50b3 100644 --- a/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache +++ b/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache @@ -4,7 +4,7 @@ package {{packageName}}; import jakarta.ws.rs.core.Response; import io.quarkus.grpc.GrpcService; -import io.quarkus.grpc.MutinyTranscodingService; +import io.quarkus.grpc.GrpcTranscoding; import io.quarkus.grpc.GrpcTranscodingDescriptor; import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; @@ -13,7 +13,7 @@ import com.google.protobuf.InvalidProtocolBufferException; @jakarta.annotation.Generated( value = "by {{classPrefix}} Grpc generator", comments = "Source: {{protoName}}") -public class {{serviceName}}Marshalling implements MutinyTranscodingService { +public class {{serviceName}}Marshalling implements GrpcTranscoding { @Override public String getGrpcServiceName() { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java index 8aded4bb4940a..98a61b9775338 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcServerRecorder.java @@ -53,7 +53,6 @@ import io.quarkus.grpc.runtime.reflection.ReflectionServiceV1alpha; import io.quarkus.grpc.runtime.supports.CompressionInterceptor; import io.quarkus.grpc.runtime.supports.blocking.BlockingServerInterceptor; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingServer; import io.quarkus.grpc.spi.GrpcBuilderProvider; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.QuarkusBindException; @@ -93,7 +92,6 @@ public static List getServices() { public void initializeGrpcServer(RuntimeValue vertxSupplier, RuntimeValue routerSupplier, - RuntimeValue transcodingServerSupplier, GrpcConfiguration cfg, ShutdownContext shutdown, Map> blockingMethodsPerService, @@ -135,7 +133,7 @@ public void initializeGrpcServer(RuntimeValue vertxSupplier, launchMode); } } else { - buildGrpcServer(vertx, configuration, routerSupplier, transcodingServerSupplier, shutdown, + buildGrpcServer(vertx, configuration, routerSupplier, shutdown, blockingMethodsPerService, virtualMethodsPerService, grpcContainer, launchMode, securityPresent); } @@ -143,13 +141,11 @@ public void initializeGrpcServer(RuntimeValue vertxSupplier, // TODO -- handle XDS private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration, RuntimeValue routerSupplier, - RuntimeValue transcodingServerSupplier, ShutdownContext shutdown, Map> blockingMethodsPerService, Map> virtualMethodsPerService, GrpcContainer grpcContainer, LaunchMode launchMode, boolean securityPresent) { GrpcServer server = GrpcServer.server(vertx); - GrpcTranscodingServer transcodingServer = transcodingServerSupplier.getValue(); List globalInterceptors = grpcContainer.getSortedGlobalInterceptors(); @@ -169,7 +165,6 @@ private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration, vertx, grpcContainer, blockingMethodsPerService, virtualMethodsPerService, compressionInterceptor, globalInterceptors, service, launchMode == LaunchMode.DEVELOPMENT); - LOGGER.infof("Registered gRPC service '%s'", service.definition.getServiceDescriptor().getName()); GrpcServiceBridge bridge = GrpcServiceBridge.bridge(serviceDefinition); bridge.bind(server); definitions.add(service.definition); @@ -228,11 +223,6 @@ private static boolean isGrpc(RoutingContext rc) { return header != null && GRPC_CONTENT_TYPE.matcher(header.toLowerCase(Locale.ROOT)).matches(); } - private static boolean isTranscoding(RoutingContext rc) { - String header = rc.request().getHeader("content-type"); - return header != null && header.toLowerCase(Locale.ROOT).startsWith("application/json"); - } - private void prodStart(GrpcContainer grpcContainer, Vertx vertx, GrpcServerConfiguration configuration, GrpcBuilderProvider provider, Map> blockingMethodsPerService, Map> virtualMethodsPerService, @@ -411,7 +401,7 @@ public static final class GrpcServiceDefinition { public final BindableService service; public final ServerServiceDefinition definition; - GrpcServiceDefinition(BindableService service, ServerServiceDefinition definition) { + public GrpcServiceDefinition(BindableService service, ServerServiceDefinition definition) { this.service = service; this.definition = definition; } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java deleted file mode 100644 index db8ee8d349401..0000000000000 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java +++ /dev/null @@ -1,497 +0,0 @@ -package io.quarkus.grpc.runtime.transcoding; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.grpc.MethodDescriptor; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.grpc.common.GrpcMessageDecoder; -import io.vertx.grpc.common.GrpcMessageEncoder; -import io.vertx.grpc.common.impl.GrpcMethodCall; -import io.vertx.grpc.server.GrpcServer; -import io.vertx.grpc.server.GrpcServerRequest; - -// # gRPC Transcoding -// -// gRPC Transcoding is a feature for mapping between a gRPC method and one or -// more HTTP REST endpoints. It allows developers to build a single API service -// that supports both gRPC APIs and REST APIs. Many systems, including [Google -// APIs](https://github.com/googleapis/googleapis), -// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC -// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), -// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature -// and use it for large scale production services. -// -// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies -// how different portions of the gRPC request message are mapped to the URL -// path, URL query parameters, and HTTP request body. It also controls how the -// gRPC response message is mapped to the HTTP response body. `HttpRule` is -// typically specified as an `google.api.http` annotation on the gRPC method. -// -// Each mapping specifies a URL path template and an HTTP method. The path -// template may refer to one or more fields in the gRPC request message, as long -// as each field is a non-repeated field with a primitive (non-message) type. -// The path template controls how fields of the request message are mapped to -// the URL path. -// -// Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/{name=messages/*}" -// }; -// } -// } -// message GetMessageRequest { -// string name = 1; // Mapped to URL path. -// } -// message Message { -// string text = 1; // The resource content. -// } -// -// This enables an HTTP REST to gRPC mapping as below: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` -// -// Any fields in the request message which are not bound by the path template -// automatically become HTTP query parameters if there is no HTTP request body. -// For example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get:"/v1/messages/{message_id}" -// }; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // Mapped to URL path. -// int64 revision = 2; // Mapped to URL query parameter `revision`. -// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. -// } -// -// This enables a HTTP JSON to RPC mapping as below: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | -// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: -// "foo"))` -// -// Note that fields which are mapped to URL query parameters must have a -// primitive type or a repeated primitive type or a non-repeated message type. -// In the case of a repeated type, the parameter can be repeated in the URL -// as `...?param=A¶m=B`. In the case of a message type, each field of the -// message is mapped to a separate parameter, such as -// `...?foo.a=A&foo.b=B&foo.c=C`. -// -// For HTTP methods that allow a request body, the `body` field -// specifies the mapping. Consider a REST update method on the -// message resource collection: -// -// service Messaging { -// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { -// option (google.api.http) = { -// patch: "/v1/messages/{message_id}" -// body: "message" -// }; -// } -// } -// message UpdateMessageRequest { -// string message_id = 1; // mapped to the URL -// Message message = 2; // mapped to the body -// } -// -// The following HTTP JSON to RPC mapping is enabled, where the -// representation of the JSON in the request body is determined by -// protos JSON encoding: -// -// HTTP | gRPC -// -----|----- -// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -// "123456" message { text: "Hi!" })` -// -// The special name `*` can be used in the body mapping to define that -// every field not bound by the path template should be mapped to the -// request body. This enables the following alternative definition of -// the update method: -// -// service Messaging { -// rpc UpdateMessage(Message) returns (Message) { -// option (google.api.http) = { -// patch: "/v1/messages/{message_id}" -// body: "*" -// }; -// } -// } -// message Message { -// string message_id = 1; -// string text = 2; -// } -// -// -// The following HTTP JSON to RPC mapping is enabled: -// -// HTTP | gRPC -// -----|----- -// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -// "123456" text: "Hi!")` -// -// Note that when using `*` in the body mapping, it is not possible to -// have HTTP parameters, as all fields not bound by the path end in -// the body. This makes this option more rarely used in practice when -// defining REST APIs. The common usage of `*` is in custom methods -// which don't use the URL at all for transferring data. -// -// It is possible to define multiple HTTP methods for one RPC by using -// the `additional_bindings` option. Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/messages/{message_id}" -// additional_bindings { -// get: "/v1/users/{user_id}/messages/{message_id}" -// } -// }; -// } -// } -// message GetMessageRequest { -// string message_id = 1; -// string user_id = 2; -// } -// -// This enables the following two alternative HTTP JSON to RPC mappings: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` -// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: -// "123456")` -// -// ## Rules for HTTP mapping -// -// 1. Leaf request fields (recursive expansion nested messages in the request -// message) are classified into three categories: -// - Fields referred by the path template. They are passed via the URL path. -// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They -// are passed via the HTTP -// request body. -// - All other fields are passed via the URL query parameters, and the -// parameter name is the field path in the request message. A repeated -// field can be represented as multiple query parameters under the same -// name. -// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL -// query parameter, all fields -// are passed via URL path and HTTP request body. -// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP -// request body, all -// fields are passed via URL path and URL query parameters. -// -// ### Path template syntax -// -// Template = "/" Segments [ Verb ] ; -// Segments = Segment { "/" Segment } ; -// Segment = "*" | "**" | LITERAL | Variable ; -// Variable = "{" FieldPath [ "=" Segments ] "}" ; -// FieldPath = IDENT { "." IDENT } ; -// Verb = ":" LITERAL ; -// -// The syntax `*` matches a single URL path segment. The syntax `**` matches -// zero or more URL path segments, which must be the last part of the URL path -// except the `Verb`. -// -// The syntax `Variable` matches part of the URL path as specified by its -// template. A variable template must not contain other variables. If a variable -// matches a single path segment, its template may be omitted, e.g. `{var}` -// is equivalent to `{var=*}`. -// -// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` -// contains any reserved character, such characters should be percent-encoded -// before the matching. -// -// If a variable contains exactly one path segment, such as `"{var}"` or -// `"{var=*}"`, when such a variable is expanded into a URL path on the client -// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The -// server side does the reverse decoding. Such variables show up in the -// [Discovery -// Document](https://developers.google.com/discovery/v1/reference/apis) as -// `{var}`. -// -// If a variable contains multiple path segments, such as `"{var=foo/*}"` -// or `"{var=**}"`, when such a variable is expanded into a URL path on the -// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. -// The server side does the reverse decoding, except "%2F" and "%2f" are left -// unchanged. Such variables show up in the -// [Discovery -// Document](https://developers.google.com/discovery/v1/reference/apis) as -// `{+var}`. -// -// ## Using gRPC API Service Configuration -// -// gRPC API Service Configuration (service config) is a configuration language -// for configuring a gRPC service to become a user-facing product. The -// service config is simply the YAML representation of the `google.api.Service` -// proto message. -// -// As an alternative to annotating your proto file, you can configure gRPC -// transcoding in your service config YAML files. You do this by specifying a -// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same -// effect as the proto annotation. This can be particularly useful if you -// have a proto that is reused in multiple services. Note that any transcoding -// specified in the service config will override any matching transcoding -// configuration in the proto. -// -// Example: -// -// http: -// rules: -// # Selects a gRPC method and applies HttpRule to it. -// - selector: example.v1.Messaging.GetMessage -// get: /v1/messages/{message_id}/{sub.subfield} -// -// ## Special notes -// -// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the -// proto to JSON conversion must follow the [proto3 -// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). -// -// While the single segment variable follows the semantics of -// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String -// Expansion, the multi segment variable **does not** follow RFC 6570 Section -// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion -// does not expand special characters like `?` and `#`, which would lead -// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding -// for multi segment variables. -// -// The path variables **must not** refer to any repeated or mapped field, -// because client libraries are not capable of handling such variable expansion. -// -// The path variables **must not** capture the leading "/" character. The reason -// is that the most common use case "{var}" does not capture the leading "/" -// character. For consistency, all path variables must share the same behavior. -// -// Repeated message fields must not be mapped to URL query parameters, because -// no client library can support such complicated mapping. -// -// If an API needs to use a JSON array for request or response body, it can map -// the request or response body to a repeated field. However, some gRPC -// Transcoding implementations may not support this feature. -public class GrpcTranscodingServer implements GrpcServer { - - private final Vertx vertx; - private Handler> requestHandler; - private Map methodMapping = new HashMap<>(); - private Map> methodCallHandlers = new HashMap<>(); - private Map> metadataHandlers = new HashMap<>(); - - public GrpcTranscodingServer(Vertx vertx) { - this.vertx = vertx; - } - - @Override - public void handle(HttpServerRequest httpRequest) { - String requestPath = httpRequest.path(); - - // Find the best matching method based on path parsing - for (Map.Entry entry : methodMapping.entrySet()) { - String pathTemplate = entry.getKey(); - String mappedMethod = entry.getValue(); - if (isPathMatch(requestPath, pathTemplate)) { - handleWithMappedMethod(httpRequest, pathTemplate, mappedMethod); - return; // Match found, stop searching - } - } - - // If no match found - handle as needed (e.g., 404) - httpRequest.response().setStatusCode(404).end(); - } - - private void handleWithMappedMethod(HttpServerRequest httpRequest, String pathTemplate, String mappedMethod) { - GrpcMethodCall methodCall = new GrpcMethodCall("/" + mappedMethod); - String fmn = methodCall.fullMethodName(); - MethodCallHandler method = methodCallHandlers.get(fmn); - - if (method != null) { - handle(pathTemplate, method, httpRequest, methodCall); - } else { - httpRequest.response().setStatusCode(500).end(); - } - } - - private void handle(String pathTemplate, MethodCallHandler method, HttpServerRequest httpRequest, - GrpcMethodCall methodCall) { - Map pathParams = extractPathParams(pathTemplate, httpRequest.path()); - Map queryParameters = new HashMap<>(httpRequest.params().entries().stream() - .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), HashMap::putAll)); - - GrpcTranscodingRequest grpcRequest = new GrpcTranscodingRequest<>(httpRequest, method.messageDecoder, - method.messageEncoder, methodCall, pathParams, queryParameters); - grpcRequest.init(); - method.handle(grpcRequest); - } - - public GrpcServer callHandler(Handler> handler) { - this.requestHandler = handler; - return this; - } - - @Override - public GrpcServer callHandler(MethodDescriptor methodDesc, - Handler> handler) { - if (handler != null) { - MethodDescriptor.Marshaller reqMarshaller = findRequestMarshaller(methodDesc.getFullMethodName()); - MethodDescriptor.Marshaller respMarshaller = findResponseMarshaller(methodDesc.getFullMethodName()); - - methodCallHandlers.put(methodDesc.getFullMethodName(), - new GrpcTranscodingServer.MethodCallHandler<>(methodDesc, - GrpcMessageDecoder.unmarshaller(reqMarshaller), - GrpcMessageEncoder.marshaller(respMarshaller), handler)); - } else { - methodCallHandlers.remove(methodDesc.getFullMethodName()); - } - return this; - } - - public void addMethodMapping(String path, String fullMethodName) { - methodMapping.put(path, fullMethodName); - } - - public void addMetadataHandler(String fullMethodName, GrpcTranscodingMetadata metadata) { - metadataHandlers.put(fullMethodName, metadata); - } - - @SuppressWarnings("unchecked") - public MethodDescriptor.Marshaller findRequestMarshaller(String fullMethodName) { - GrpcTranscodingMetadata metadata = metadataHandlers.get(fullMethodName); - return (MethodDescriptor.Marshaller) metadata.getRequestMarshaller(); - } - - @SuppressWarnings("unchecked") - public MethodDescriptor.Marshaller findResponseMarshaller(String fullMethodName) { - GrpcTranscodingMetadata metadata = metadataHandlers.get(fullMethodName); - return (MethodDescriptor.Marshaller) metadata.getResponseMarshaller(); - } - - private boolean isPathMatch(String requestPath, String pathTemplate) { - String[] requestParts = requestPath.split("/"); - String[] templateParts = parsePath(pathTemplate).split("/"); // Use the enhanced parsePath - - if (requestParts.length != templateParts.length) { - return false; // Mismatch in number of segments - } - - for (int i = 0; i < requestParts.length; i++) { - String requestPart = requestParts[i]; - String templatePart = templateParts[i]; - - if (templatePart.startsWith("{") && templatePart.endsWith("}")) { - // It's a variable, any non-empty segment would match - if (requestPart.isEmpty()) { - return false; - } - } else { - // It's a literal segment - if (!requestPart.equals(templatePart)) { - return false; - } - } - } - - return true; // All segments matched - } - - public String parsePath(String path) { - Map extractedParams = extractPathParams(path); - StringBuilder parsedPathBuilder = new StringBuilder(); - - for (Map.Entry entry : extractedParams.entrySet()) { - parsedPathBuilder.append("/").append(entry.getValue()); - } - - return parsedPathBuilder.toString(); - } - - public Map extractPathParams(String pathTemplate, String httpPath) { - Map extractedParams = new HashMap<>(); - - String[] pathParts = httpPath.split("/"); - String[] templateParts = pathTemplate.split("/"); - - for (int i = 0; i < pathParts.length; i++) { - String pathPart = pathParts[i]; - String templatePart = templateParts[i]; - - if (templatePart.startsWith("{") && templatePart.endsWith("}")) { - String paramName = templatePart.substring(1, templatePart.length() - 1); - extractedParams.put(paramName, pathPart); - } - } - - return extractedParams; - } - - public Map extractPathParams(String path) { - // Regular expressions for template segments - String singleSegmentVarPattern = "\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}"; - String multiSegmentVarPattern = "\\{\\+([a-zA-Z_][a-zA-Z0-9_]*)\\}"; - - Map extractedParams = new LinkedHashMap<>(); - - String[] parts = path.split("/"); - - for (String part : parts) { - if (part.isEmpty()) { - continue; // Skip empty segments - } - - Matcher singleMatcher = Pattern.compile(singleSegmentVarPattern).matcher(part); - Matcher multiMatcher = Pattern.compile(multiSegmentVarPattern).matcher(part); - - if (singleMatcher.matches()) { - String paramName = singleMatcher.group(1); - extractedParams.put(paramName, "{" + paramName + "}"); - } else if (multiMatcher.matches()) { - String paramName = multiMatcher.group(1); - extractedParams.put(paramName, "{+" + paramName + "}"); - } else { - extractedParams.put(part, part); - } - } - - return extractedParams; - } - - private static class MethodCallHandler implements Handler> { - - final MethodDescriptor def; - final GrpcMessageDecoder messageDecoder; - final GrpcMessageEncoder messageEncoder; - final Handler> handler; - - MethodCallHandler(MethodDescriptor def, GrpcMessageDecoder messageDecoder, - GrpcMessageEncoder messageEncoder, Handler> handler) { - this.def = def; - this.messageDecoder = messageDecoder; - this.messageEncoder = messageEncoder; - this.handler = handler; - } - - @Override - public void handle(GrpcServerRequest grpcRequest) { - handler.handle(grpcRequest); - } - } -} diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingBridge.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingBridge.java similarity index 93% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingBridge.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingBridge.java index ae4c293d8c826..c0ad3278cd480 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingBridge.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingBridge.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -51,8 +51,10 @@ private void bind(GrpcTranscodingServer server, ServerMethodDefiniti .findRequestMarshaller(methodDef.getMethodDescriptor().getFullMethodName()); MethodDescriptor.Marshaller respMarshaller = server .findResponseMarshaller(methodDef.getMethodDescriptor().getFullMethodName()); + GrpcTranscodingBridge.ServerCallImpl call = new GrpcTranscodingBridge.ServerCallImpl<>(req, methodDef, reqMarshaller, respMarshaller); + ServerCall.Listener listener = callHandler.startCall(call, Utils.readMetadata(req.headers())); call.init(listener); }); @@ -66,8 +68,8 @@ private static class ServerCallImpl extends ServerCall { private final MethodDescriptor.Marshaller reqMarshaller; private final MethodDescriptor.Marshaller respMarshaller; - private final TranscodingReadStreamAdapter readAdapter; - private final TranscodingWriteStreamAdapter writeAdapter; + private final GrpcTranscodingReadStreamAdapter readAdapter; + private final GrpcTranscodingWriteStreamAdapter writeAdapter; private ServerCall.Listener listener; private boolean halfClosed; private boolean closed; @@ -80,7 +82,7 @@ public ServerCallImpl(GrpcServerRequest req, ServerMethodDefinition() { + this.readAdapter = new GrpcTranscodingReadStreamAdapter() { @Override protected void handleClose() { halfClosed = true; @@ -92,7 +94,7 @@ protected void handleMessage(Req msg) { listener.onMessage(msg); } }; - this.writeAdapter = new TranscodingWriteStreamAdapter() { + this.writeAdapter = new GrpcTranscodingWriteStreamAdapter() { @Override protected void handleReady() { listener.onReady(); @@ -109,8 +111,8 @@ void init(ServerCall.Listener listener) { } }); - readAdapter.init(req, new TranscodingMessageDecoder<>(reqMarshaller)); - writeAdapter.init(req.response(), new TranscodingMessageEncoder<>(respMarshaller)); + readAdapter.init(req, new GrpcTranscodingMessageDecoder<>(reqMarshaller)); + writeAdapter.init(req.response(), new GrpcTranscodingMessageEncoder<>(respMarshaller)); } private Attributes createAttributes() { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingContainer.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingContainer.java similarity index 53% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingContainer.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingContainer.java index ee77a85482268..7f8cfec3119ab 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingContainer.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingContainer.java @@ -1,18 +1,18 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import io.quarkus.grpc.MutinyTranscodingService; +import io.quarkus.grpc.GrpcTranscoding; @ApplicationScoped public class GrpcTranscodingContainer { @Inject - Instance services; + Instance services; - public Instance getServices() { + public Instance getServices() { return services; } } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java similarity index 67% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java index 15e6973e41eff..5e131509111c8 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import java.io.ByteArrayInputStream; @@ -6,11 +6,11 @@ import io.vertx.grpc.common.GrpcMessage; import io.vertx.grpc.common.GrpcMessageDecoder; -public class TranscodingMessageDecoder implements GrpcMessageDecoder { +public class GrpcTranscodingMessageDecoder implements GrpcMessageDecoder { private final MethodDescriptor.Marshaller marshaller; - public TranscodingMessageDecoder(MethodDescriptor.Marshaller marshaller) { + public GrpcTranscodingMessageDecoder(MethodDescriptor.Marshaller marshaller) { this.marshaller = marshaller; } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageEncoder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java similarity index 84% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageEncoder.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java index 92d63f96e54ee..48f991b11c49e 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageEncoder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -8,11 +8,11 @@ import io.vertx.grpc.common.GrpcMessage; import io.vertx.grpc.common.GrpcMessageEncoder; -public class TranscodingMessageEncoder implements GrpcMessageEncoder { +public class GrpcTranscodingMessageEncoder implements GrpcMessageEncoder { private final MethodDescriptor.Marshaller marshaller; - public TranscodingMessageEncoder(MethodDescriptor.Marshaller marshaller) { + public GrpcTranscodingMessageEncoder(MethodDescriptor.Marshaller marshaller) { this.marshaller = marshaller; } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMetadata.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java similarity index 97% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMetadata.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java index 1b4808b51c635..031cd84a213a9 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMetadata.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import com.google.protobuf.Message; diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMethod.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java similarity index 93% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMethod.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java index 08e908beaed2e..5ad1e3765d617 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingMethod.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; public final class GrpcTranscodingMethod { diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingReadStreamAdapter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java similarity index 83% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingReadStreamAdapter.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java index 0e31fd41b2f1c..b5aa6f1b160af 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingReadStreamAdapter.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java @@ -1,8 +1,8 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import io.vertx.grpc.common.GrpcReadStream; -public class TranscodingReadStreamAdapter { +public class GrpcTranscodingReadStreamAdapter { private GrpcReadStream stream; private int request = 0; @@ -10,7 +10,7 @@ public class TranscodingReadStreamAdapter { /** * Init the adapter with the stream. */ - public final void init(GrpcReadStream stream, TranscodingMessageDecoder decoder) { + public final void init(GrpcReadStream stream, GrpcTranscodingMessageDecoder decoder) { stream.messageHandler(msg -> { handleMessage(decoder.decode(msg)); }); diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java similarity index 86% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java index 47a9882e78541..0d4265eca8f8b 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime; +package io.quarkus.grpc.transcoding; import java.util.ArrayList; import java.util.List; @@ -15,21 +15,17 @@ import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; import io.quarkus.arc.Arc; +import io.quarkus.grpc.GrpcTranscoding; import io.quarkus.grpc.GrpcTranscodingDescriptor; -import io.quarkus.grpc.MutinyTranscodingService; import io.quarkus.grpc.auth.GrpcSecurityInterceptor; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingBridge; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingContainer; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingMetadata; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingMethod; -import io.quarkus.grpc.runtime.transcoding.GrpcTranscodingServer; +import io.quarkus.grpc.runtime.GrpcContainer; +import io.quarkus.grpc.runtime.GrpcServerRecorder; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; @@ -56,12 +52,12 @@ public RuntimeValue initializeMarshallingServer(RuntimeVa } List grpcServices = collectServiceDefinitions(grpcContainer.getServices()); - List transcodingServices = collectTranscodingServices(container.getServices()); + List transcodingServices = collectTranscodingServices(container.getServices()); List> mappedMethods = new ArrayList<>(); LOGGER.info("Initializing gRPC transcoding services"); - for (MutinyTranscodingService transcodingService : transcodingServices) { + for (GrpcTranscoding transcodingService : transcodingServices) { GrpcServerRecorder.GrpcServiceDefinition grpcService = findGrpcService(grpcServices, transcodingService); List transcodingMethods = findTranscodingMethods(httpMethods, transcodingService.getGrpcServiceName()); @@ -72,8 +68,6 @@ public RuntimeValue initializeMarshallingServer(RuntimeVa GrpcTranscodingMethod transcodingMethod = findTranscodingMethod(transcodingMethods, methodDescriptor); String path = transcodingMethod.getUriTemplate(); - HttpMethod httpMethod = HttpMethod.valueOf(transcodingMethod.getHttpMethodName()); - GrpcTranscodingMetadata metadata = createMetadata(transcodingMethod, methodDescriptor, transcodingService); @@ -114,14 +108,15 @@ public void handle(Void unused) { private GrpcTranscodingMetadata createMetadata( GrpcTranscodingMethod transcodingMethod, MethodDescriptor methodDescriptor, - MutinyTranscodingService transcodingService) { + GrpcTranscoding transcodingService) { String fullMethodName = methodDescriptor.getFullMethodName() .substring(methodDescriptor.getFullMethodName().lastIndexOf("/") + 1); fullMethodName = Character.toLowerCase(fullMethodName.charAt(0)) + fullMethodName.substring(1); GrpcTranscodingDescriptor descriptor = transcodingService.findTranscodingDescriptor(fullMethodName); - return new GrpcTranscodingMetadata<>(transcodingMethod.getHttpMethodName(), + return new GrpcTranscodingMetadata<>( + transcodingMethod.getHttpMethodName(), fullMethodName, descriptor.getRequestMarshaller(), descriptor.getResponseMarshaller(), @@ -166,9 +161,9 @@ private static List collectServiceDefi return definitions; } - private static List collectTranscodingServices(Instance services) { - List transcodingServices = new ArrayList<>(); - for (MutinyTranscodingService service : services) { + private static List collectTranscodingServices(Instance services) { + List transcodingServices = new ArrayList<>(); + for (GrpcTranscoding service : services) { transcodingServices.add(service); } @@ -176,7 +171,7 @@ private static List collectTranscodingServices(Instanc } private static GrpcServerRecorder.GrpcServiceDefinition findGrpcService( - List grpcServices, MutinyTranscodingService transcodingService) { + List grpcServices, GrpcTranscoding transcodingService) { for (GrpcServerRecorder.GrpcServiceDefinition grpcService : grpcServices) { if (grpcService.getImplementationClassName().startsWith(transcodingService.getGrpcServiceName())) { return grpcService; diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java similarity index 99% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java index f1f835696a9d5..8af75490b65ce 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import static io.vertx.grpc.common.GrpcError.mapHttp2ErrorCode; diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java similarity index 99% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java index bc54d39d07cc1..820bb0595d369 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java @@ -1,4 +1,4 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import java.util.Map; import java.util.Objects; diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingServer.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingServer.java new file mode 100644 index 0000000000000..bafbafc7acd17 --- /dev/null +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingServer.java @@ -0,0 +1,231 @@ +package io.quarkus.grpc.transcoding; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.grpc.MethodDescriptor; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.grpc.common.GrpcMessageDecoder; +import io.vertx.grpc.common.GrpcMessageEncoder; +import io.vertx.grpc.common.impl.GrpcMethodCall; +import io.vertx.grpc.server.GrpcServer; +import io.vertx.grpc.server.GrpcServerRequest; + +/** + * @see for the HTTP mapping rules + */ +public class GrpcTranscodingServer implements GrpcServer { + + private final Vertx vertx; + private Handler> requestHandler; + private Map methodMapping = new HashMap<>(); + private Map> methodCallHandlers = new HashMap<>(); + private Map> metadataHandlers = new HashMap<>(); + + public GrpcTranscodingServer(Vertx vertx) { + this.vertx = vertx; + } + + @Override + public void handle(HttpServerRequest httpRequest) { + String requestPath = httpRequest.path(); + + for (Map.Entry entry : methodMapping.entrySet()) { + String pathTemplate = entry.getKey(); + String mappedMethod = entry.getValue(); + if (isPathMatch(requestPath, pathTemplate)) { + GrpcTranscodingMetadata metadata = metadataHandlers.get(mappedMethod); + if (metadata != null) { + if (metadata.getHttpMethodName().equals(httpRequest.method().name())) { + handleWithMappedMethod(httpRequest, pathTemplate, mappedMethod); + return; + } + } + } + } + + httpRequest.response().setStatusCode(404).end(); + } + + private void handleWithMappedMethod(HttpServerRequest httpRequest, String pathTemplate, String mappedMethod) { + GrpcMethodCall methodCall = new GrpcMethodCall("/" + mappedMethod); + String fmn = methodCall.fullMethodName(); + MethodCallHandler method = methodCallHandlers.get(fmn); + + if (method != null) { + handle(pathTemplate, method, httpRequest, methodCall); + } else { + httpRequest.response().setStatusCode(500).end(); + } + } + + private void handle(String pathTemplate, MethodCallHandler method, HttpServerRequest httpRequest, + GrpcMethodCall methodCall) { + Map pathParams = extractPathParams(pathTemplate, httpRequest.path()); + Map queryParameters = new HashMap<>(httpRequest.params().entries().stream() + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), HashMap::putAll)); + + GrpcTranscodingRequest grpcRequest = new GrpcTranscodingRequest<>(httpRequest, method.messageDecoder, + method.messageEncoder, methodCall, pathParams, queryParameters); + grpcRequest.init(); + method.handle(grpcRequest); + } + + public GrpcServer callHandler(Handler> handler) { + this.requestHandler = handler; + return this; + } + + @Override + public GrpcServer callHandler(MethodDescriptor methodDesc, + Handler> handler) { + if (handler != null) { + MethodDescriptor.Marshaller reqMarshaller = findRequestMarshaller(methodDesc.getFullMethodName()); + MethodDescriptor.Marshaller respMarshaller = findResponseMarshaller(methodDesc.getFullMethodName()); + + methodCallHandlers.put(methodDesc.getFullMethodName(), + new GrpcTranscodingServer.MethodCallHandler<>(methodDesc, + GrpcMessageDecoder.unmarshaller(reqMarshaller), + GrpcMessageEncoder.marshaller(respMarshaller), handler)); + } else { + methodCallHandlers.remove(methodDesc.getFullMethodName()); + } + return this; + } + + public void addMethodMapping(String path, String fullMethodName) { + methodMapping.put(path, fullMethodName); + } + + public void addMetadataHandler(String fullMethodName, GrpcTranscodingMetadata metadata) { + metadataHandlers.put(fullMethodName, metadata); + } + + @SuppressWarnings("unchecked") + public MethodDescriptor.Marshaller findRequestMarshaller(String fullMethodName) { + GrpcTranscodingMetadata metadata = metadataHandlers.get(fullMethodName); + return (MethodDescriptor.Marshaller) metadata.getRequestMarshaller(); + } + + @SuppressWarnings("unchecked") + public MethodDescriptor.Marshaller findResponseMarshaller(String fullMethodName) { + GrpcTranscodingMetadata metadata = metadataHandlers.get(fullMethodName); + return (MethodDescriptor.Marshaller) metadata.getResponseMarshaller(); + } + + private boolean isPathMatch(String requestPath, String pathTemplate) { + String[] requestParts = requestPath.split("/"); + String[] templateParts = parsePath(pathTemplate).split("/"); // Use the enhanced parsePath + + if (requestParts.length != templateParts.length) { + return false; // Mismatch in number of segments + } + + for (int i = 0; i < requestParts.length; i++) { + String requestPart = requestParts[i]; + String templatePart = templateParts[i]; + + if (templatePart.startsWith("{") && templatePart.endsWith("}")) { + // It's a variable, any non-empty segment would match + if (requestPart.isEmpty()) { + return false; + } + } else { + // It's a literal segment + if (!requestPart.equals(templatePart)) { + return false; + } + } + } + + return true; // All segments matched + } + + public String parsePath(String path) { + Map extractedParams = extractPathParams(path); + StringBuilder parsedPathBuilder = new StringBuilder(); + + for (Map.Entry entry : extractedParams.entrySet()) { + parsedPathBuilder.append("/").append(entry.getValue()); + } + + return parsedPathBuilder.toString(); + } + + public Map extractPathParams(String pathTemplate, String httpPath) { + Map extractedParams = new HashMap<>(); + + String[] pathParts = httpPath.split("/"); + String[] templateParts = pathTemplate.split("/"); + + for (int i = 0; i < pathParts.length; i++) { + String pathPart = pathParts[i]; + String templatePart = templateParts[i]; + + if (templatePart.startsWith("{") && templatePart.endsWith("}")) { + String paramName = templatePart.substring(1, templatePart.length() - 1); + extractedParams.put(paramName, pathPart); + } + } + + return extractedParams; + } + + public Map extractPathParams(String path) { + // Regular expressions for template segments + String singleSegmentVarPattern = "\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}"; + String multiSegmentVarPattern = "\\{\\+([a-zA-Z_][a-zA-Z0-9_]*)\\}"; + + Map extractedParams = new LinkedHashMap<>(); + + String[] parts = path.split("/"); + + for (String part : parts) { + if (part.isEmpty()) { + continue; // Skip empty segments + } + + Matcher singleMatcher = Pattern.compile(singleSegmentVarPattern).matcher(part); + Matcher multiMatcher = Pattern.compile(multiSegmentVarPattern).matcher(part); + + if (singleMatcher.matches()) { + String paramName = singleMatcher.group(1); + extractedParams.put(paramName, "{" + paramName + "}"); + } else if (multiMatcher.matches()) { + String paramName = multiMatcher.group(1); + extractedParams.put(paramName, "{+" + paramName + "}"); + } else { + extractedParams.put(part, part); + } + } + + return extractedParams; + } + + private static class MethodCallHandler implements Handler> { + + final MethodDescriptor def; + final GrpcMessageDecoder messageDecoder; + final GrpcMessageEncoder messageEncoder; + final Handler> handler; + + MethodCallHandler(MethodDescriptor def, GrpcMessageDecoder messageDecoder, + GrpcMessageEncoder messageEncoder, Handler> handler) { + this.def = def; + this.messageDecoder = messageDecoder; + this.messageEncoder = messageEncoder; + this.handler = handler; + } + + @Override + public void handle(GrpcServerRequest grpcRequest) { + handler.handle(grpcRequest); + } + } +} diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingWriteStreamAdapter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingWriteStreamAdapter.java similarity index 84% rename from extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingWriteStreamAdapter.java rename to extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingWriteStreamAdapter.java index d14dc5d8c5c99..0f2f0327ab5b6 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingWriteStreamAdapter.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingWriteStreamAdapter.java @@ -1,9 +1,9 @@ -package io.quarkus.grpc.runtime.transcoding; +package io.quarkus.grpc.transcoding; import io.vertx.grpc.common.GrpcMessageEncoder; import io.vertx.grpc.common.GrpcWriteStream; -public class TranscodingWriteStreamAdapter { +public class GrpcTranscodingWriteStreamAdapter { private GrpcWriteStream stream; private boolean ready; @@ -15,7 +15,7 @@ public class TranscodingWriteStreamAdapter { protected void handleReady() { } - public final void init(GrpcWriteStream stream, TranscodingMessageEncoder encoder) { + public final void init(GrpcWriteStream stream, GrpcTranscodingMessageEncoder encoder) { synchronized (this) { this.stream = stream; this.encoder = encoder; diff --git a/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java b/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java index fba8dab932d86..36e2e7baa583c 100644 --- a/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java +++ b/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java @@ -52,7 +52,7 @@ public void testResourceLookup() { public void testNestedResourceLookup() { given() .contentType(ContentType.JSON) - .when().put("/v1/resources/update/1234") + .when().get("/v1/resources/update/1234") .then() .statusCode(200) .body("message", is("Greeting with id '1234' updated with nested resource details: name='update'"));