io.quarkus
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 98a61b9775338f..a9c93eef87ad77 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
@@ -189,8 +189,6 @@ private void buildGrpcServer(Vertx vertx, GrpcServerConfiguration configuration,
LOGGER.info("Starting new Quarkus gRPC server (using Vert.x transport)...");
Route route = routerSupplier.getValue().route().handler(ctx -> {
- LOGGER.info("Request received => " + ctx.request().path());
-
if (!isGrpc(ctx)) {
ctx.next();
} else {
diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcTranscodingConfig.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcTranscodingConfig.java
new file mode 100644
index 00000000000000..fa7390b9b7a2fd
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcTranscodingConfig.java
@@ -0,0 +1,20 @@
+package io.quarkus.grpc.runtime.config;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+/**
+ * Configuration root for gRPC Transcoding feature in Quarkus. gRPC Transcoding allows you to create
+ * RESTful JSON APIs that are backed by existing gRPC services.
+ */
+@ConfigRoot(name = "grpc.transcoding", phase = ConfigPhase.BUILD_TIME)
+public class GrpcTranscodingConfig {
+
+ /**
+ * Flag to enable or disable the gRPC Transcoding feature.
+ * The default value is `false` (disabled).
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean enabled;
+}
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 00000000000000..6499fe808ad220
--- /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/GrpcTranscodingMessageWriter.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageWriter.java
new file mode 100644
index 00000000000000..192bd6ab28d53a
--- /dev/null
+++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingMessageWriter.java
@@ -0,0 +1,79 @@
+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.
+ * @throws IllegalArgumentException If the provided `existingPayload` cannot be parsed as valid JSON.
+ */
+ public static Map mergeParameters(Map pathParams, Map queryParams,
+ Buffer existingPayload) {
+ Map allParams = new HashMap<>();
+
+ if (existingPayload != null && existingPayload.getBytes().length > 0) {
+ try {
+ String existingPayloadJson = new String(existingPayload.getBytes());
+ allParams = new HashMap(Json.decodeValue(existingPayloadJson, Map.class));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid payload", e);
+ }
+ }
+
+ 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/GrpcTranscodingRequest.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/transcoding/GrpcTranscodingRequest.java
index 8af75490b65cec..c4912ff6cd60a0 100644
--- 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
@@ -2,7 +2,6 @@
import static io.vertx.grpc.common.GrpcError.mapHttp2ErrorCode;
-import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collector;
@@ -108,7 +107,7 @@ public void init() {
}
private GrpcMessage mergeParametersIntoMessage(GrpcMessage msg) {
- Map allParams = mergeParameters(
+ Map allParams = GrpcTranscodingMessageWriter.mergeParameters(
pathParams,
queryParams,
msg.payload());
@@ -117,49 +116,6 @@ private GrpcMessage mergeParametersIntoMessage(GrpcMessage msg) {
return GrpcMessage.message(msg.encoding(), Buffer.buffer(jsonPayload));
}
- private Map mergeParameters(Map pathParams, Map queryParams,
- Buffer existingPayload) {
- Map allParams = new HashMap<>();
-
- if (existingPayload != null && existingPayload.getBytes().length > 0) {
- try {
- String existingPayloadJson = new String(existingPayload.getBytes());
- allParams = new HashMap(Json.decodeValue(existingPayloadJson, Map.class));
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid payload", e);
- }
- }
-
- // Process pathParams
- for (Map.Entry entry : pathParams.entrySet()) {
- insertNestedParam(allParams, entry.getKey(), entry.getValue());
- }
-
- // Process queryParams
- for (Map.Entry entry : queryParams.entrySet()) {
- insertNestedParam(allParams, entry.getKey(), entry.getValue());
- }
-
- return allParams;
- }
-
- private void insertNestedParam(Map paramsMap, String key, String value) {
- String[] pathComponents = key.split("\\."); // Split by periods
-
- // Iterate until the last component
- 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<>()); // Create nested object
- }
- currentLevel = (Map) currentLevel.get(component);
- }
-
- // Insert the final value
- currentLevel.put(pathComponents[pathComponents.length - 1], value);
- }
-
protected Req decodeMessage(GrpcMessage msg) throws CodecException {
return messageDecoder.decode(msg);
}
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
index bafbafc7acd175..5983ee4751c77d 100644
--- 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
@@ -1,10 +1,7 @@
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;
@@ -24,9 +21,9 @@ 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<>();
+ 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;
@@ -39,7 +36,7 @@ public void handle(HttpServerRequest httpRequest) {
for (Map.Entry entry : methodMapping.entrySet()) {
String pathTemplate = entry.getKey();
String mappedMethod = entry.getValue();
- if (isPathMatch(requestPath, pathTemplate)) {
+ if (GrpcTranscodingHttpUtils.isPathMatch(requestPath, pathTemplate)) {
GrpcTranscodingMetadata, ?> metadata = metadataHandlers.get(mappedMethod);
if (metadata != null) {
if (metadata.getHttpMethodName().equals(httpRequest.method().name())) {
@@ -67,7 +64,7 @@ private void handleWithMappedMethod(HttpServerRequest httpRequest, String pathTe
private void handle(String pathTemplate, MethodCallHandler method, HttpServerRequest httpRequest,
GrpcMethodCall methodCall) {
- Map pathParams = extractPathParams(pathTemplate, httpRequest.path());
+ 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));
@@ -119,95 +116,6 @@ public MethodDescriptor.Marshaller findResponseMarshaller(String fullMeth
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;
diff --git a/extensions/grpc/stubs/pom.xml b/extensions/grpc/stubs/pom.xml
index d494224d19e469..d3a387d2272203 100644
--- a/extensions/grpc/stubs/pom.xml
+++ b/extensions/grpc/stubs/pom.xml
@@ -98,7 +98,6 @@
quarkus-grpc-protoc-plugin
${project.version}
io.quarkus.grpc.protoc.plugin.MutinyGrpcGenerator
- --skipTranscoding
diff --git a/integration-tests/grpc-transcoding/src/main/resources/application.properties b/integration-tests/grpc-transcoding/src/main/resources/application.properties
index 747229c0a246cb..298b78be5325cd 100644
--- a/integration-tests/grpc-transcoding/src/main/resources/application.properties
+++ b/integration-tests/grpc-transcoding/src/main/resources/application.properties
@@ -1,6 +1,7 @@
quarkus.grpc.server.port=9001
quarkus.grpc.server.instances=2
quarkus.grpc.server.use-separate-server=false
+quarkus.grpc.transcoding.enabled=true
%vertx.quarkus.grpc.server.use-separate-server=false
%n2o.quarkus.grpc.server.use-separate-server=true
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 84544a0a183a4a..a79214f5901fbb 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -56,8 +56,7 @@
classpath:enforcer-rules/quarkus-banned-dependencies-okhttp.xml
-
+
@@ -180,7 +179,7 @@
infinispan-cache
devtools
devtools-registry-client
-
+ gradle
main
kafka
kafka-ssl