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..099ae8ba5 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,13 +1,21 @@ 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.List; import java.util.Map; import java.util.Set; import org.eclipse.microprofile.openapi.models.Extensible; 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.OpenApiConfig; @@ -22,6 +30,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; @@ -105,19 +139,51 @@ private static boolean profileIncluded(OpenApiConfig config, Set profile } public String[] getDefaultConsumes(AnnotationScannerContext context, MethodInfo methodInfo) { - return context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + if (methodInfo.parametersCount() > 0) { + List parameters = methodInfo.parameters(); + for (MethodParameterInfo mip : parameters) { + if (isRequestBody(mip)) { + if (isStreaming(mip.type())) { + return context.getConfig().getDefaultStreamingConsumes() + .orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } else if (isPrimimive(mip.type())) { + return context.getConfig().getDefaultPrimitivesConsumes() + .orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + return context.getConfig().getDefaultConsumes().orElseGet(OpenApiConstants.DEFAULT_MEDIA_TYPES); + } + } + } + + return new String[] {}; } 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.kind().equals(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..94d3eae10 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 @@ -36,6 +36,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.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; @@ -990,7 +991,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 +1016,18 @@ 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; + } + + default boolean isRequestBody(MethodParameterInfo mip) { + return mip.annotations().isEmpty(); + } + /** * 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 f01d8e9cb..1dc0c2b00 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 @@ -439,10 +439,12 @@ private void processResourceMethod(final AnnotationScannerContext context, // Figure out the current @Produces and @Consumes (if any) String[] defaultConsumes = getDefaultConsumes(context, method); - context.setCurrentConsumes(getMediaTypes(context, method, JaxRsConstants.CONSUMES, defaultConsumes)); + 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); @@ -541,16 +543,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..3c4765502 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; @@ -290,9 +291,11 @@ private void processControllerMethod(final AnnotationScannerContext context, // Figure out the current @Produces and @Consumes (if any) String[] defaultConsumes = getDefaultConsumes(context, 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 @@ -383,10 +386,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..11f97f48f 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) { + 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. * @@ -234,9 +244,11 @@ private void processRouteMethod(final AnnotationScannerContext context, // Figure out the current @Produces and @Consumes (if any) String[] defaultConsumes = getDefaultConsumes(context, 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));