From b31c9b9b57a87759e768980706e8b121d0b43259 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 5 May 2021 10:34:39 +0200 Subject: [PATCH] Dev UI - initial gRPC support - list all registered services - make it possible to test unary calls via Dev UI --- .../deployment/BindableServiceBuildItem.java | 4 + .../quarkus/grpc/deployment/GrpcDotNames.java | 14 +- .../grpc/deployment/GrpcServerProcessor.java | 1 + .../devmode/GrpcDevConsoleProcessor.java | 270 ++++++++++++++++++ .../resources/dev-templates/embedded.html | 3 + .../main/resources/dev-templates/service.html | 86 ++++++ .../resources/dev-templates/services.html | 75 +++++ ...ClientInterceptorPriorityReversedTest.java | 5 +- .../ClientInterceptorPriorityTest.java | 5 +- .../ClientInterceptorRegistrationTest.java | 5 +- .../devconsole/DevConsoleUnaryMethodTest.java | 31 ++ .../MutinyGrpcServiceWithPlainTextTest.java | 5 +- .../server/MutinyGrpcServiceWithSSLTest.java | 5 +- ...ServerInterceptorPriorityReversedTest.java | 5 +- .../ServerInterceptorPriorityTest.java | 5 +- .../ServerInterceptorRegistrationTest.java | 5 +- .../server/services/MutinyHelloService.java | 4 +- .../src/main/resources/MutinyStub.mustache | 3 +- .../grpc/runtime/GrpcServerRecorder.java | 12 +- .../devmode/GrpcDevConsoleRecorder.java | 27 ++ .../grpc/runtime/devmode/GrpcServices.java | 160 +++++++++++ .../io/quarkus/grpc/runtime/MutinyGrpc.java | 8 + .../devmode/console/DevConsoleProcessor.java | 16 +- .../main/resources/dev-templates/main.html | 2 +- .../spi/DevConsoleRouteBuildItem.java | 35 ++- 25 files changed, 764 insertions(+), 27 deletions(-) create mode 100644 extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/devmode/GrpcDevConsoleProcessor.java create mode 100644 extensions/grpc/deployment/src/main/resources/dev-templates/embedded.html create mode 100644 extensions/grpc/deployment/src/main/resources/dev-templates/service.html create mode 100644 extensions/grpc/deployment/src/main/resources/dev-templates/services.html create mode 100644 extensions/grpc/deployment/src/test/java/io/quarkus/grpc/devconsole/DevConsoleUnaryMethodTest.java create mode 100644 extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/GrpcDevConsoleRecorder.java create mode 100644 extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/GrpcServices.java create mode 100644 extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyGrpc.java diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/BindableServiceBuildItem.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/BindableServiceBuildItem.java index f2a8120ce67ba..0019faa6ca4bd 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/BindableServiceBuildItem.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/BindableServiceBuildItem.java @@ -31,4 +31,8 @@ public boolean hasBlockingMethods() { return !blockingMethods.isEmpty(); } + public DotName getServiceClass() { + return serviceClass; + } + } 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 e2608006f9d0c..0734eea1c4eaa 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 @@ -10,6 +10,7 @@ import io.quarkus.grpc.GrpcClient; import io.quarkus.grpc.GrpcService; import io.quarkus.grpc.runtime.GeneratedGrpcBean; +import io.quarkus.grpc.runtime.MutinyGrpc; import io.quarkus.grpc.runtime.MutinyStub; import io.quarkus.grpc.runtime.supports.Channels; import io.quarkus.grpc.runtime.supports.GrpcClientConfigProvider; @@ -17,15 +18,16 @@ public class GrpcDotNames { - static final DotName BINDABLE_SERVICE = DotName.createSimple(BindableService.class.getName()); - static final DotName CHANNEL = DotName.createSimple(Channel.class.getName()); - static final DotName GRPC_CLIENT = DotName.createSimple(GrpcClient.class.getName()); - static final DotName GRPC_SERVICE = DotName.createSimple(GrpcService.class.getName()); + public static final DotName BINDABLE_SERVICE = DotName.createSimple(BindableService.class.getName()); + public static final DotName CHANNEL = DotName.createSimple(Channel.class.getName()); + public static final DotName GRPC_CLIENT = DotName.createSimple(GrpcClient.class.getName()); + public static final DotName GRPC_SERVICE = DotName.createSimple(GrpcService.class.getName()); static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName()); - static final DotName ABSTRACT_BLOCKING_STUB = DotName.createSimple(AbstractBlockingStub.class.getName()); - static final DotName MUTINY_STUB = DotName.createSimple(MutinyStub.class.getName()); + public static final DotName ABSTRACT_BLOCKING_STUB = DotName.createSimple(AbstractBlockingStub.class.getName()); + public static final DotName MUTINY_STUB = DotName.createSimple(MutinyStub.class.getName()); + public static final DotName MUTINY_GRPC = DotName.createSimple(MutinyGrpc.class.getName()); static final DotName GENERATED_GRPC_BEAN = DotName.createSimple(GeneratedGrpcBean.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 ccf07ee04b8c3..ef17c920034fe 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 @@ -302,4 +302,5 @@ void registerSslResources(BuildProducer resourceBu ExtensionSslNativeSupportBuildItem extensionSslNativeSupport() { return new ExtensionSslNativeSupportBuildItem(GRPC_SERVER); } + } diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/devmode/GrpcDevConsoleProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/devmode/GrpcDevConsoleProcessor.java new file mode 100644 index 0000000000000..da21201d1de13 --- /dev/null +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/devmode/GrpcDevConsoleProcessor.java @@ -0,0 +1,270 @@ +package io.quarkus.grpc.deployment.devmode; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.util.JsonFormat; + +import io.grpc.Channel; +import io.grpc.MethodDescriptor; +import io.grpc.MethodDescriptor.Marshaller; +import io.grpc.MethodDescriptor.PrototypeMarshaller; +import io.grpc.ServiceDescriptor; +import io.grpc.netty.NettyChannelBuilder; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.runtime.BeanLookupSupplier; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; +import io.quarkus.grpc.deployment.GrpcDotNames; +import io.quarkus.grpc.protoc.plugin.MutinyGrpcGenerator; +import io.quarkus.grpc.runtime.devmode.GrpcDevConsoleRecorder; +import io.quarkus.grpc.runtime.devmode.GrpcServices; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class GrpcDevConsoleProcessor { + + private static final Logger LOG = Logger.getLogger(GrpcDevConsoleProcessor.class); + + @BuildStep(onlyIf = IsDevelopment.class) + public void devConsoleInfo(BuildProducer beans, + BuildProducer infos) { + beans.produce(AdditionalBeanBuildItem.unremovableOf(GrpcServices.class)); + infos.produce( + new DevConsoleRuntimeTemplateInfoBuildItem("grpcServices", + new BeanLookupSupplier(GrpcServices.class))); + } + + @BuildStep(onlyIf = IsDevelopment.class) + public void collectMessagePrototypes(CombinedIndexBuildItem index, + // Dummy producer to ensure the build step is executed + BuildProducer service) + throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, InvalidProtocolBufferException { + Map messagePrototypes = new HashMap<>(); + + for (Class grpcServiceClass : getGrpcServices(index.getIndex())) { + + Method method = grpcServiceClass.getDeclaredMethod("getServiceDescriptor"); + ServiceDescriptor serviceDescriptor = (ServiceDescriptor) method.invoke(null); + + for (MethodDescriptor methodDescriptor : serviceDescriptor.getMethods()) { + Marshaller requestMarshaller = methodDescriptor.getRequestMarshaller(); + if (requestMarshaller instanceof PrototypeMarshaller) { + PrototypeMarshaller protoMarshaller = (PrototypeMarshaller) requestMarshaller; + Object prototype = protoMarshaller.getMessagePrototype(); + messagePrototypes.put(methodDescriptor.getFullMethodName() + "_REQUEST", + JsonFormat.printer().includingDefaultValueFields().print((MessageOrBuilder) prototype)); + } + } + } + DevConsoleManager.setGlobal("io.quarkus.grpc.messagePrototypes", messagePrototypes); + + } + + @Record(value = RUNTIME_INIT) + @BuildStep + DevConsoleRouteBuildItem registerTestEndpoint(GrpcDevConsoleRecorder recorder, CombinedIndexBuildItem index) + throws ClassNotFoundException, NoSuchMethodException, + SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + // Store the server config so that it can be used in the test endpoint handler + recorder.setServerConfiguration(); + return new DevConsoleRouteBuildItem("test", "POST", new TestEndpointHandler(getGrpcServices(index.getIndex())), true); + } + + static class TestEndpointHandler implements Handler { + + private Map blockingStubs; + private Map serviceDescriptors; + private final Collection> grpcServiceClasses; + + TestEndpointHandler(Collection> grpcServiceClasses) { + this.grpcServiceClasses = grpcServiceClasses; + } + + void init() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + if (blockingStubs == null) { + blockingStubs = new HashMap<>(); + serviceDescriptors = new HashMap<>(); + + Map serverConfig = DevConsoleManager.getGlobal("io.quarkus.grpc.serverConfig"); + + if (Boolean.FALSE.equals(serverConfig.get("ssl"))) { + for (Class grpcServiceClass : grpcServiceClasses) { + + Method method = grpcServiceClass.getDeclaredMethod("getServiceDescriptor"); + ServiceDescriptor serviceDescriptor = (ServiceDescriptor) method.invoke(null); + serviceDescriptors.put(serviceDescriptor.getName(), serviceDescriptor); + + // TODO more config options + Channel channel = NettyChannelBuilder + .forAddress(serverConfig.get("host").toString(), (Integer) serverConfig.get("port")) + .usePlaintext() + .build(); + Method blockingStubFactoryMethod; + + try { + blockingStubFactoryMethod = grpcServiceClass.getDeclaredMethod("newBlockingStub", Channel.class); + } catch (NoSuchMethodException e) { + LOG.warnf("Ignoring gRPC service - newBlockingStub() method not declared on %s", grpcServiceClass); + continue; + } + + Object blockingStub = blockingStubFactoryMethod.invoke(null, channel); + blockingStubs.put(serviceDescriptor.getName(), blockingStub); + } + } + } + } + + @Override + public void handle(RoutingContext context) { + try { + // Lazily initialize the handler + init(); + } catch (Exception e) { + throw new IllegalStateException("Unable to initialize the test endpoint handler"); + } + + String serviceName = context.request().getParam("serviceName"); + String methodName = context.request().getParam("methodName"); + String testJsonData = context.getBodyAsString(); + + Object blockingStub = blockingStubs.get(serviceName); + + if (blockingStub == null) { + error(context, "No blocking stub found for: " + serviceName); + } else { + ServiceDescriptor serviceDescriptor = serviceDescriptors.get(serviceName); + MethodDescriptor methodDescriptor = null; + for (MethodDescriptor method : serviceDescriptor.getMethods()) { + if (method.getBareMethodName().equals(methodName)) { + methodDescriptor = method; + } + } + + if (methodDescriptor == null) { + error(context, "No method descriptor found for: " + serviceName + "/" + methodName); + } else { + + // We need to find the correct method declared on the blocking stub + Method stubMethod = null; + String realMethodName = decapitalize(methodDescriptor.getBareMethodName()); + + for (Method method : blockingStub.getClass().getDeclaredMethods()) { + if (method.getName().equals(realMethodName)) { + stubMethod = method; + } + } + + if (stubMethod == null) { + error(context, realMethodName + " method not declared on the " + blockingStub.getClass()); + } else { + + // Identify the request class + Marshaller requestMarshaller = methodDescriptor.getRequestMarshaller(); + if (requestMarshaller instanceof PrototypeMarshaller) { + PrototypeMarshaller protoMarshaller = (PrototypeMarshaller) requestMarshaller; + Class requestType = protoMarshaller.getMessagePrototype().getClass(); + + try { + // Create a new builder for the request message, e.g. HelloRequest.newBuilder() + Method newBuilderMethod = requestType.getDeclaredMethod("newBuilder"); + Message.Builder builder = (Builder) newBuilderMethod.invoke(null); + ; + + // Use the test data to build the request object + JsonFormat.parser().merge(testJsonData, builder); + + // Invoke the blocking stub method and format the response as JSON + Object response = stubMethod.invoke(blockingStub, builder.build()); + context.response().putHeader("Content-Type", "application/json"); + context.end(JsonFormat.printer().print((MessageOrBuilder) response)); + + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + error(context, "Unable to identify the request type for: " + methodDescriptor); + } + } + } + } + + } + } + + static void error(RoutingContext rc, String message) { + LOG.warn(message); + rc.response().setStatusCode(500).end(message); + } + + Collection> getGrpcServices(IndexView index) throws ClassNotFoundException { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + Set serviceClassNames = new HashSet<>(); + for (ClassInfo mutinyGrpc : index.getAllKnownImplementors(GrpcDotNames.MUTINY_GRPC)) { + // Find the original impl class + // e.g. examples.MutinyGreeterGrpc -> examples.GreeterGrpc + DotName originalImplName = DotName + .createSimple(mutinyGrpc.name().toString().replace(MutinyGrpcGenerator.CLASS_PREFIX, "")); + ClassInfo originalImpl = index.getClassByName(originalImplName); + if (originalImpl == null) { + throw new IllegalStateException( + "The original implementation class of a gRPC service not found:" + originalImplName); + } + // Must declare static io.grpc.ServiceDescriptor getServiceDescriptor() + MethodInfo getServiceDescriptor = originalImpl.method("getServiceDescriptor"); + if (getServiceDescriptor != null && Modifier.isStatic(getServiceDescriptor.flags()) + && getServiceDescriptor.returnType().name().toString().equals(ServiceDescriptor.class.getName())) { + serviceClassNames.add(getServiceDescriptor.declaringClass().name().toString()); + } + } + List> serviceClasses = new ArrayList<>(); + for (String className : serviceClassNames) { + serviceClasses.add(tccl.loadClass(className)); + } + return serviceClasses; + } + + static String decapitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && + Character.isUpperCase(name.charAt(0))) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + +} diff --git a/extensions/grpc/deployment/src/main/resources/dev-templates/embedded.html b/extensions/grpc/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 0000000000000..f63deefe0232a --- /dev/null +++ b/extensions/grpc/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,3 @@ + + + Services diff --git a/extensions/grpc/deployment/src/main/resources/dev-templates/service.html b/extensions/grpc/deployment/src/main/resources/dev-templates/service.html new file mode 100644 index 0000000000000..8633ca93ca4b0 --- /dev/null +++ b/extensions/grpc/deployment/src/main/resources/dev-templates/service.html @@ -0,0 +1,86 @@ +{#include main} + {#style} + span.app-class { + cursor:pointer; + color:blue; + text-decoration:underline; + } + {/style} + {#script} + $(document).ready(function(){ + if (!ideKnown()) { + return; + } + $(".class-candidate").each(function() { + var className = $(this).text(); + if (appClassLang(className)) { + $(this).addClass("app-class"); + } + }); + + $(".app-class").on("click", function() { + openInIDE($(this).text()); + }); + }); + + function sendTestRequest(serviceName, methodName) { + const testRequest = document.getElementById(serviceName + "/" + methodName + "_request"); + $.ajax({ + type: "POST", + url: "test?serviceName=" + encodeURIComponent(serviceName) + "&methodName=" + encodeURIComponent(methodName), + contentType: "application/json", + data: testRequest.value, + success: function (data) { + const testResponse = document.getElementById(serviceName + "/" + methodName + "_response"); + testResponse.value = JSON.stringify(data); + } + }); + } + + {/script} + {#breadcrumbs} Services{/breadcrumbs} + {#title}{info:grpcServices.get(currentRequest.params.get('name')).name}{/title} + {#body} + + {#let service=info:grpcServices.get(currentRequest.params.get('name'))} +

+ {#when service.status} + {#is SERVING} + + {#is NOT_SERVING} + + {#is in UNKNOWN UNRECOGNIZED} + + {/when} + {service.name} +

+
+ Implemented by: {service.serviceClass} +
+ + {#for method in service.methodsWithPrototypes} +
+

{method.type} {method.bareMethodName}

+ {#if method.isTestable} + {#when method.type} + {#is UNARY} +
+
+
+ +
+ +
+
+ +
+
+
+ {/when} + {/if} + {/for} + + {/let} + + {/body} +{/include} \ No newline at end of file diff --git a/extensions/grpc/deployment/src/main/resources/dev-templates/services.html b/extensions/grpc/deployment/src/main/resources/dev-templates/services.html new file mode 100644 index 0000000000000..eed79393d2ec8 --- /dev/null +++ b/extensions/grpc/deployment/src/main/resources/dev-templates/services.html @@ -0,0 +1,75 @@ +{#include main} + {#style} + span.app-class { + cursor:pointer; + color:blue; + text-decoration:underline; + } + span.larger-badge { + font-size: 1em; + } + {/style} + {#script} + $(document).ready(function(){ + if (!ideKnown()) { + return; + } + $(".class-candidate").each(function() { + var className = $(this).text(); + if (appClassLang(className)) { + $(this).addClass("app-class"); + } + }); + + $(".app-class").on("click", function() { + openInIDE($(this).text()); + }); + }); + {/script} + {#title}Services{/title} + {#body} + + + + + + + + + + + + {#for service in info:grpcServices.infos} + + + + + + + {/for} + +
#Name and StatusImplementation ClassMethods
{count}. + {#when service.status} + {#is SERVING} + + {#is NOT_SERVING} + + {#is in UNKNOWN UNRECOGNIZED} + + {/when} + {service.name} + + {service.serviceClass} + +
    + {#each service.methodsWithPrototypes} +
  • {it.type} {it.bareMethodName}
  • + {/each} +
      +
+ {#if service.hasTestableMethod} + Test + {/if} +
+ {/body} +{/include} \ No newline at end of file diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityReversedTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityReversedTest.java index a3afd8e0e79a3..ad55ba96ce30b 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityReversedTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityReversedTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -26,7 +28,8 @@ public class ClientInterceptorPriorityReversedTest { () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MySecondClientInterceptor.class, MyFirstClientInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)) .withConfigurationResource("hello-config.properties"); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityTest.java index b4eda4d5b6656..8b85bbdecf94d 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorPriorityTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -26,7 +28,8 @@ public class ClientInterceptorPriorityTest { () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MyFirstClientInterceptor.class, MySecondClientInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)) .withConfigurationResource("hello-config.properties"); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorRegistrationTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorRegistrationTest.java index edb7b7b5bec4b..e748f0fa64d8e 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorRegistrationTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/interceptors/ClientInterceptorRegistrationTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -25,7 +27,8 @@ public class ClientInterceptorRegistrationTest { static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MyFirstClientInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)) .withConfigurationResource("hello-config.properties"); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/devconsole/DevConsoleUnaryMethodTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/devconsole/DevConsoleUnaryMethodTest.java new file mode 100644 index 0000000000000..926a5650cf2c2 --- /dev/null +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/devconsole/DevConsoleUnaryMethodTest.java @@ -0,0 +1,31 @@ +package io.quarkus.grpc.devconsole; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.grpc.examples.helloworld.MutinyGreeterGrpc; +import io.quarkus.grpc.server.services.MutinyHelloService; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class DevConsoleUnaryMethodTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addPackage(MutinyGreeterGrpc.class.getPackage()) + .addClass(MutinyHelloService.class)); + + @Test + public void testUnaryMethodCall() { + RestAssured.with().body("{\n\"name\": \"Martin\"}") + .post("q/dev/io.quarkus.quarkus-grpc/test?serviceName=helloworld.Greeter&methodName=SayHello") + .then() + .statusCode(200) + .body(Matchers.containsString("Hello Martin")); + + } + +} diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java index 2d18e7aae3b6b..bc21fa8def4b1 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java @@ -6,6 +6,8 @@ import com.google.protobuf.EmptyProtos; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -31,7 +33,8 @@ public class MutinyGrpcServiceWithPlainTextTest extends GrpcServiceTestBase { .setFlatClassPath(true).setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, TestServiceGrpc.class)); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java index 7b496f5accfac..edcf342c1f61c 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java @@ -13,6 +13,8 @@ import com.google.protobuf.EmptyProtos; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -41,7 +43,8 @@ public class MutinyGrpcServiceWithSSLTest extends GrpcServiceTestBase { .setFlatClassPath(true).setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, TestServiceGrpc.class)) diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityReversedTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityReversedTest.java index f38aa2d51382c..789ebaa394df9 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityReversedTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityReversedTest.java @@ -13,6 +13,8 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -28,7 +30,8 @@ public class ServerInterceptorPriorityReversedTest { static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MySecondInterceptor.class, MyFirstInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)); protected ManagedChannel channel; diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityTest.java index c336b274a4667..513584414039b 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorPriorityTest.java @@ -13,6 +13,8 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -28,7 +30,8 @@ public class ServerInterceptorPriorityTest { static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MyFirstInterceptor.class, MySecondInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)); protected ManagedChannel channel; diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorRegistrationTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorRegistrationTest.java index 40fe92067cfe4..ce659ffa89489 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorRegistrationTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/interceptors/ServerInterceptorRegistrationTest.java @@ -13,6 +13,8 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.GreeterBean; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloReplyOrBuilder; @@ -28,7 +30,8 @@ public class ServerInterceptorRegistrationTest { static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) .addClasses(MutinyHelloService.class, MyFirstInterceptor.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + GreeterGrpc.class, Greeter.class, GreeterBean.class, HelloRequest.class, HelloReply.class, + MutinyGreeterGrpc.class, HelloRequestOrBuilder.class, HelloReplyOrBuilder.class)); protected ManagedChannel channel; diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/MutinyHelloService.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/MutinyHelloService.java index 01fa1c1de77fc..5959f5ba2d4ba 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/MutinyHelloService.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/services/MutinyHelloService.java @@ -1,13 +1,13 @@ package io.quarkus.grpc.server.services; +import io.grpc.examples.helloworld.Greeter; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; -import io.grpc.examples.helloworld.MutinyGreeterGrpc; import io.quarkus.grpc.GrpcService; import io.smallrye.mutiny.Uni; @GrpcService -public class MutinyHelloService extends MutinyGreeterGrpc.GreeterImplBase { +public class MutinyHelloService implements Greeter { @Override public Uni sayHello(HelloRequest request) { diff --git a/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache b/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache index c9e50dd12982d..87d15821e3e8a 100644 --- a/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache +++ b/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache @@ -8,14 +8,13 @@ import static io.grpc.stub.ServerCalls.asyncServerStreamingCall; import static io.grpc.stub.ServerCalls.asyncClientStreamingCall; import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall; - {{#deprecated}} @java.lang.Deprecated {{/deprecated}} @javax.annotation.Generated( value = "by {{classPrefix}} Grpc generator", comments = "Source: {{protoName}}") -public final class {{className}} { +public final class {{className}} implements io.quarkus.grpc.runtime.MutinyGrpc { private {{className}}() {} public static {{classPrefix}}{{serviceName}}Stub new{{classPrefix}}Stub(io.grpc.Channel channel) { 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 0b1356f9d8955..69326a2326b08 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 @@ -68,6 +68,11 @@ public class GrpcServerRecorder { private Map> blockingMethodsPerService = Collections.emptyMap(); private static volatile DevModeWrapper devModeWrapper; + private static volatile List services; + + public static List getServices() { + return services; + } public void initializeGrpcServer(RuntimeValue vertxSupplier, GrpcConfiguration cfg, @@ -243,10 +248,15 @@ private static List collectServiceDefinitions(Instance config = Arc.container().instance(GrpcConfiguration.class)) { + GrpcServerConfiguration serverConfig = config.get().server; + Map map = new HashMap<>(); + map.put("host", serverConfig.host); + map.put("port", serverConfig.port); + map.put("ssl", serverConfig.ssl.certificate.isPresent() || serverConfig.ssl.keyStore.isPresent()); + DevConsoleManager.setGlobal("io.quarkus.grpc.serverConfig", map); + } + } + +} diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/GrpcServices.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/GrpcServices.java new file mode 100644 index 0000000000000..a6bfc5b2fe6fa --- /dev/null +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/devmode/GrpcServices.java @@ -0,0 +1,160 @@ +package io.quarkus.grpc.runtime.devmode; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import grpc.health.v1.HealthOuterClass.HealthCheckResponse.ServingStatus; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.ServerMethodDefinition; +import io.quarkus.arc.Subclass; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.grpc.runtime.GrpcServerRecorder; +import io.quarkus.grpc.runtime.GrpcServerRecorder.GrpcServiceDefinition; +import io.quarkus.grpc.runtime.config.GrpcConfiguration; +import io.quarkus.grpc.runtime.devmode.GrpcServices.ServiceDefinitionAndStatus; +import io.quarkus.grpc.runtime.health.GrpcHealthStorage; + +@Singleton +public class GrpcServices extends AbstractMap { + + @Inject + GrpcConfiguration configuration; + + @Inject + GrpcHealthStorage healthStorage; + + public List getInfos() { + List services = new ArrayList<>(GrpcServerRecorder.getServices().size()); + for (GrpcServiceDefinition definition : GrpcServerRecorder.getServices()) { + services.add(new ServiceDefinitionAndStatus(definition, healthStorage.getStatuses() + .getOrDefault(definition.definition.getServiceDescriptor().getName(), ServingStatus.UNKNOWN))); + } + return services; + } + + @Override + public Set> entrySet() { + Set> entries = new HashSet<>(); + for (GrpcServiceDefinition definition : GrpcServerRecorder.getServices()) { + entries.add(new ServiceDefinitionAndStatus(definition, healthStorage.getStatuses() + .getOrDefault(definition.definition.getServiceDescriptor().getName(), ServingStatus.UNKNOWN))); + } + return entries; + } + + public class ServiceDefinitionAndStatus implements Map.Entry { + + public final GrpcServiceDefinition definition; + public final ServingStatus status; + + public ServiceDefinitionAndStatus(GrpcServiceDefinition definition, ServingStatus status) { + this.definition = definition; + this.status = status; + } + + public String getName() { + return definition.definition.getServiceDescriptor().getName(); + } + + public String getServiceClass() { + Class instanceClass = definition.service.getClass(); + if (definition.service instanceof Subclass) { + instanceClass = instanceClass.getSuperclass(); + } + return instanceClass.getName(); + } + + public Collection> getMethods() { + return definition.definition.getMethods(); + } + + public Collection getMethodsWithPrototypes() { + Map prototypes = DevConsoleManager.getGlobal("io.quarkus.grpc.messagePrototypes"); + List methods = new ArrayList<>(); + for (ServerMethodDefinition method : getMethods()) { + methods.add( + new MethodAndPrototype(method, + prototypes.get(method.getMethodDescriptor().getFullMethodName() + "_REQUEST"))); + } + return methods; + } + + @Override + public String getKey() { + return getName(); + } + + @Override + public ServiceDefinitionAndStatus getValue() { + return this; + } + + @Override + public ServiceDefinitionAndStatus setValue(ServiceDefinitionAndStatus value) { + throw new UnsupportedOperationException(); + } + + public boolean hasTestableMethod() { + if (configuration.server.ssl.certificate.isPresent() || configuration.server.ssl.keyStore.isPresent()) { + return false; + } + Map prototypes = DevConsoleManager.getGlobal("io.quarkus.grpc.messagePrototypes"); + for (ServerMethodDefinition method : getMethods()) { + if (method.getMethodDescriptor().getType() == MethodType.UNARY + && prototypes.containsKey(method.getMethodDescriptor().getFullMethodName() + "_REQUEST")) { + return true; + } + } + return false; + } + + } + + public class MethodAndPrototype { + + private final ServerMethodDefinition definition; + private final String prototype; + + public MethodAndPrototype(ServerMethodDefinition definition, String prototype) { + this.definition = definition; + this.prototype = prototype; + } + + public MethodType getType() { + return definition.getMethodDescriptor().getType(); + } + + public String getBareMethodName() { + return definition.getMethodDescriptor().getBareMethodName(); + } + + public String getFullMethodName() { + return definition.getMethodDescriptor().getFullMethodName(); + } + + public boolean hasPrototype() { + return prototype != null; + } + + public boolean isTestable() { + if (configuration.server.ssl.certificate.isPresent() || configuration.server.ssl.keyStore.isPresent()) { + return false; + } + return MethodType.UNARY == getType(); + } + + public String getPrototype() { + return prototype; + } + + } + +} diff --git a/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyGrpc.java b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyGrpc.java new file mode 100644 index 0000000000000..890f67db8d07a --- /dev/null +++ b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyGrpc.java @@ -0,0 +1,8 @@ +package io.quarkus.grpc.runtime; + +/** + * Used to mark a generated Mutiny gRPC service. + */ +public interface MutinyGrpc { + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 6812a9a329e44..7d0b351178e10 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -59,7 +59,6 @@ import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.deployment.util.ArtifactInfoUtil; import io.quarkus.deployment.util.WebJarUtil; import io.quarkus.dev.console.DevConsoleManager; @@ -106,8 +105,10 @@ import io.vertx.core.impl.EventLoopContext; import io.vertx.core.impl.VertxInternal; import io.vertx.core.net.impl.VertxHandler; +import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; public class DevConsoleProcessor { @@ -317,10 +318,13 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List groupAndArtifact = i.groupIdAndArtifactId(curateOutcomeBuildItem); // deployment side handling - if (!(i.getHandler() instanceof BytecodeRecorderImpl.ReturnedProxy)) { - router.route(HttpMethod.valueOf(i.getMethod()), - "/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath()) - .handler(i.getHandler()); + if (i.isDeploymentSide()) { + Route route = router.route(HttpMethod.valueOf(i.getMethod()), + "/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath()); + if (i.isBodyHandlerRequired()) { + route.handler(BodyHandler.create()); + } + route.handler(i.getHandler()); } } @@ -369,7 +373,7 @@ public void setupDevConsoleRoutes( Entry groupAndArtifact = i.groupIdAndArtifactId(curateOutcomeBuildItem); // if the handler is a proxy, then that means it's been produced by a recorder and therefore belongs in the regular runtime Vert.x instance // otherwise this is handled in the setupDeploymentSideHandling method - if (i.getHandler() instanceof BytecodeRecorderImpl.ReturnedProxy) { + if (!i.isDeploymentSide()) { routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .routeFunction( "dev/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath(), diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html index 3c7091c4d9122..d63db0fe8ec8d 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html @@ -38,7 +38,7 @@ {#if currentExtensionName == 'Eclipse Vert.x - HTTP'} {#insert title/} {#else} - {currentExtensionName} {#insert title/} + {currentExtensionName}{#insert breadcrumbs/} {#insert title/} {/if} diff --git a/extensions/vertx-http/dev-console-spi/src/main/java/io/quarkus/devconsole/spi/DevConsoleRouteBuildItem.java b/extensions/vertx-http/dev-console-spi/src/main/java/io/quarkus/devconsole/spi/DevConsoleRouteBuildItem.java index bf2c2966d225c..e17ccb8936d7d 100644 --- a/extensions/vertx-http/dev-console-spi/src/main/java/io/quarkus/devconsole/spi/DevConsoleRouteBuildItem.java +++ b/extensions/vertx-http/dev-console-spi/src/main/java/io/quarkus/devconsole/spi/DevConsoleRouteBuildItem.java @@ -5,6 +5,7 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.deployment.util.ArtifactInfoUtil; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -14,7 +15,11 @@ *

* Routes are registered under /q/dev/{groupId}.{artifactId}/ *

- * This handler executes in the deployment class loader. + * The route is registered: + *

    + *
  • in the "regular" app router (runtime class loader), if the handler is produced by a recorder (i.e. implements + * {@link io.quarkus.deployment.recording.BytecodeRecorderImpl.ReturnedProxy}),
  • + *
  • in the Dev UI router (deployment class loader).
  • */ public final class DevConsoleRouteBuildItem extends MultiBuildItem { @@ -24,6 +29,7 @@ public final class DevConsoleRouteBuildItem extends MultiBuildItem { private final String method; private final Class callerClass; private final Handler handler; + private final boolean isBodyHandlerRequired; public DevConsoleRouteBuildItem(String groupId, String artifactId, String path, String method, Handler handler) { @@ -33,10 +39,12 @@ public DevConsoleRouteBuildItem(String groupId, String artifactId, String path, this.method = method; this.handler = handler; this.callerClass = null; + this.isBodyHandlerRequired = false; } public DevConsoleRouteBuildItem(String path, String method, Handler handler) { + // we cannot use this() because the caller detection would not work String callerClassName = new RuntimeException().getStackTrace()[1].getClassName(); try { callerClass = Thread.currentThread().getContextClassLoader().loadClass(callerClassName); @@ -48,6 +56,23 @@ public DevConsoleRouteBuildItem(String path, String method, this.path = path; this.method = method; this.handler = handler; + this.isBodyHandlerRequired = false; + } + + public DevConsoleRouteBuildItem(String path, String method, + Handler handler, boolean isBodyHandlerRequired) { + String callerClassName = new RuntimeException().getStackTrace()[1].getClassName(); + try { + callerClass = Thread.currentThread().getContextClassLoader().loadClass(callerClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + this.groupId = null; + this.artifactId = null; + this.path = path; + this.method = method; + this.handler = handler; + this.isBodyHandlerRequired = isBodyHandlerRequired; } /** @@ -74,4 +99,12 @@ public Handler getHandler() { return handler; } + public boolean isDeploymentSide() { + return !(handler instanceof BytecodeRecorderImpl.ReturnedProxy); + } + + public boolean isBodyHandlerRequired() { + return isBodyHandlerRequired; + } + }