Skip to content

Commit

Permalink
Allow using @Blocking on implementation in addition to interface for …
Browse files Browse the repository at this point in the history
…JAX-RS Resources

Fixes: quarkusio#15940
  • Loading branch information
geoand authored and juazugas committed Apr 8, 2021
1 parent 19bfa02 commit d1cf5ea
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -373,40 +373,41 @@ private boolean hasProperModifiers(MethodInfo info) {
}

private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo,
String[] classProduces, String[] classConsumes, Set<String> classNameBindings, DotName httpMethod, MethodInfo info,
String methodPath,
Set<String> classPathParameters, String classSseElementType) {
String[] classProduces, String[] classConsumes, Set<String> classNameBindings, DotName httpMethod,
MethodInfo currentMethodInfo,
String methodPath, Set<String> classPathParameters, String classSseElementType) {
try {
Map<String, Object> methodContext = new HashMap<>();
Set<String> pathParameters = new HashSet<>(classPathParameters);
URLUtils.parsePathParameters(methodPath, pathParameters);
Map<DotName, AnnotationInstance>[] parameterAnnotations = new Map[info.parameters().size()];
MethodParameter[] methodParameters = new MethodParameter[info.parameters()
Map<DotName, AnnotationInstance>[] 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<DotName, AnnotationInstance> 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();
Expand All @@ -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();
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}
Expand All @@ -481,52 +483,68 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf
sseElementType = defaultProducesForType[0];
}
}
Set<String> nameBindingNames = nameBindingNames(info, classNameBindings);
boolean blocking = defaultBlocking;
Map.Entry<AnnotationTarget, AnnotationInstance> blockingAnnotation = getInheritableAnnotation(info, BLOCKING);
Map.Entry<AnnotationTarget, AnnotationInstance> 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<String> 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)
.setSseElementType(sseElementType)
.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<AnnotationTarget, AnnotationInstance> blockingAnnotation = getInheritableAnnotation(info, BLOCKING);
Map.Entry<AnnotationTarget, AnnotationInstance> 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) {

}
Expand Down

0 comments on commit d1cf5ea

Please sign in to comment.