Skip to content

Commit

Permalink
Merge pull request #1203 from MikeEdgar/issue-1201
Browse files Browse the repository at this point in the history
Include overridden methods/classes in JAX-RS resource method scan
  • Loading branch information
MikeEdgar authored Aug 4, 2022
2 parents 7fbdb4e + 6edfcc9 commit 0286b21
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,29 @@ protected void processPathParameters(ClassInfo resourceClass, MethodInfo resourc
}

protected void processOperationParameters(MethodInfo resourceMethod, ResourceParameters parameters) {
// Phase II - Read method argument @Parameter and framework's annotations
resourceMethod.annotations()
List<MethodInfo> candidateMethods = JandexUtil.ancestry(resourceMethod, scannerContext.getAugmentedIndex())
.entrySet()
.stream()
.filter(a -> !JandexUtil.equals(a.target(), resourceMethod))
.map(Map.Entry::getValue)
.filter(Objects::nonNull)
.collect(Collectors.toList());

/*
* Phase II - Read method arguments: @Parameter and framework's annotations
*
* Read the resource method and any method super classes/interfaces that it may override
*/
candidateMethods.stream()
.flatMap(m -> m.annotations().stream().filter(a -> !JandexUtil.equals(a.target(), m)))
.forEach(this::readAnnotatedType);

// Phase III - Read method @Parameter(s) annotations
resourceMethod.annotations()
.stream()
.filter(a -> JandexUtil.equals(a.target(), resourceMethod))
/*
* Phase III - Read @Parameter(s) annotations directly on the recourd method
*
* Read the resource method and any method super classes/interfaces that it may override
*/
candidateMethods.stream()
.flatMap(m -> m.annotations().stream().filter(a -> JandexUtil.equals(a.target(), m)))
.filter(a -> openApiParameterAnnotations.contains(a.name()))
.forEach(this::readParameterAnnotation);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -732,4 +733,50 @@ public static boolean isSupplier(AnnotationTarget target) {

return method.returnType().kind() != Type.Kind.VOID && method.parameterTypes().isEmpty();
}

public static Map<ClassInfo, MethodInfo> ancestry(MethodInfo method, AugmentedIndexView index) {
ClassInfo declaringClass = method.declaringClass();
Type resourceType = Type.create(declaringClass.name(), Type.Kind.CLASS);
Map<ClassInfo, Type> chain = inheritanceChain(index, declaringClass, resourceType);
Map<ClassInfo, MethodInfo> ancestry = new LinkedHashMap<>();

for (ClassInfo classInfo : chain.keySet()) {
ancestry.put(classInfo, null);

classInfo.methods()
.stream()
.filter(m -> !m.isSynthetic())
.filter(m -> isSameSignature(method, m))
.findFirst()
.ifPresent(m -> ancestry.put(classInfo, m));

interfaces(index, classInfo)
.stream()
.filter(type -> !TypeUtil.knownJavaType(type.name()))
.map(index::getClass)
.filter(Objects::nonNull)
.map(iface -> {
ancestry.put(iface, null);
return iface;
})
.flatMap(iface -> iface.methods().stream())
.filter(m -> isSameSignature(method, m))
.forEach(m -> ancestry.put(m.declaringClass(), m));
}

return ancestry;
}

public static List<MethodInfo> overriddenMethods(MethodInfo method, List<MethodInfo> candidates) {
return candidates.stream()
.filter(m -> !method.equals(m))
.filter(m -> isSameSignature(method, m))
.collect(Collectors.toList());
}

public static boolean isSameSignature(MethodInfo m1, MethodInfo m2) {
return Objects.equals(m1.name(), m2.name())
&& m1.parametersCount() == m2.parametersCount()
&& Objects.equals(m1.parameterTypes(), m2.parameterTypes());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package io.smallrye.openapi.jaxrs;

import static io.smallrye.openapi.runtime.util.JandexUtil.overriddenMethods;

import java.lang.reflect.Modifier;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
Expand Down Expand Up @@ -265,13 +268,20 @@ private void processResourceMethods(final AnnotationScannerContext context,

// Process exception mapper to auto generate api response based on method exceptions
Map<DotName, List<AnnotationInstance>> exceptionAnnotationMap = processExceptionMappers(context);
List<MethodInfo> methods = getResourceMethods(context, resourceClass);
Collections.reverse(methods);

for (MethodInfo methodInfo : getResourceMethods(context, resourceClass)) {
for (MethodInfo methodInfo : methods) {
final AtomicInteger resourceCount = new AtomicInteger(0);

JaxRsConstants.HTTP_METHODS
.stream()
.filter(methodInfo::hasAnnotation)
.filter(httpMethod -> {
if (methodInfo.hasAnnotation(httpMethod)) {
return true;
}
return overriddenMethods(methodInfo, methods).stream().anyMatch(m -> m.hasAnnotation(httpMethod));
})
.map(DotName::withoutPackagePrefix)
.map(PathItem.HttpMethod::valueOf)
.forEach(httpMethod -> {
Expand Down Expand Up @@ -428,12 +438,11 @@ private void processResourceMethod(final AnnotationScannerContext context,
JaxRsLogging.log.processingMethod(method.toString());

// Figure out the current @Produces and @Consumes (if any)
CurrentScannerInfo.setCurrentConsumes(getMediaTypes(method, JaxRsConstants.CONSUMES,
context.getConfig().getDefaultConsumes().orElse(OpenApiConstants.DEFAULT_MEDIA_TYPES.get()))
.orElse(null));
CurrentScannerInfo.setCurrentProduces(getMediaTypes(method, JaxRsConstants.PRODUCES,
context.getConfig().getDefaultProduces().orElse(OpenApiConstants.DEFAULT_MEDIA_TYPES.get()))
.orElse(null));
String[] defaultConsumes = context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES);
CurrentScannerInfo.setCurrentConsumes(getMediaTypes(context, method, JaxRsConstants.CONSUMES, defaultConsumes));

String[] defaultProduces = context.getConfig().getDefaultProduces().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES);
CurrentScannerInfo.setCurrentProduces(getMediaTypes(context, method, JaxRsConstants.PRODUCES, defaultProduces));

// Process any @Operation annotation
Optional<Operation> maybeOperation = processOperation(context, resourceClass, method);
Expand Down Expand Up @@ -507,24 +516,45 @@ private void processResourceMethod(final AnnotationScannerContext context,
}
}

static Optional<String[]> getMediaTypes(MethodInfo resourceMethod, Set<DotName> annotationName, String[] defaultValue) {
AnnotationInstance annotation = JandexUtil.getAnnotation(resourceMethod, annotationName);
/**
* Search for {@code annotationName} on {@code resourceMethod} or any of the methods it overrides. If
* not found, search for {@code annotationName} on {@code resourceMethod}'s containing class or any
* of its super-classes or interfaces.
*/
static String[] getMediaTypes(AnnotationScannerContext context, MethodInfo resourceMethod, Set<DotName> annotationName,
String[] defaultValue) {

return JandexUtil.ancestry(resourceMethod, context.getAugmentedIndex()).entrySet()
.stream()
.map(e -> getMediaTypeAnnotation(e.getKey(), e.getValue(), annotationName))
.filter(Objects::nonNull)
.map(annotation -> mediaTypeValue(annotation, defaultValue))
.findFirst()
.orElse(null);
}

static AnnotationInstance getMediaTypeAnnotation(ClassInfo clazz, MethodInfo method, Set<DotName> annotationName) {
AnnotationInstance annotation = null;

if (method != null) {
annotation = JandexUtil.getAnnotation(method, annotationName);
}

if (annotation == null) {
annotation = JandexUtil.getClassAnnotation(resourceMethod.declaringClass(), annotationName);
annotation = JandexUtil.getClassAnnotation(clazz, annotationName);
}

if (annotation != null) {
AnnotationValue annotationValue = annotation.value();
return annotation;
}

if (annotationValue != null) {
return Optional.of(flattenAndTrimMediaTypes(annotationValue.asStringArray()));
}
static String[] mediaTypeValue(AnnotationInstance mediaTypeAnnotation, String[] defaultValue) {
AnnotationValue annotationValue = mediaTypeAnnotation.value();

return Optional.of(defaultValue);
if (annotationValue != null) {
return flattenAndTrimMediaTypes(annotationValue.asStringArray());
}

return Optional.empty();
return defaultValue;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -237,7 +238,7 @@ protected String pathOf(AnnotationTarget target) {
path = JandexUtil.getClassAnnotation(target.asClass(), JaxRsConstants.PATH);
break;
case METHOD:
path = JandexUtil.getAnnotation(target.asMethod(), JaxRsConstants.PATH);
path = pathOf(target.asMethod());
break;
default:
break;
Expand All @@ -260,6 +261,18 @@ protected String pathOf(AnnotationTarget target) {
return "";
}

AnnotationInstance pathOf(MethodInfo method) {
return JandexUtil.ancestry(method, scannerContext.getAugmentedIndex())
.entrySet()
.stream()
.map(Map.Entry::getValue)
.filter(Objects::nonNull)
.map(m -> JandexUtil.getAnnotation(m, JaxRsConstants.PATH))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

@Override
protected boolean isSubResourceLocator(MethodInfo method) {
switch (method.returnType().kind()) {
Expand All @@ -277,8 +290,12 @@ protected boolean isSubResourceLocator(MethodInfo method) {

@Override
protected boolean isResourceMethod(MethodInfo method) {
return method.annotations()
return JandexUtil.ancestry(method, scannerContext.getAugmentedIndex())
.entrySet()
.stream()
.map(Map.Entry::getValue)
.filter(Objects::nonNull)
.flatMap(m -> m.annotations().stream())
.map(AnnotationInstance::name)
.anyMatch(JaxRsConstants.HTTP_METHODS::contains);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void testJavaxInheritedResourceMethod() throws IOException, JSONException {
test.io.smallrye.openapi.runtime.scanner.javax.Greetable.class,
test.io.smallrye.openapi.runtime.scanner.javax.Greetable.GreetingBean.class);

testInheritedResourceMethod(i);
testInheritedResourceMethod(i, "resource.inheritance.params.json");
}

@Test
Expand All @@ -44,16 +44,22 @@ void testJakartaInheritedResourceMethod() throws IOException, JSONException {
test.io.smallrye.openapi.runtime.scanner.jakarta.Greetable.class,
test.io.smallrye.openapi.runtime.scanner.jakarta.Greetable.GreetingBean.class);

testInheritedResourceMethod(i);
testInheritedResourceMethod(i, "resource.inheritance.params.json");
}

void testInheritedResourceMethod(Index i) throws IOException, JSONException {
@Test
void testJakartaInheritedResources() throws IOException, JSONException {
Index i = indexOf(test.io.smallrye.openapi.runtime.scanner.jakarta.ParameterDefaultValueInheritance.CLASSES);
testInheritedResourceMethod(i, "resource.inheritance.param-default-values.json");
}

void testInheritedResourceMethod(Index i, String expectedResource) throws IOException, JSONException {
OpenApiConfig config = emptyConfig();
IndexView filtered = new FilteredIndexView(i, config);
OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, filtered);
OpenAPI result = scanner.scan();
printToConsole(result);
assertJsonEquals("resource.inheritance.params.json", result);
assertJsonEquals(expectedResource, result);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package test.io.smallrye.openapi.runtime.scanner.jakarta;

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

public class ParameterDefaultValueInheritance {

public static final Class<?>[] CLASSES = {
InterfaceAlpha.class,
InterfaceBeta.class,
Resource1.class
};

interface InterfaceAlpha {
@GET
@Path("/alpha")
@Produces(MediaType.TEXT_PLAIN)
String getAlpha(@DefaultValue("1") @QueryParam("omega") int omega);
}

@Produces(MediaType.TEXT_XML)
interface InterfaceBeta extends InterfaceAlpha {
@Override
@GET
@Path("/alpha")
@Produces(MediaType.TEXT_PLAIN)
String getAlpha(@DefaultValue("10") @QueryParam("omega") int omega);

@GET
@Path("/beta")
String getBeta(@DefaultValue("true") @QueryParam("upsilon") boolean upsilon);
}

@Path("/1")
static class Resource1 implements InterfaceBeta {
@Override
public String getAlpha(int omega) {
return null;
}

@Override
public String getBeta(@DefaultValue("false") @QueryParam("upsilon") boolean upsilon) {
return null;
}
}
}
Loading

0 comments on commit 0286b21

Please sign in to comment.