diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index baf5c9734..b4ef61af7 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -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 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); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java index 1a44c7fe8..231ca3ba0 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java @@ -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; @@ -732,4 +733,50 @@ public static boolean isSupplier(AnnotationTarget target) { return method.returnType().kind() != Type.Kind.VOID && method.parameterTypes().isEmpty(); } + + public static Map ancestry(MethodInfo method, AugmentedIndexView index) { + ClassInfo declaringClass = method.declaringClass(); + Type resourceType = Type.create(declaringClass.name(), Type.Kind.CLASS); + Map chain = inheritanceChain(index, declaringClass, resourceType); + Map 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 overriddenMethods(MethodInfo method, List 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()); + } } diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java index 0ba02f00f..55d991388 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java @@ -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; @@ -265,13 +268,20 @@ private void processResourceMethods(final AnnotationScannerContext context, // Process exception mapper to auto generate api response based on method exceptions Map> exceptionAnnotationMap = processExceptionMappers(context); + List 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 -> { @@ -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 maybeOperation = processOperation(context, resourceClass, method); @@ -507,24 +516,45 @@ private void processResourceMethod(final AnnotationScannerContext context, } } - static Optional getMediaTypes(MethodInfo resourceMethod, Set 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 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 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; } /** diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java index 5ccbd6fe1..6a3e89ce5 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java @@ -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; @@ -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; @@ -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()) { @@ -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); } diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ResourceInheritanceTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ResourceInheritanceTests.java index 6b9f6b954..f638ca985 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ResourceInheritanceTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ResourceInheritanceTests.java @@ -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 @@ -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); } } diff --git a/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterDefaultValueInheritance.java b/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterDefaultValueInheritance.java new file mode 100644 index 000000000..722e4f2db --- /dev/null +++ b/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterDefaultValueInheritance.java @@ -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; + } + } +} diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.inheritance.param-default-values.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.inheritance.param-default-values.json new file mode 100644 index 000000000..f30a7aaf9 --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.inheritance.param-default-values.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.3", + "paths": { + "/1/alpha": { + "get": { + "parameters": [ + { + "name": "omega", + "in": "query", + "schema": { + "format": "int32", + "default": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/1/beta": { + "get": { + "parameters": [ + { + "name": "upsilon", + "in": "query", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/xml": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +}