From d1cf5ea28798dfba1e82c4e1607b6cba0dd77302 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 24 Mar 2021 13:07:52 +0200 Subject: [PATCH] Allow using @Blocking on implementation in addition to interface for JAX-RS Resources Fixes: #15940 --- .../test/simple/InterfaceWithImplTest.java | 67 ++++++++++++ .../common/processor/EndpointIndexer.java | 100 +++++++++++------- 2 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/InterfaceWithImplTest.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/InterfaceWithImplTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/InterfaceWithImplTest.java new file mode 100644 index 0000000000000..64a2e217ee789 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/InterfaceWithImplTest.java @@ -0,0 +1,67 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.core.BlockingOperationSupport; +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.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.common.annotation.Blocking; + +public class InterfaceWithImplTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Greeting.class, GreetingImpl.class)); + + @Test + public void test() { + RestAssured.get("/hello/greeting/universe") + .then().body(Matchers.equalTo("name: universe / blocking: true")); + + RestAssured.get("/hello/greeting2/universe") + .then().body(Matchers.equalTo("name: universe / blocking: false")); + } + + @Path("/hello") + public interface Greeting { + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/greeting/{name}") + String greeting(String name); + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/greeting2/{name}") + String greeting2(String name); + } + + public static class GreetingImpl implements Greeting { + + @Override + @Blocking + public String greeting(String name) { + return resultString(name); + } + + @Override + public String greeting2(String name) { + return resultString(name); + } + + private String resultString(String name) { + return "name: " + name + " / blocking: " + BlockingOperationSupport.isBlockingAllowed(); + } + } + +} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 70e3f261bc81b..79be7536cc31b 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -373,40 +373,41 @@ private boolean hasProperModifiers(MethodInfo info) { } private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, - String[] classProduces, String[] classConsumes, Set classNameBindings, DotName httpMethod, MethodInfo info, - String methodPath, - Set classPathParameters, String classSseElementType) { + String[] classProduces, String[] classConsumes, Set classNameBindings, DotName httpMethod, + MethodInfo currentMethodInfo, + String methodPath, Set classPathParameters, String classSseElementType) { try { Map methodContext = new HashMap<>(); Set pathParameters = new HashSet<>(classPathParameters); URLUtils.parsePathParameters(methodPath, pathParameters); - Map[] parameterAnnotations = new Map[info.parameters().size()]; - MethodParameter[] methodParameters = new MethodParameter[info.parameters() + Map[] parameterAnnotations = new Map[currentMethodInfo.parameters().size()]; + MethodParameter[] methodParameters = new MethodParameter[currentMethodInfo.parameters() .size()]; - for (int paramPos = 0; paramPos < info.parameters().size(); ++paramPos) { + for (int paramPos = 0; paramPos < currentMethodInfo.parameters().size(); ++paramPos) { parameterAnnotations[paramPos] = new HashMap<>(); } - for (AnnotationInstance i : info.annotations()) { + for (AnnotationInstance i : currentMethodInfo.annotations()) { if (i.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { parameterAnnotations[i.target().asMethodParameter().position()].put(i.name(), i); } } - String[] consumes = extractProducesConsumesValues(info.annotation(CONSUMES), classConsumes); + String[] consumes = extractProducesConsumesValues(currentMethodInfo.annotation(CONSUMES), classConsumes); boolean suspended = false; boolean sse = false; boolean formParamRequired = false; boolean multipart = false; boolean hasBodyParam = false; - TypeArgMapper typeArgMapper = new TypeArgMapper(info.declaringClass(), index); + TypeArgMapper typeArgMapper = new TypeArgMapper(currentMethodInfo.declaringClass(), index); for (int i = 0; i < methodParameters.length; ++i) { Map anns = parameterAnnotations[i]; boolean encoded = anns.containsKey(ResteasyReactiveDotNames.ENCODED); - Type paramType = info.parameters().get(i); - String errorLocation = "method " + info + " on class " + info.declaringClass(); + Type paramType = currentMethodInfo.parameters().get(i); + String errorLocation = "method " + currentMethodInfo + " on class " + currentMethodInfo.declaringClass(); PARAM parameterResult = extractParameterInfo(currentClassInfo, actualEndpointInfo, existingConverters, additionalReaders, - anns, paramType, errorLocation, false, hasRuntimeConverters, pathParameters, info.parameterName(i), + anns, paramType, errorLocation, false, hasRuntimeConverters, pathParameters, + currentMethodInfo.parameterName(i), methodContext); suspended |= parameterResult.isSuspended(); sse |= parameterResult.isSse(); @@ -416,7 +417,8 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf if (type == ParameterType.BODY) { if (hasBodyParam) throw new RuntimeException( - "Resource method " + info + " can only have a single body parameter: " + info.parameterName(i)); + "Resource method " + currentMethodInfo + " can only have a single body parameter: " + + currentMethodInfo.parameterName(i)); hasBodyParam = true; } String elementType = parameterResult.getElementType(); @@ -444,7 +446,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf if (hasBodyParam) { throw new RuntimeException( "'@MultipartForm' cannot be used in a resource method that contains a body parameter. Offending method is '" - + info.declaringClass().name() + "#" + info.toString() + "'"); + + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo.toString() + "'"); } boolean validConsumes = false; if (consumes != null) { @@ -459,18 +461,18 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf if (!validConsumes) { throw new RuntimeException( "'@MultipartForm' can only be used on methods that annotated with '@Consumes(MediaType.MULTIPART_FORM_DATA)'. Offending method is '" - + info.declaringClass().name() + "#" + info.toString() + "'"); + + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo.toString() + "'"); } } - Type nonAsyncReturnType = getNonAsyncReturnType(info.returnType()); + Type nonAsyncReturnType = getNonAsyncReturnType(currentMethodInfo.returnType()); addWriterForType(additionalWriters, nonAsyncReturnType); - String[] produces = extractProducesConsumesValues(info.annotation(PRODUCES), classProduces); + String[] produces = extractProducesConsumesValues(currentMethodInfo.annotation(PRODUCES), classProduces); produces = applyDefaultProduces(produces, nonAsyncReturnType); String sseElementType = classSseElementType; - AnnotationInstance sseElementTypeAnnotation = info.annotation(REST_SSE_ELEMENT_TYPE); + AnnotationInstance sseElementTypeAnnotation = currentMethodInfo.annotation(REST_SSE_ELEMENT_TYPE); if (sseElementTypeAnnotation != null) { sseElementType = sseElementTypeAnnotation.value().asString(); } @@ -481,32 +483,25 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf sseElementType = defaultProducesForType[0]; } } - Set nameBindingNames = nameBindingNames(info, classNameBindings); - boolean blocking = defaultBlocking; - Map.Entry blockingAnnotation = getInheritableAnnotation(info, BLOCKING); - Map.Entry nonBlockingAnnotation = getInheritableAnnotation(info, - NON_BLOCKING); - if ((blockingAnnotation != null) && (nonBlockingAnnotation != null)) { - if (blockingAnnotation.getKey().kind() == AnnotationTarget.Kind.METHOD) { - // the most specific annotation was the @Blocking annotation on the method - blocking = true; - } else { - // the most specific annotation was the @NonBlocking annotation on the method - blocking = false; + Set nameBindingNames = nameBindingNames(currentMethodInfo, classNameBindings); + boolean blocking = isBlocking(currentMethodInfo, defaultBlocking); + // we want to allow "overriding" the blocking/non-blocking setting from an implementation class + // when the class defining the annotations is an interface + if (!actualEndpointInfo.equals(currentClassInfo) && Modifier.isInterface(currentClassInfo.flags())) { + MethodInfo actualMethodInfo = actualEndpointInfo.method(currentMethodInfo.name(), + currentMethodInfo.parameters().toArray(new Type[0])); + if (actualMethodInfo != null) { + blocking = isBlocking(actualMethodInfo, blocking); } - } else if ((blockingAnnotation != null) && (nonBlockingAnnotation == null)) { - blocking = true; - } else if ((nonBlockingAnnotation != null) && (blockingAnnotation == null)) { - blocking = false; } - ResourceMethod method = createResourceMethod(info, methodContext) + ResourceMethod method = createResourceMethod(currentMethodInfo, methodContext) .setHttpMethod(httpMethod == null ? null : httpAnnotationToMethod.get(httpMethod)) .setPath(methodPath) .setConsumes(consumes) .setProduces(produces) .setNameBindingNames(nameBindingNames) - .setName(info.name()) + .setName(currentMethodInfo.name()) .setBlocking(blocking) .setSuspended(suspended) .setSse(sse) @@ -514,19 +509,42 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf .setFormParamRequired(formParamRequired) .setMultipart(multipart) .setParameters(methodParameters) - .setSimpleReturnType(toClassName(info.returnType(), currentClassInfo, actualEndpointInfo, index)) + .setSimpleReturnType( + toClassName(currentMethodInfo.returnType(), currentClassInfo, actualEndpointInfo, index)) // FIXME: resolved arguments ? - .setReturnType(determineReturnType(info, typeArgMapper, currentClassInfo, actualEndpointInfo, index)); - handleAdditionalMethodProcessing((METHOD) method, currentClassInfo, info); + .setReturnType( + determineReturnType(currentMethodInfo, typeArgMapper, currentClassInfo, actualEndpointInfo, index)); + handleAdditionalMethodProcessing((METHOD) method, currentClassInfo, currentMethodInfo); if (resourceMethodCallback != null) { - resourceMethodCallback.accept(new AbstractMap.SimpleEntry<>(info, method)); + resourceMethodCallback.accept(new AbstractMap.SimpleEntry<>(currentMethodInfo, method)); } return method; } catch (Exception e) { - throw new RuntimeException("Failed to process method " + info.declaringClass().name() + "#" + info.toString(), e); + throw new RuntimeException("Failed to process method " + currentMethodInfo.declaringClass().name() + "#" + + currentMethodInfo.toString(), e); } } + private boolean isBlocking(MethodInfo info, boolean defaultValue) { + Map.Entry blockingAnnotation = getInheritableAnnotation(info, BLOCKING); + Map.Entry nonBlockingAnnotation = getInheritableAnnotation(info, + NON_BLOCKING); + if ((blockingAnnotation != null) && (nonBlockingAnnotation != null)) { + if (blockingAnnotation.getKey().kind() == AnnotationTarget.Kind.METHOD) { + // the most specific annotation was the @Blocking annotation on the method + return true; + } else { + // the most specific annotation was the @NonBlocking annotation on the method + return false; + } + } else if ((blockingAnnotation != null) && (nonBlockingAnnotation == null)) { + return true; + } else if ((nonBlockingAnnotation != null) && (blockingAnnotation == null)) { + return false; + } + return defaultValue; + } + protected void handleMultipart(ClassInfo multipartClassInfo) { }