diff --git a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java index 13d056efb..eb03d8905 100644 --- a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java +++ b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java @@ -225,6 +225,14 @@ default Optional getDefaultPrimitivesConsumes() { return getConfigValue(OpenApiConstants.DEFAULT_CONSUMES_PRIMITIVES, String[].class, Optional::of, Optional::empty); } + default Optional getDefaultStreamingProduces() { + return getConfigValue(OpenApiConstants.DEFAULT_PRODUCES_STREAMING, String[].class, Optional::of, Optional::empty); + } + + default Optional getDefaultStreamingConsumes() { + return getConfigValue(OpenApiConstants.DEFAULT_CONSUMES_STREAMING, String[].class, Optional::of, Optional::empty); + } + default Optional allowNakedPathParameter() { return Optional.empty(); } diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/JDKConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/JDKConstants.java index 9c4d0ac02..83e58e5d4 100644 --- a/core/src/main/java/io/smallrye/openapi/api/constants/JDKConstants.java +++ b/core/src/main/java/io/smallrye/openapi/api/constants/JDKConstants.java @@ -8,6 +8,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import org.jboss.jandex.DotName; @@ -27,6 +28,7 @@ public class JDKConstants { public static final DotName DOTNAME_OPTIONAL_INT = DotName.createSimple(OptionalInt.class.getName()); public static final DotName DOTNAME_OPTIONAL_LONG = DotName.createSimple(OptionalLong.class.getName()); public static final DotName COMPLETION_STAGE_NAME = DotName.createSimple(CompletionStage.class.getName()); + public static final DotName COMPLETABLE_FUTURE_NAME = DotName.createSimple(CompletableFuture.class.getName()); public static final Set DOTNAME_OPTIONALS = Collections .unmodifiableSet(new HashSet<>(Arrays.asList(DOTNAME_OPTIONAL, diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java index eb92426cd..8397399bc 100644 --- a/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java +++ b/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java @@ -64,6 +64,8 @@ public final class OpenApiConstants { public static final String DEFAULT_CONSUMES = SMALLRYE_PREFIX + "defaultConsumes"; public static final String DEFAULT_PRODUCES_PRIMITIVES = SMALLRYE_PREFIX + "defaultPrimitivesProduces"; public static final String DEFAULT_CONSUMES_PRIMITIVES = SMALLRYE_PREFIX + "defaultPrimitivesConsumes"; + public static final String DEFAULT_PRODUCES_STREAMING = SMALLRYE_PREFIX + "defaultStreamingProduces"; + public static final String DEFAULT_CONSUMES_STREAMING = SMALLRYE_PREFIX + "defaultStreamingConsumes"; public static final String MAXIMUM_STATIC_FILE_SIZE = SMALLRYE_PREFIX + "maximumStaticFileSize"; public static final String AUTO_INHERITANCE = SMALLRYE_PREFIX + "auto-inheritance"; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java index e38b5400c..f03a66d9b 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java @@ -1,5 +1,11 @@ package io.smallrye.openapi.runtime.scanner.spi; +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -12,6 +18,7 @@ import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.constants.OpenApiConstants; +import io.smallrye.openapi.runtime.scanner.ResourceParameters; import io.smallrye.openapi.runtime.util.TypeUtil; /** @@ -22,6 +29,32 @@ public abstract class AbstractAnnotationScanner implements AnnotationScanner { private static final String EMPTY = ""; + private static final Set PRIMITIVE_OBJECTS = new HashSet<>(); + private static final Set STREAM_OBJECTS = new HashSet<>(); + + static { + PRIMITIVE_OBJECTS.add(DotName.createSimple(String.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Integer.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Short.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Long.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Float.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Double.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Boolean.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(Character.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(BigDecimal.class)); + PRIMITIVE_OBJECTS.add(DotName.createSimple(BigInteger.class)); + + STREAM_OBJECTS.add(DotName.createSimple(File.class)); + STREAM_OBJECTS.add(DotName.createSimple(Path.class)); + STREAM_OBJECTS.add(DotName.createSimple(InputStream.class)); + STREAM_OBJECTS.add(DotName.createSimple(Reader.class)); + STREAM_OBJECTS.add(DotName.createSimple(Byte.class)); + STREAM_OBJECTS.add(DotName.createSimple(byte[].class)); + STREAM_OBJECTS.add(DotName.createSimple("io.vertx.core.file.AsyncFile")); + STREAM_OBJECTS.add(DotName.createSimple("io.vertx.core.buffer.Buffer")); + + } + protected String currentAppPath = EMPTY; private String contextRoot = EMPTY; @@ -104,20 +137,51 @@ private static boolean profileIncluded(OpenApiConfig config, Set profile return config.getScanProfiles().stream().anyMatch(profiles::contains); } - public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo) { - return context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + @Override + public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo, + final ResourceParameters params) { + Type requestBodyType = getRequestBodyParameterClassType(context, methodInfo, params); + + if (requestBodyType != null) { + if (isStreaming(requestBodyType)) { + return context.getConfig().getDefaultStreamingConsumes() + .orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } else if (isPrimimive(requestBodyType)) { + return context.getConfig().getDefaultPrimitivesConsumes() + .orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + return context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + return new String[] {}; } + @Override public String[] getDefaultProduces(AnnotationScannerContext context, MethodInfo methodInfo) { - if (isPrimimive(methodInfo.returnType())) { + if (isStreaming(methodInfo.returnType())) { + return context.getConfig().getDefaultStreamingProduces().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } else if (isPrimimive(methodInfo.returnType())) { return context.getConfig().getDefaultPrimitivesProduces().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); } return context.getConfig().getDefaultProduces().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); } private boolean isPrimimive(Type type) { - return type.kind().equals(Type.Kind.PRIMITIVE) - || type.name().equals(DotName.createSimple(String.class)) - || (TypeUtil.isWrappedType(type) && isPrimimive(TypeUtil.unwrapType(type))); + if (type != null) { + return type.kind().equals(Type.Kind.PRIMITIVE) + || PRIMITIVE_OBJECTS.contains(type.name()) + || (isWrapperType(type) && isPrimimive(unwrapType(type))) + || (TypeUtil.isWrappedType(type) && isPrimimive(TypeUtil.unwrapType(type))); + } + return false; + } + + private boolean isStreaming(Type type) { + if (type != null) { + return (type.kind().equals(Type.Kind.PRIMITIVE) && type.name().equals(DotName.createSimple(byte.class))) + || STREAM_OBJECTS.contains(type.name()) + || (isWrapperType(type) && isStreaming(unwrapType(type))) + || (TypeUtil.isWrappedType(type) && isStreaming(TypeUtil.unwrapType(type))); + } + return false; } } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java index d5626f17a..9a731e041 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java @@ -101,7 +101,7 @@ public boolean containsScannerAnnotations(List instances, // Allow runtimes to set the context root path public void setContextRoot(String path); - public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo); + public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo, ResourceParameters params); public String[] getDefaultProduces(AnnotationScannerContext context, MethodInfo methodInfo); @@ -990,7 +990,7 @@ && getConsumes(context) != null) { } if (schema != null) { - ModelUtil.setRequestBodySchema(requestBody, schema, getConsumes(context)); + ModelUtil.setRequestBodySchema(requestBody, schema, getConsumesForRequestBody(context)); } if (requestBody.getRequired() == null && TypeUtil.isOptional(requestBodyType)) { @@ -1015,6 +1015,14 @@ default String[] getConsumes(final AnnotationScannerContext context) { return currentConsumes; } + default String[] getConsumesForRequestBody(final AnnotationScannerContext context) { + String[] currentConsumes = context.getCurrentConsumes(); + if (currentConsumes == null || currentConsumes.length == 0) { + currentConsumes = context.getDefaultConsumes(); + } + return currentConsumes; + } + /** * Go through the method parameters looking for one that is not a Kotlin Continuation, * is not annotated with a jax-rs/spring annotation, and is not a known path parameter. diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java index 97d82a2d6..b85ef62eb 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java @@ -48,6 +48,8 @@ public class AnnotationScannerContext { private final Set jsonViews = new LinkedHashSet<>(); private String[] currentConsumes; private String[] currentProduces; + private String[] defaultConsumes; + private String[] defaultProduces; private Optional currentScanner = Optional.empty(); private final SchemaRegistry schemaRegistry; private final JavaSecurityProcessor javaSecurityProcessor; @@ -149,6 +151,22 @@ public void setCurrentProduces(String[] currentProduces) { this.currentProduces = currentProduces; } + public String[] getDefaultConsumes() { + return defaultConsumes; + } + + public void setDefaultConsumes(String[] defaultConsumes) { + this.defaultConsumes = defaultConsumes; + } + + public String[] getDefaultProduces() { + return defaultProduces; + } + + public void setDefaultProduces(String[] defaultProduces) { + this.defaultProduces = defaultProduces; + } + public Optional getCurrentScanner() { return currentScanner; } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java index 93e3a7716..ca4076c2b 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java @@ -309,6 +309,8 @@ public class TypeUtil { wrapperTypes.addAll(JaxbConstants.JAXB_ELEMENT); wrapperTypes.add(MutinyConstants.UNI_TYPE.name()); + wrapperTypes.add(JDKConstants.COMPLETION_STAGE_NAME); + wrapperTypes.add(JDKConstants.COMPLETABLE_FUTURE_NAME); } private static void indexOptional(Indexer indexer, String className, ClassLoader contextLoader) { 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 e97c50536..7ec0f2d31 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 @@ -446,11 +446,13 @@ private void processResourceMethod(final AnnotationScannerContext context, JaxRsLogging.log.processingMethod(method.toString()); // Figure out the current @Produces and @Consumes (if any) - String[] defaultConsumes = getDefaultConsumes(context, method); - context.setCurrentConsumes(getMediaTypes(context, method, JaxRsConstants.CONSUMES, defaultConsumes)); + String[] defaultConsumes = getDefaultConsumes(context, method, getResourceParameters(context, resourceClass, method)); + context.setDefaultConsumes(defaultConsumes); + context.setCurrentConsumes(getMediaTypes(context, method, JaxRsConstants.CONSUMES, defaultConsumes).orElse(null)); String[] defaultProduces = getDefaultProduces(context, method); - context.setCurrentProduces(getMediaTypes(context, method, JaxRsConstants.PRODUCES, defaultProduces)); + context.setDefaultProduces(defaultProduces); + context.setCurrentProduces(getMediaTypes(context, method, JaxRsConstants.PRODUCES, defaultProduces).orElse(null)); // Process any @Operation annotation Optional maybeOperation = processOperation(context, resourceClass, method); @@ -463,10 +465,7 @@ private void processResourceMethod(final AnnotationScannerContext context, processOperationTags(context, method, context.getOpenApi(), resourceTags, operation); // Process @Parameter annotations. - Function reader = t -> ParameterReader.readParameter(context, t); - - ResourceParameters params = JaxRsParameterProcessor.process(context, currentAppPath, resourceClass, method, - reader, context.getExtensions()); + ResourceParameters params = getResourceParameters(context, resourceClass, method); List operationParams = params.getOperationParameters(); operation.setParameters(operationParams); if (locatorPathParameters != null && operationParams != null) { @@ -528,6 +527,13 @@ private void processResourceMethod(final AnnotationScannerContext context, } } + private ResourceParameters getResourceParameters(final AnnotationScannerContext context, + final ClassInfo resourceClass, final MethodInfo method) { + Function reader = t -> ParameterReader.readParameter(context, t); + return JaxRsParameterProcessor.process(context, currentAppPath, resourceClass, method, + reader, context.getExtensions()); + } + /** * Remove from the list of locator parameters and parameter present in the list of operation parameters. * Parameters are considered the same if they have the same value for name and {@code in}. @@ -549,16 +555,15 @@ static List excludeOperationParameters(List locatorParams, * 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) { + static Optional getMediaTypes(AnnotationScannerContext context, MethodInfo resourceMethod, + Set annotationName, String[] defaultValue) { return context.getAugmentedIndex().ancestry(resourceMethod).entrySet() .stream() .map(e -> getMediaTypeAnnotation(e.getKey(), e.getValue(), annotationName)) .filter(Objects::nonNull) .map(annotation -> mediaTypeValue(annotation, defaultValue)) - .findFirst() - .orElse(null); + .findFirst(); } static AnnotationInstance getMediaTypeAnnotation(ClassInfo clazz, MethodInfo method, Set annotationName) { diff --git a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java index 3de75efc9..aa01f8de0 100644 --- a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java +++ b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java @@ -20,6 +20,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import io.smallrye.openapi.api.constants.OpenApiConstants; @@ -289,10 +290,12 @@ private void processControllerMethod(final AnnotationScannerContext context, SpringLogging.log.processingMethod(method.toString()); // Figure out the current @Produces and @Consumes (if any) - String[] defaultConsumes = getDefaultConsumes(context, method); + String[] defaultConsumes = getDefaultConsumes(context, method, getResourceParameters(context, resourceClass, method)); + context.setDefaultConsumes(defaultConsumes); context.setCurrentConsumes(getMediaTypes(method, SpringConstants.MAPPING_CONSUMES, defaultConsumes).orElse(null)); String[] defaultProduces = getDefaultProduces(context, method); + context.setDefaultProduces(defaultProduces); context.setCurrentProduces(getMediaTypes(method, SpringConstants.MAPPING_PRODUCES, defaultProduces).orElse(null)); // Process any @Operation annotation @@ -307,10 +310,7 @@ private void processControllerMethod(final AnnotationScannerContext context, // Process @Parameter annotations. PathItem pathItem = new PathItemImpl(); - Function reader = t -> ParameterReader.readParameter(context, t); - ResourceParameters params = SpringParameterProcessor.process(context, currentAppPath, resourceClass, - method, reader, - context.getExtensions()); + ResourceParameters params = getResourceParameters(context, resourceClass, method); operation.setParameters(params.getOperationParameters()); pathItem.setParameters(ListUtil.mergeNullableLists(locatorPathParameters, params.getPathItemParameters())); @@ -360,6 +360,15 @@ private void processControllerMethod(final AnnotationScannerContext context, } } + private ResourceParameters getResourceParameters(final AnnotationScannerContext context, + final ClassInfo resourceClass, + final MethodInfo method) { + Function reader = t -> ParameterReader.readParameter(context, t); + return SpringParameterProcessor.process(context, currentAppPath, resourceClass, + method, reader, + context.getExtensions()); + } + static Optional getMediaTypes(MethodInfo resourceMethod, String property, String[] defaultValue) { Set annotationNames = new HashSet<>(SpringConstants.HTTP_METHODS); annotationNames.add(SpringConstants.REQUEST_MAPPING); @@ -383,10 +392,12 @@ static Optional getMediaTypes(MethodInfo resourceMethod, String proper if (annotationValue != null) { return Optional.of(annotationValue.asStringArray()); } - - return Optional.of(defaultValue); } return Optional.empty(); } + + public boolean isRequestBody(MethodParameterInfo mip) { + return mip.annotations().isEmpty() || Annotations.hasAnnotation(mip, SpringConstants.REQUEST_BODY); + } } diff --git a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java index 885b4ad32..b09dfdcbd 100644 --- a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java +++ b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java @@ -212,6 +212,16 @@ private void processRouteMethods(final AnnotationScannerContext context, locatorPathParameters))); } + @Override + public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo, ResourceParameters params) { + return context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + + @Override + public String[] getDefaultProduces(AnnotationScannerContext context, MethodInfo methodInfo) { + return context.getConfig().getDefaultProduces().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + /** * Process a single Vert.x method to produce an OpenAPI Operation. * @@ -233,10 +243,12 @@ private void processRouteMethod(final AnnotationScannerContext context, VertxLogging.log.processingMethod(method.toString()); // Figure out the current @Produces and @Consumes (if any) - String[] defaultConsumes = getDefaultConsumes(context, method); + String[] defaultConsumes = getDefaultConsumes(context, method, getResourceParameters(context, resourceClass, method)); + context.setDefaultConsumes(defaultConsumes); context.setCurrentConsumes(getMediaTypes(method, VertxConstants.ROUTE_CONSUMES, defaultConsumes).orElse(null)); String[] defaultProduces = getDefaultProduces(context, method); + context.setDefaultProduces(defaultProduces); context.setCurrentProduces(getMediaTypes(method, VertxConstants.ROUTE_PRODUCES, defaultProduces).orElse(null)); @@ -252,11 +264,7 @@ private void processRouteMethod(final AnnotationScannerContext context, // Process @Parameter annotations. PathItem pathItem = new PathItemImpl(); - Function reader = t -> ParameterReader.readParameter(context, t); - - ResourceParameters params = VertxParameterProcessor.process(context, currentAppPath, resourceClass, - method, reader, - context.getExtensions()); + ResourceParameters params = getResourceParameters(context, resourceClass, method); operation.setParameters(params.getOperationParameters()); pathItem.setParameters(ListUtil.mergeNullableLists(locatorPathParameters, params.getPathItemParameters())); @@ -306,6 +314,15 @@ private void processRouteMethod(final AnnotationScannerContext context, } } + private ResourceParameters getResourceParameters(final AnnotationScannerContext context, + final ClassInfo resourceClass, + final MethodInfo method) { + Function reader = t -> ParameterReader.readParameter(context, t); + return VertxParameterProcessor.process(context, currentAppPath, resourceClass, + method, reader, + context.getExtensions()); + } + /** * Determine if the route method should be scanned or skipped. *