getServices() {
+ return services;
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingHttpUtils.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingHttpUtils.java
new file mode 100644
index 0000000000000..6499fe808ad22
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingHttpUtils.java
@@ -0,0 +1,84 @@
+package io.quarkus.grpc.transcoding;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The `GrpcTranscodingHttpUtils` class provides utility functions for path handling
+ * and parameter extraction during the gRPC message transcoding process. Its key
+ * functions include:
+ *
+ * Checking if a request path matches a given gRPC path template.
+ * Extracting path parameters from both gRPC path templates and concrete HTTP paths.
+ */
+public class GrpcTranscodingHttpUtils {
+
+ /**
+ * Determines if a given HTTP request path conforms to a specified gRPC path template.
+ *
+ * @param requestPath The actual HTTP request path to be checked.
+ * @param pathTemplate The gRPC path template defining the expected structure.
+ * @return `true` if the paths match, `false` otherwise.
+ */
+ public static boolean isPathMatch(String requestPath, String pathTemplate) {
+ int pathIndex = 0;
+ int templateIndex = 0;
+
+ while (pathIndex < requestPath.length() && templateIndex < pathTemplate.length()) {
+ int pathEnd = requestPath.indexOf('/', pathIndex);
+ int templateEnd = pathTemplate.indexOf('/', templateIndex);
+
+ // Extract the current segment from both paths
+ String requestPart = pathEnd == -1 ? requestPath.substring(pathIndex) : requestPath.substring(pathIndex, pathEnd);
+ String templatePart = templateEnd == -1 ? pathTemplate.substring(templateIndex)
+ : pathTemplate.substring(templateIndex, templateEnd);
+
+ // Check if the template part is a variable segment
+ if (templatePart.startsWith("{") && templatePart.endsWith("}")) {
+ if (requestPart.isEmpty()) {
+ return false;
+ }
+ // Skip to the end of the next segment
+ pathIndex = pathEnd != -1 ? pathEnd + 1 : requestPath.length();
+ templateIndex = templateEnd != -1 ? templateEnd + 1 : pathTemplate.length();
+ } else {
+ if (!requestPart.equals(templatePart)) {
+ return false;
+ }
+
+ // Skip to the end of the next segment
+ pathIndex = pathEnd != -1 ? pathEnd + 1 : requestPath.length();
+ templateIndex = templateEnd != -1 ? templateEnd + 1 : pathTemplate.length();
+ }
+ }
+
+ // Ensure both paths have been fully consumed
+ return pathIndex == requestPath.length() && templateIndex == pathTemplate.length();
+ }
+
+ /**
+ * Extracts path parameters from a gRPC path template and an associated HTTP path.
+ *
+ * @param pathTemplate The gRPC path template defining the parameter structure.
+ * @param httpPath The actual HTTP path from which to extract the parameter values.
+ * @return A `Map` containing the extracted parameter names and their corresponding values.
+ */
+ public static 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;
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java
new file mode 100644
index 0000000000000..c61dffb50ff59
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageDecoder.java
@@ -0,0 +1,27 @@
+package io.quarkus.grpc.transcoding;
+
+import java.io.ByteArrayInputStream;
+
+import io.grpc.MethodDescriptor;
+import io.vertx.grpc.common.GrpcMessage;
+import io.vertx.grpc.common.GrpcMessageDecoder;
+
+/*
+ * A message decoder that uses a {@link MethodDescriptor.Marshaller} to decode the message payload.
+ *
+ * @param The type of the message payload.
+ * @see io.vertx.grpc.common.impl.GrpcMessageDecoderImpl for the original implementation
+ */
+public class GrpcTranscodingMessageDecoder implements GrpcMessageDecoder {
+
+ private final MethodDescriptor.Marshaller marshaller;
+
+ public GrpcTranscodingMessageDecoder(MethodDescriptor.Marshaller marshaller) {
+ this.marshaller = marshaller;
+ }
+
+ @Override
+ public T decode(GrpcMessage msg) {
+ return marshaller.parse(new ByteArrayInputStream(msg.payload().getBytes()));
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java
new file mode 100644
index 0000000000000..4a2e39a03aa41
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageEncoder.java
@@ -0,0 +1,51 @@
+package io.quarkus.grpc.transcoding;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import io.grpc.MethodDescriptor;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.grpc.common.GrpcMessage;
+import io.vertx.grpc.common.GrpcMessageEncoder;
+
+/*
+ * A message encoder that uses a {@link MethodDescriptor.Marshaller} to encode the message payload.
+ *
+ * @param The type of the message payload.
+ * @see io.vertx.grpc.common.impl.GrpcMessageEncoderImpl for the original implementation
+ */
+public class GrpcTranscodingMessageEncoder implements GrpcMessageEncoder {
+
+ private final MethodDescriptor.Marshaller marshaller;
+
+ public GrpcTranscodingMessageEncoder(MethodDescriptor.Marshaller marshaller) {
+ this.marshaller = marshaller;
+ }
+
+ @Override
+ public GrpcMessage encode(T msg) {
+ return new GrpcMessage() {
+ private Buffer encoded;
+
+ @Override
+ public String encoding() {
+ return "identity";
+ }
+
+ @Override
+ public Buffer payload() {
+ if (encoded == null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ marshaller.stream(msg).transferTo(baos);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ byte[] bytes = baos.toByteArray();
+ encoded = Buffer.buffer(bytes);
+ }
+ return encoded;
+ }
+ };
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageWriter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageWriter.java
new file mode 100644
index 0000000000000..303c5eebdb025
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageWriter.java
@@ -0,0 +1,74 @@
+package io.quarkus.grpc.transcoding;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.Json;
+
+/**
+ * The `GrpcTranscodingMessageWriter` class assists with the manipulation of gRPC
+ * message payloads during the transcoding process. Its responsibilities include:
+ *
+ * Merging existing JSON payloads, path parameters, and query parameters into a
+ * unified map representation.
+ * Providing the logic for inserting nested parameters within the generated map.
+ */
+public class GrpcTranscodingMessageWriter {
+
+ private final static String SEPARATOR = "\\.";
+
+ /**
+ * Merges path parameters, query parameters, and an optional existing JSON payload
+ * into a single `Map` object. This method provides a centralized way to combine
+ * parameters during gRPC message transcoding.
+ *
+ * @param pathParams A map containing path parameters extracted from the request.
+ * @param queryParams A map containing query parameters extracted from the request.
+ * @param existingPayload An optional Vert.x `Buffer` containing an existing JSON payload.
+ * @return A `Map` representing the merged parameters.
+ */
+ public static Map mergeParameters(Map pathParams, Map queryParams,
+ Buffer existingPayload) {
+ Map allParams = new HashMap<>();
+
+ if (existingPayload != null && existingPayload.getBytes().length > 0) {
+ String existingPayloadJson = new String(existingPayload.getBytes());
+ allParams = new HashMap(Json.decodeValue(existingPayloadJson, Map.class));
+ }
+
+ for (Map.Entry entry : pathParams.entrySet()) {
+ insertNestedParam(allParams, entry.getKey(), entry.getValue());
+ }
+
+ for (Map.Entry entry : queryParams.entrySet()) {
+ insertNestedParam(allParams, entry.getKey(), entry.getValue());
+ }
+
+ return allParams;
+ }
+
+ /**
+ * Inserts a key-value pair into a nested structure within a `Map`. This method supports
+ * the creation of hierarchical parameter structures during the transcoding process.
+ * Key components are separated by periods ('.').
+ *
+ * @param paramsMap The `Map` object where the nested parameter will be inserted.
+ * @param key The parameter key, potentially containing periods for nested structures.
+ * @param value The parameter value to be inserted.
+ */
+ public static void insertNestedParam(Map paramsMap, String key, String value) {
+ String[] pathComponents = key.split(SEPARATOR);
+
+ Map currentLevel = paramsMap;
+ for (int i = 0; i < pathComponents.length - 1; i++) {
+ String component = pathComponents[i];
+ if (!currentLevel.containsKey(component)) {
+ currentLevel.put(component, new HashMap<>());
+ }
+ currentLevel = (Map) currentLevel.get(component);
+ }
+
+ currentLevel.put(pathComponents[pathComponents.length - 1], value);
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java
new file mode 100644
index 0000000000000..46a1fec9d8111
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMetadata.java
@@ -0,0 +1,64 @@
+package io.quarkus.grpc.transcoding;
+
+import com.google.protobuf.Message;
+
+import io.grpc.MethodDescriptor;
+import io.quarkus.grpc.GrpcTranscodingMarshaller;
+import io.vertx.core.http.HttpMethod;
+
+/**
+ * A metadata class that holds the transcoding information for a gRPC method.
+ *
+ * @param The type of the request message.
+ * @param The type of the response message.
+ */
+public class GrpcTranscodingMetadata {
+
+ private final HttpMethod httpMethod;
+ private final String uriTemplate;
+ private final String grpcMethodName;
+ private final GrpcTranscodingMarshaller requestMarshaller;
+ private final GrpcTranscodingMarshaller responseMarshaller;
+ private final MethodDescriptor methodDescriptor;
+
+ public GrpcTranscodingMetadata(HttpMethod httpMethod,
+ String uriTemplate,
+ String grpcMethodName,
+ GrpcTranscodingMarshaller requestMarshaller,
+ GrpcTranscodingMarshaller responseMarshaller, MethodDescriptor methodDescriptor) {
+ this.httpMethod = httpMethod;
+ this.uriTemplate = uriTemplate;
+ this.grpcMethodName = grpcMethodName;
+ this.requestMarshaller = requestMarshaller;
+ this.responseMarshaller = responseMarshaller;
+ this.methodDescriptor = methodDescriptor;
+ }
+
+ public HttpMethod getHttpMethod() {
+ return httpMethod;
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public String getGrpcMethodName() {
+ return grpcMethodName;
+ }
+
+ public GrpcTranscodingMarshaller getRequestMarshaller() {
+ return requestMarshaller;
+ }
+
+ public GrpcTranscodingMarshaller getResponseMarshaller() {
+ return responseMarshaller;
+ }
+
+ public MethodDescriptor getMethodDescriptor() {
+ return methodDescriptor;
+ }
+
+ public String getUriAsRegex() {
+ return uriTemplate.replaceAll("\\{[^/]+\\}", "[^/]+");
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java
new file mode 100644
index 0000000000000..3fbaf90a0d11e
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMethod.java
@@ -0,0 +1,29 @@
+package io.quarkus.grpc.transcoding;
+
+/**
+ * A metadata class that holds the transcoding information for a gRPC method.
+ */
+public final class GrpcTranscodingMethod {
+
+ private final String grpcMethodName;
+ private final String httpMethodName;
+ private final String uriTemplate;
+
+ public GrpcTranscodingMethod(String grpcMethodName, String httpMethodName, String uriTemplate) {
+ this.grpcMethodName = grpcMethodName;
+ this.httpMethodName = httpMethodName;
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getGrpcMethodName() {
+ return grpcMethodName;
+ }
+
+ public String getHttpMethodName() {
+ return httpMethodName;
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java
new file mode 100644
index 0000000000000..74efd3db76905
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingReadStreamAdapter.java
@@ -0,0 +1,57 @@
+package io.quarkus.grpc.transcoding;
+
+import io.vertx.grpc.common.GrpcReadStream;
+
+/**
+ * Adapter for {@link GrpcReadStream} to handle message and close events.
+ *
+ * @param The type of the message payload.
+ * @see io.vertx.grpc.common.impl.ReadStreamAdapter for the original implementation
+ */
+public class GrpcTranscodingReadStreamAdapter {
+
+ private GrpcReadStream stream;
+ private int request = 0;
+
+ /**
+ * Init the adapter with the stream.
+ */
+ public final void init(GrpcReadStream stream, GrpcTranscodingMessageDecoder decoder) {
+ stream.messageHandler(msg -> {
+ handleMessage(decoder.decode(msg));
+ });
+ stream.endHandler(v -> {
+ handleClose();
+ });
+ this.stream = stream;
+ stream.pause();
+ if (request > 0) {
+ stream.fetch(request);
+ }
+ }
+
+ /**
+ * Override this to handle close event
+ */
+ protected void handleClose() {
+
+ }
+
+ /**
+ * Override this to handle message event
+ */
+ protected void handleMessage(T msg) {
+
+ }
+
+ /**
+ * Request {@code num} messages
+ */
+ public final void request(int num) {
+ if (stream != null) {
+ stream.fetch(num);
+ } else {
+ request += num;
+ }
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java
new file mode 100644
index 0000000000000..732add6b48bb1
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRecorder.java
@@ -0,0 +1,191 @@
+package io.quarkus.grpc.transcoding;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import io.vertx.core.http.HttpMethod;
+import jakarta.enterprise.inject.Instance;
+
+import org.jboss.logging.Logger;
+
+import com.google.protobuf.Message;
+
+import io.grpc.BindableService;
+import io.grpc.MethodDescriptor;
+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.auth.GrpcSecurityInterceptor;
+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.ext.web.Route;
+import io.vertx.ext.web.Router;
+
+@Recorder
+public class GrpcTranscodingRecorder {
+
+ private static final Logger LOGGER = Logger.getLogger(GrpcTranscodingRecorder.class.getName());
+ private static final String CONTENT_TYPE = "application/json";
+
+ public RuntimeValue initializeMarshallingServer(RuntimeValue vertxSupplier,
+ RuntimeValue routerSupplier,
+ ShutdownContext shutdown, Map> httpMethods,
+ boolean securityPresent) {
+ GrpcTranscodingServer transcodingServer = new GrpcTranscodingServer(vertxSupplier.getValue());
+
+ GrpcContainer grpcContainer = Arc.container().instance(GrpcContainer.class).get();
+ GrpcTranscodingContainer container = Arc.container().instance(GrpcTranscodingContainer.class).get();
+
+ if (grpcContainer == null) {
+ throw new IllegalStateException("GrpcContainer not found");
+ }
+
+ if (container == null) {
+ throw new IllegalStateException("GrpcTranscodingContainer not found");
+ }
+
+ List grpcServices = collectServiceDefinitions(grpcContainer.getServices());
+ List transcodingServices = collectTranscodingServices(container.getServices());
+
+ List> mappedMethods = new ArrayList<>();
+
+ LOGGER.info("Initializing gRPC transcoding services");
+ for (GrpcTranscoding transcodingService : transcodingServices) {
+ GrpcServerRecorder.GrpcServiceDefinition grpcService = findGrpcService(grpcServices, transcodingService);
+ List transcodingMethods = findTranscodingMethods(httpMethods,
+ transcodingService.getGrpcServiceName());
+
+ for (ServerMethodDefinition, ?> serviceDefinition : grpcService.definition.getMethods()) {
+ MethodDescriptor methodDescriptor = (MethodDescriptor) serviceDefinition
+ .getMethodDescriptor();
+ GrpcTranscodingMethod transcodingMethod = findTranscodingMethod(transcodingMethods, methodDescriptor);
+
+ String path = transcodingMethod.getUriTemplate();
+ GrpcTranscodingMetadata, ?> metadata = createMetadata(transcodingMethod, methodDescriptor,
+ transcodingService);
+
+ transcodingServer.addMethodMapping(path, methodDescriptor.getFullMethodName());
+ transcodingServer.addMetadataHandler(methodDescriptor.getFullMethodName(), metadata);
+
+ mappedMethods.add(serviceDefinition);
+
+ Route route = routerSupplier.getValue().route()
+ .pathRegex(metadata.getUriAsRegex())
+ .method(metadata.getHttpMethod())
+ .consumes(CONTENT_TYPE)
+ .handler(ctx -> {
+ if (securityPresent) {
+ GrpcSecurityInterceptor.propagateSecurityIdentityWithDuplicatedCtx(ctx);
+ }
+ if (!Context.isOnEventLoopThread()) {
+ Context capturedVertxContext = Vertx.currentContext();
+ if (capturedVertxContext != null) {
+ capturedVertxContext.runOnContext(new Handler() {
+ @Override
+ public void handle(Void unused) {
+ transcodingServer.handle(ctx.request());
+ }
+ });
+ return;
+ }
+ }
+
+ transcodingServer.handle(ctx.request());
+ });
+
+ shutdown.addShutdownTask(route::remove);
+ }
+ }
+
+ GrpcTranscodingBridge bridge = new GrpcTranscodingBridge(mappedMethods);
+ bridge.bind(transcodingServer);
+
+ return new RuntimeValue<>(transcodingServer);
+ }
+
+ private GrpcTranscodingMetadata createMetadata(
+ GrpcTranscodingMethod transcodingMethod, MethodDescriptor methodDescriptor,
+ 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);
+ HttpMethod httpMethod = HttpMethod.valueOf(transcodingMethod.getHttpMethodName().toUpperCase());
+
+ return new GrpcTranscodingMetadata<>(
+ httpMethod,
+ transcodingMethod.getUriTemplate(),
+ fullMethodName,
+ descriptor.getRequestMarshaller(),
+ descriptor.getResponseMarshaller(),
+ methodDescriptor);
+ }
+
+ private List findTranscodingMethods(Map> transcodingMethods,
+ String grpcServiceName) {
+ List methods = new ArrayList<>();
+ for (Map.Entry> entry : transcodingMethods.entrySet()) {
+ if (entry.getKey().startsWith(grpcServiceName)) {
+ methods.addAll(entry.getValue());
+ }
+ }
+
+ return methods;
+ }
+
+ private GrpcTranscodingMethod findTranscodingMethod(List transcodingMethods,
+ MethodDescriptor, ?> methodDescriptor) {
+ String fullMethodName = methodDescriptor.getFullMethodName();
+ fullMethodName = fullMethodName.substring(fullMethodName.lastIndexOf("/") + 1);
+ fullMethodName = Character.toLowerCase(fullMethodName.charAt(0)) + fullMethodName.substring(1);
+
+ for (GrpcTranscodingMethod transcodingMethod : transcodingMethods) {
+ if (transcodingMethod.getGrpcMethodName().startsWith(fullMethodName)) {
+ return transcodingMethod;
+ }
+ }
+
+ throw new IllegalStateException("Transcoding method not found for " + fullMethodName);
+ }
+
+ private List collectServiceDefinitions(
+ Instance services) {
+ List definitions = new ArrayList<>();
+ for (BindableService service : services) {
+ ServerServiceDefinition definition = service.bindService();
+ definitions.add(new GrpcServerRecorder.GrpcServiceDefinition(service, definition));
+ }
+
+ return definitions;
+ }
+
+ private List collectTranscodingServices(Instance services) {
+ List transcodingServices = new ArrayList<>();
+ for (GrpcTranscoding service : services) {
+ transcodingServices.add(service);
+ }
+
+ return transcodingServices;
+ }
+
+ private GrpcServerRecorder.GrpcServiceDefinition findGrpcService(
+ List grpcServices, GrpcTranscoding transcodingService) {
+ for (GrpcServerRecorder.GrpcServiceDefinition grpcService : grpcServices) {
+ if (grpcService.getImplementationClassName().startsWith(transcodingService.getGrpcServiceName())) {
+ return grpcService;
+ }
+ }
+
+ throw new IllegalStateException("gRPC service not found for " + transcodingService.getGrpcServiceName());
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java
new file mode 100644
index 0000000000000..40871aee6eaff
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java
@@ -0,0 +1,308 @@
+package io.quarkus.grpc.transcoding;
+
+import static io.vertx.grpc.common.GrpcError.mapHttp2ErrorCode;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.Collector;
+
+import io.vertx.codegen.annotations.Nullable;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.MultiMap;
+import io.vertx.core.Promise;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpConnection;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.http.StreamResetException;
+import io.vertx.core.http.impl.HttpServerRequestInternal;
+import io.vertx.core.impl.ContextInternal;
+import io.vertx.core.impl.future.PromiseInternal;
+import io.vertx.core.json.DecodeException;
+import io.vertx.core.json.Json;
+import io.vertx.core.streams.ReadStream;
+import io.vertx.core.streams.impl.InboundBuffer;
+import io.vertx.grpc.common.CodecException;
+import io.vertx.grpc.common.GrpcError;
+import io.vertx.grpc.common.GrpcMessage;
+import io.vertx.grpc.common.GrpcMessageDecoder;
+import io.vertx.grpc.common.GrpcMessageEncoder;
+import io.vertx.grpc.common.GrpcReadStream;
+import io.vertx.grpc.common.ServiceName;
+import io.vertx.grpc.common.impl.GrpcMethodCall;
+import io.vertx.grpc.server.GrpcServerRequest;
+import io.vertx.grpc.server.GrpcServerResponse;
+
+/**
+ * A gRPC transcoding request that maps HTTP requests to gRPC methods.
+ *
+ * @param The type of the request message.
+ * @param The type of the response message.
+ * @see io.vertx.grpc.server.impl.GrpcServerRequestImpl for the original implementation
+ */
+public class GrpcTranscodingRequest implements GrpcReadStream, Handler, GrpcServerRequest {
+
+ static final GrpcMessage END_SENTINEL = new GrpcMessage() {
+ @Override
+ public String encoding() {
+ return null;
+ }
+
+ @Override
+ public Buffer payload() {
+ return null;
+ }
+ };
+
+ private final HttpServerRequest httpRequest;
+ private final GrpcServerResponse response;
+ private GrpcMethodCall methodCall;
+ protected final ContextInternal context;
+ private final ReadStream stream;
+ private final InboundBuffer queue;
+ private Buffer buffer;
+ private Handler errorHandler;
+ private Handler exceptionHandler;
+ private Handler messageHandler;
+ private Handler endHandler;
+ private GrpcMessage last;
+ private final GrpcMessageDecoder messageDecoder;
+ private final Promise end;
+ private final Map pathParams;
+ private final Map queryParams;
+
+ public GrpcTranscodingRequest(HttpServerRequest httpRequest,
+ GrpcMessageDecoder messageDecoder,
+ GrpcMessageEncoder messageEncoder,
+ GrpcMethodCall methodCall,
+ Map pathParams,
+ Map queryParams) {
+ this.httpRequest = httpRequest;
+ this.response = new GrpcTranscodingResponse<>(this, httpRequest.response(), messageEncoder);
+ this.methodCall = methodCall;
+ this.pathParams = pathParams;
+ this.queryParams = queryParams;
+
+ this.context = (ContextInternal) ((HttpServerRequestInternal) httpRequest).context();
+ this.stream = httpRequest;
+ this.queue = new InboundBuffer<>(context);
+ this.messageDecoder = messageDecoder;
+ this.end = context.promise();
+ }
+
+ public void init() {
+ stream.handler(this);
+ stream.endHandler(v -> queue.write(END_SENTINEL));
+ stream.exceptionHandler(err -> {
+ if (err instanceof StreamResetException) {
+ handleReset(((StreamResetException) err).getCode());
+ } else {
+ handleException(err);
+ }
+ });
+ queue.drainHandler(v -> stream.resume());
+ queue.handler(msg -> {
+ if (msg == END_SENTINEL) {
+ if (httpRequest.bytesRead() == 0) {
+ handleMessage(mergeParametersIntoMessage(msg));
+ }
+
+ handleEnd();
+ } else {
+ handleMessage(mergeParametersIntoMessage(msg));
+ }
+ });
+ }
+
+ private GrpcMessage mergeParametersIntoMessage(GrpcMessage msg) {
+ try {
+ Map allParams = GrpcTranscodingMessageWriter.mergeParameters(
+ pathParams,
+ queryParams,
+ msg.payload());
+
+ byte[] jsonPayload = Json.encode(allParams).getBytes();
+ return GrpcMessage.message(msg.encoding(), Buffer.buffer(jsonPayload));
+ } catch (DecodeException e) {
+ // Invalid JSON payload
+ httpRequest.response().setStatusCode(422).end();
+ return null;
+ }
+ }
+
+ protected Req decodeMessage(GrpcMessage msg) throws CodecException {
+ return messageDecoder.decode(msg);
+ }
+
+ @Override
+ public void handle(Buffer chunk) {
+ if (buffer == null) {
+ buffer = chunk;
+ } else {
+ buffer.appendBuffer(chunk);
+ }
+
+ Buffer payload = buffer.slice();
+ GrpcMessage message = GrpcMessage.message("identity", payload);
+ boolean pause = !queue.write(message);
+
+ if (pause) {
+ stream.pause();
+ }
+
+ buffer = null;
+ }
+
+ protected void handleReset(long code) {
+ Handler handler = errorHandler;
+ if (handler != null) {
+ GrpcError error = mapHttp2ErrorCode(code);
+ if (error != null) {
+ handler.handle(error);
+ }
+ }
+ }
+
+ protected void handleException(Throwable err) {
+ end.tryFail(err);
+ Handler handler = exceptionHandler;
+ if (handler != null) {
+ handler.handle(err);
+ }
+ }
+
+ protected void handleEnd() {
+ end.tryComplete();
+ Handler handler = endHandler;
+ if (handler != null) {
+ handler.handle(null);
+ }
+ }
+
+ protected void handleMessage(GrpcMessage msg) {
+ last = msg;
+ Handler handler = messageHandler;
+ if (handler != null) {
+ handler.handle(msg);
+ }
+ }
+
+ @Override
+ public ServiceName serviceName() {
+ return methodCall.serviceName();
+ }
+
+ @Override
+ public String methodName() {
+ return methodCall.methodName();
+ }
+
+ @Override
+ public String fullMethodName() {
+ return methodCall.fullMethodName();
+ }
+
+ @Override
+ public GrpcServerResponse response() {
+ return response;
+ }
+
+ @Override
+ public MultiMap headers() {
+ return httpRequest.headers();
+ }
+
+ @Override
+ public String encoding() {
+ return httpRequest.getHeader("grpc-encoding");
+ }
+
+ @Override
+ public GrpcServerRequest messageHandler(@Nullable Handler handler) {
+ messageHandler = handler;
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest errorHandler(@Nullable Handler handler) {
+ errorHandler = handler;
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest exceptionHandler(@Nullable Handler handler) {
+ exceptionHandler = handler;
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest handler(@Nullable Handler handler) {
+ if (handler != null) {
+ return messageHandler(msg -> {
+ Req decoded;
+ try {
+ decoded = decodeMessage(msg);
+ } catch (CodecException e) {
+ response.cancel();
+ return;
+ }
+ handler.handle(decoded);
+ });
+ } else {
+ return messageHandler(null);
+ }
+ }
+
+ @Override
+ public GrpcServerRequest pause() {
+ queue.pause();
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest resume() {
+ queue.resume();
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest fetch(long amount) {
+ queue.fetch(amount);
+ return this;
+ }
+
+ @Override
+ public GrpcServerRequest endHandler(@Nullable Handler handler) {
+ this.endHandler = handler;
+ return this;
+ }
+
+ @Override
+ public Future last() {
+ return end().map(v -> decodeMessage(last));
+ }
+
+ @Override
+ public Future end() {
+ return end.future();
+ }
+
+ @Override
+ public Future collecting(Collector collector) {
+ PromiseInternal promise = context.promise();
+ C cumulation = collector.supplier().get();
+ BiConsumer accumulator = collector.accumulator();
+ handler(elt -> accumulator.accept(cumulation, elt));
+ endHandler(v -> {
+ R result = collector.finisher().apply(cumulation);
+ promise.tryComplete(result);
+ });
+ exceptionHandler(promise::tryFail);
+ return promise.future();
+ }
+
+ @Override
+ public HttpConnection connection() {
+ return httpRequest.connection();
+ }
+}
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java
new file mode 100644
index 0000000000000..abe58cee39cbc
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingResponse.java
@@ -0,0 +1,248 @@
+package io.quarkus.grpc.transcoding;
+
+import java.util.Map;
+import java.util.Objects;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.CompositeByteBuf;
+import io.netty.buffer.Unpooled;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.MultiMap;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.grpc.common.GrpcError;
+import io.vertx.grpc.common.GrpcMessage;
+import io.vertx.grpc.common.GrpcMessageEncoder;
+import io.vertx.grpc.common.GrpcStatus;
+import io.vertx.grpc.common.impl.Utils;
+import io.vertx.grpc.server.GrpcServerResponse;
+
+/**
+ * A gRPC transcoding response that maps gRPC responses to HTTP responses.
+ *
+ * @param The type of the request message.
+ * @param The type of the response message.
+ * @see io.vertx.grpc.server.impl.GrpcServerResponseImpl for the original implementation
+ */
+public class GrpcTranscodingResponse implements GrpcServerResponse {
+
+ private final GrpcTranscodingRequest request;
+ private final HttpServerResponse httpResponse;
+ private final GrpcMessageEncoder encoder;
+ private GrpcStatus status = GrpcStatus.OK;
+ private String statusMessage;
+ private boolean headersSent;
+ private boolean trailersSent;
+ private boolean cancelled;
+ private MultiMap headers, trailers;
+
+ public GrpcTranscodingResponse(GrpcTranscodingRequest request, HttpServerResponse httpResponse,
+ GrpcMessageEncoder encoder) {
+ this.request = request;
+ this.httpResponse = httpResponse;
+ this.encoder = encoder;
+ }
+
+ public GrpcServerResponse status(GrpcStatus status) {
+ Objects.requireNonNull(status);
+ this.status = status;
+ return this;
+ }
+
+ @Override
+ public GrpcServerResponse statusMessage(String msg) {
+ this.statusMessage = msg;
+ return this;
+ }
+
+ // We don't need to implement this method
+ public GrpcServerResponse encoding(String encoding) {
+ // ????
+ return this;
+ }
+
+ @Override
+ public MultiMap headers() {
+ if (headersSent) {
+ throw new IllegalStateException("Headers already sent");
+ }
+ if (headers == null) {
+ headers = MultiMap.caseInsensitiveMultiMap();
+ }
+ return headers;
+ }
+
+ @Override
+ public MultiMap trailers() {
+ if (trailersSent) {
+ throw new IllegalStateException("Trailers already sent");
+ }
+ if (trailers == null) {
+ trailers = MultiMap.caseInsensitiveMultiMap();
+ }
+ return trailers;
+ }
+
+ @Override
+ public GrpcTranscodingResponse exceptionHandler(Handler handler) {
+ httpResponse.exceptionHandler(handler);
+ return this;
+ }
+
+ @Override
+ public Future write(Resp message) {
+ return writeMessage(encoder.encode(message));
+ }
+
+ @Override
+ public Future end(Resp message) {
+ return endMessage(encoder.encode(message));
+ }
+
+ @Override
+ public Future writeMessage(GrpcMessage data) {
+ return writeMessage(data, false);
+ }
+
+ @Override
+ public Future endMessage(GrpcMessage message) {
+ return writeMessage(message, true);
+ }
+
+ public Future end() {
+ return writeMessage(null, true);
+ }
+
+ @Override
+ public GrpcServerResponse setWriteQueueMaxSize(int maxSize) {
+ httpResponse.setWriteQueueMaxSize(maxSize);
+ return this;
+ }
+
+ @Override
+ public boolean writeQueueFull() {
+ return httpResponse.writeQueueFull();
+ }
+
+ @Override
+ public GrpcServerResponse drainHandler(Handler handler) {
+ httpResponse.drainHandler(handler);
+ return this;
+ }
+
+ @Override
+ public void cancel() {
+ if (cancelled) {
+ return;
+ }
+ cancelled = true;
+ Future fut = request.end();
+ boolean requestEnded;
+ if (fut.failed()) {
+ return;
+ } else {
+ requestEnded = fut.succeeded();
+ }
+ if (!requestEnded || !trailersSent) {
+ httpResponse.reset(GrpcError.CANCELLED.http2ResetCode);
+ }
+ }
+
+ private Future writeMessage(GrpcMessage message, boolean end) {
+ if (cancelled) {
+ throw new IllegalStateException("The stream has been cancelled");
+ }
+ if (trailersSent) {
+ throw new IllegalStateException("The stream has been closed");
+ }
+
+ if (message == null && !end) {
+ throw new IllegalStateException();
+ }
+
+ boolean trailersOnly = status != GrpcStatus.OK && !headersSent && end;
+
+ MultiMap responseHeaders = httpResponse.headers();
+ if (!headersSent) {
+ headersSent = true;
+ if (headers != null && headers.size() > 0) {
+ for (Map.Entry header : headers) {
+ responseHeaders.add(header.getKey(), header.getValue());
+ }
+ }
+
+ responseHeaders.set("content-type", "application/json");
+ }
+
+ if (end) {
+ if (!trailersSent) {
+ trailersSent = true;
+ }
+ MultiMap responseTrailers;
+ if (trailersOnly) {
+ responseTrailers = httpResponse.headers();
+ } else {
+ responseTrailers = httpResponse.trailers();
+ }
+
+ if (trailers != null && trailers.size() > 0) {
+ for (Map.Entry trailer : trailers) {
+ responseTrailers.add(trailer.getKey(), trailer.getValue());
+ }
+ }
+ if (!responseHeaders.contains("grpc-status")) {
+ responseTrailers.set("grpc-status", status.toString());
+ }
+ if (status != GrpcStatus.OK) {
+ String msg = statusMessage;
+ if (msg != null && !responseHeaders.contains("grpc-status-message")) {
+ responseTrailers.set("grpc-message", Utils.utf8PercentEncode(msg));
+ }
+ } else {
+ responseTrailers.remove("grpc-message");
+ }
+ if (message != null) {
+ Buffer encoded = encode(message);
+ if (encoded == null) {
+ throw new IllegalStateException("The message is null");
+ }
+
+ responseHeaders.set("content-length", String.valueOf(encoded.length()));
+ return httpResponse.end(encoded);
+ } else {
+ return httpResponse.end();
+ }
+ } else {
+ Buffer encoded = encode(message);
+ if (encoded == null) {
+ throw new IllegalStateException("The message is null");
+ }
+
+ responseHeaders.set("content-length", String.valueOf(encoded.length()));
+ return httpResponse.write(encoded);
+ }
+ }
+
+ private Buffer encode(GrpcMessage message) {
+ if (message == null) {
+ return null;
+ }
+
+ ByteBuf bbuf = message.payload().getByteBuf();
+ CompositeByteBuf composite = Unpooled.compositeBuffer();
+ composite.addComponent(true, bbuf);
+ return Buffer.buffer(composite);
+ }
+
+ @Override
+ public void write(Resp data, Handler> handler) {
+ write(data).onComplete(handler);
+ }
+
+ @Override
+ public void end(Handler> handler) {
+ end().onComplete(handler);
+ }
+}
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..4d09ad8b6a62a
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingServer.java
@@ -0,0 +1,137 @@
+package io.quarkus.grpc.transcoding;
+
+import java.util.HashMap;
+import java.util.Map;
+
+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;
+
+/**
+ * A gRPC transcoding server that maps HTTP requests to gRPC methods.
+ *
+ * @see io.vertx.grpc.server.impl.GrpcServerImpl for the original implementation
+ * @see for the HTTP mapping rules
+ */
+public class GrpcTranscodingServer implements GrpcServer {
+
+ private final Vertx vertx;
+ private Handler> requestHandler;
+ private final Map methodMapping = new HashMap<>();
+ private final Map> methodCallHandlers = new HashMap<>();
+ private final 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 (GrpcTranscodingHttpUtils.isPathMatch(pathTemplate, requestPath)) {
+ 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 = GrpcTranscodingHttpUtils.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 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/transcoding/GrpcTranscodingWriteStreamAdapter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingWriteStreamAdapter.java
new file mode 100644
index 0000000000000..cf1414ad96712
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingWriteStreamAdapter.java
@@ -0,0 +1,55 @@
+package io.quarkus.grpc.transcoding;
+
+import io.vertx.grpc.common.GrpcMessageEncoder;
+import io.vertx.grpc.common.GrpcWriteStream;
+
+/**
+ * A write stream adapter that uses a {@link GrpcMessageEncoder} to encode the message payload.
+ *
+ * @param The type of the message payload.
+ * @see io.vertx.grpc.common.impl.WriteStreamAdapter for the original implementation
+ */
+public class GrpcTranscodingWriteStreamAdapter {
+
+ private GrpcWriteStream