From f586024fd1af4492369a82c62d28c60331a0d9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Thu, 28 Jul 2022 16:25:11 +0200 Subject: [PATCH] RESTEasy Reactive: rewoked multipart form parameters #22205 - Merged support into @BeanParam handling - Deprecated @MultipartForm - Auto-detect @BeanParam classes, annotation now optional - Use ParamConverter instead of MessageBodyReader for plain text multiparts - Auto-default to Accept: urlencoded or multipart depending on types of @FormParam present - Support multipart form elements as endpoint parameters and fields - Breaking: need @RestForm(FileUpload.ALL) for getting all uploads --- .../main/asciidoc/rest-client-reactive.adoc | 59 +- docs/src/main/asciidoc/resteasy-reactive.adoc | 130 ++-- .../JaxrsClientReactiveProcessor.java | 504 +++++++-------- .../reactive/runtime/RestClientBase.java | 133 ++++ .../ParameterContainersBuildItem.java | 20 + .../ResteasyReactiveCommonProcessor.java | 12 + .../deployment/test/MultipartResource.java | 43 +- .../deployment/test/MultipartTest.java | 73 +++ .../jaxb/deployment/test/MultipartTest.java | 28 +- .../QuarkusMultipartParamHandler.java | 68 -- .../deployment/ResteasyReactiveProcessor.java | 14 +- .../ErroneousFieldMultipartInputTest.java | 59 -- .../multipart/FormDataWithAllUploads.java | 2 +- .../test/multipart/InvalidEncodingTest.java | 4 +- ...ltFormAttributeMultipartFormInputTest.java | 4 +- .../MalformedMultipartInputTest.java | 4 +- .../test/multipart/MultipartInputTest.java | 62 ++ .../MultipartInputWithAllUploadsTest.java | 21 + .../test/multipart/MultipartResource.java | 63 +- .../MultipartResourceWithAllUploads.java | 29 +- .../multipart/OtherMultipartResource.java | 5 +- ...geFormAttributeMultipartFormInputTest.java | 4 +- .../multipart/MultipartDetectionTest.java | 152 +++++ .../reactive/multipart/MultipartResource.java | 24 + .../processor/beanparam/BeanParamItem.java | 8 +- .../processor/beanparam/BeanParamParser.java | 50 +- .../processor/beanparam/FormParamItem.java | 29 +- .../scanning/ClientEndpointIndexer.java | 13 +- .../common/processor/EndpointIndexer.java | 114 ++-- ...easyReactiveParameterContainerScanner.java | 29 + .../resteasy/reactive/MultipartForm.java | 8 + .../org/jboss/resteasy/reactive/PartType.java | 5 +- .../common/model/MethodParameter.java | 13 +- .../reactive/common/model/ResourceMethod.java | 13 +- .../reactive/common/util/types/Types.java | 23 + .../reactive/multipart/FileUpload.java | 6 + .../ResteasyReactiveDeploymentManager.java | 4 + .../processor/ServerEndpointIndexer.java | 38 +- ...sformedFieldInjectionIndexerExtension.java | 5 +- ...neratedMultipartParamIndexerExtension.java | 61 -- .../multipart/MultipartFeature.java | 2 - .../MultipartPopulatorGenerator.java | 585 ------------------ .../scanning/ClassInjectorTransformer.java | 468 +++++++++++++- .../core/multipart/MultipartSupport.java | 147 ++++- .../MultipartFormParamExtractor.java | 99 +++ .../parameters/converters/ArrayConverter.java | 1 + .../parameters/converters/ListConverter.java | 1 + .../converters/OptionalConverter.java | 1 + .../converters/RuntimeResolvedConverter.java | 8 + .../parameters/converters/SetConverter.java | 1 + .../converters/SortedSetConverter.java | 1 + .../startup/RuntimeResourceDeployment.java | 75 ++- .../server/model/ServerMethodParameter.java | 5 +- .../server/model/ServerResourceMethod.java | 4 +- .../ErroneousFieldMultipartInputTest.java | 58 -- .../multipart/FormDataWithAllUploads.java | 2 +- .../test/multipart/InvalidEncodingTest.java | 4 +- ...ltFormAttributeMultipartFormInputTest.java | 4 +- .../test/multipart/MultipartResource.java | 6 +- .../MultipartResourceWithAllUploads.java | 4 +- .../client/multipart/MultipartClient.java | 81 +++ .../client/multipart/MultipartResource.java | 150 +++++ .../multipart/MultipartResourceTest.java | 95 +++ 63 files changed, 2397 insertions(+), 1341 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ParameterContainersBuildItem.java delete mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartParamHandler.java delete mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/ErroneousFieldMultipartInputTest.java create mode 100644 extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java create mode 100644 extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResource.java create mode 100644 independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java delete mode 100644 independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/GeneratedMultipartParamIndexerExtension.java delete mode 100644 independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/MultipartFormParamExtractor.java delete mode 100644 independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/ErroneousFieldMultipartInputTest.java diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index ecd65dcc1ca52..e2f3200065220 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -825,35 +825,42 @@ REST Client Reactive support multipart messages. REST Client Reactive allows sending data as multipart forms. This way you can for example send files efficiently. -To send data as a multipart form, you need to create a class that would encapsulate all the fields -to be sent, e.g. +To send data as a multipart form, you can just use the regular `@RestForm` (or `@FormParam`) annotations: [source, java] ---- -public class FormDto { - @FormParam("file") - @PartType(MediaType.APPLICATION_OCTET_STREAM) - public File file; - - @FormParam("otherField") - @PartType(MediaType.TEXT_PLAIN) - public String textProperty; -} + @POST + @Path("/binary") + String sendMultipart(@RestForm File file, @RestForm String otherField); ---- -The method that sends a form needs to specify multipart form data as the consumed media type, e.g. +Parameters specified as `File`, `Path`, `byte[]` or `Buffer` are sent as files and default to the +`application/octet-stream` MIME type. Other `@RestForm` parameter types default to the `text/plain` +MIME type. You can override these defaults with the `@PartType` annotation. + +Naturally, you can also group these parameters into a containing class: + [source, java] ---- + public static class Parameters { + @RestForm + File file; + + @RestForm + String otherField; + } + @POST - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.TEXT_PLAIN) @Path("/binary") - String sendMultipart(@MultipartForm FormDto data); + String sendMultipart(Parameters parameters); ---- -Fields specified as `File`, `Path`, `byte[]` or `Buffer` are sent as files; as binary files for -`@PartType(MediaType.APPLICATION_OCTET_STREAM)`, as text files for other content types. -Other fields are sent as form attributes. +Any `@RestForm` parameter of the type `File`, `Path`, `byte[]` or `Buffer`, as well as any +annotated with `@PartType` automatically imply a `@Consumes(MediaType.MULTIPART_FORM_DATA)` +on the method if there is no `@Consumes` present. + +NOTE: If there are `@RestForm` parameters that are not multipart-implying, then +`@Consumes(MediaType.APPLICATION_FORM_URLENCODED)` is implied. There are a few modes in which the form data can be encoded. By default, Rest Client Reactive uses RFC1738. @@ -865,6 +872,20 @@ by specifying `quarkus.rest-client.multipart-post-encoder-mode` in your clients created with the `@RegisterRestClient` annotation. All the available modes are described in the link:https://netty.io/4.1/api/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.EncoderMode.html[Netty documentation] +You can also send JSON multiparts by specifying the `@PartType` annotation: + +[source, java] +---- + public static class Person { + public String firstName; + public String lastName; + } + + @POST + @Path("/json") + String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person); +---- + === Receiving Multipart Messages REST Client Reactive also supports receiving multipart messages. As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g. @@ -890,7 +911,7 @@ Then, create an interface method that corresponds to the call and make it return @GET @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/get-file") - FormDto data sendMultipart(); + FormDto data receiveMultipart(); ---- At the moment, multipart response support is subject to the following limitations: diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 2280368ece3a6..2b2609cfeef64 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -306,12 +306,13 @@ public class Endpoint { @RestHeader("X-Cheese-Secret-Handshake") String secretHandshake, @RestForm String smell) { - return type + "/" + variant + "/" + age + "/" + level + "/" + secretHandshake + "/" + smell; + return type + "/" + variant + "/" + age + "/" + level + "/" + + secretHandshake + "/" + smell; } } ---- -NOTE: the link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestPath.html[`@RestPath`] +NOTE: The link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestPath.html[`@RestPath`] annotation is optional: any parameter whose name matches an existing URI template variable will be automatically assumed to have link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestPath.html[`@RestPath`]. @@ -337,6 +338,58 @@ quarkus.log.category."org.jboss.resteasy.reactive.server.handlers.ParameterHandl ---- ==== +==== Grouping parameters in a custom class +[[parameter-grouping]] + +You can group your request parameters in a container class instead of declaring them as method parameters to you endpoint, +so we can rewrite the previous example like this: + +[source,java] +---- +package org.acme.rest; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestCookie; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestHeader; +import org.jboss.resteasy.reactive.RestMatrix; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +@Path("/cheeses/{type}") +public class Endpoint { + + public static class Parameters { + @RestPath + String type; + + @RestMatrix + String variant; + + @RestQuery + String age; + + @RestCookie + String level; + + @RestHeader("X-Cheese-Secret-Handshake") + String secretHandshake; + + @RestForm + String smell; + } + + @POST + public String allParams(Parameters parameters) { + return parameters.type + "/" + parameters.variant + "/" + parameters.age + + "/" + parameters.level + "/" + parameters.secretHandshake + + "/" + parameters.smell; + } +} +---- + === Declaring URI parameters [[uri-parameters]] @@ -424,35 +477,44 @@ NOTE: You can add support for more <>. [[multipart]] === Handling Multipart Form data -To handle HTTP requests that have `multipart/form-data` as their content type, RESTEasy Reactive introduces the -link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/MultipartForm.html[`@MultipartForm`] annotation. +To handle HTTP requests that have `multipart/form-data` as their content type, you can use the regular +link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestForm.html[`@RestForm`] annotation, but we have special types +that allow you to access the parts as files or as entities. Let us look at an example of its use. -Assuming an HTTP request containing a file upload and a form value containing a string description need to be handled, we could write a POJO -that will hold this information like so: +Assuming an HTTP request containing a file upload, a JSON entity and a form value containing a string description, we could write +the following endpoint: [source,java] ---- +import javax.ws.rs.POST; +import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; -public class FormData { - - @RestForm - @PartType(MediaType.TEXT_PLAIN) - public String description; - - @RestForm("image") - public FileUpload file; +@Path("multipart") +public class MultipartResource { + public static class Person { + public String firstName; + public String lastName; + } + + @POST + public void multipart(@RestForm String description, + @RestForm("image") FileUpload file, + @RestForm @PartType(MediaType.APPLICATION_JSON) Person person) { + // do something + } } ---- -The `name` field will contain the data contained in the part of HTTP request called `description` (because +The `description` parameter will contain the data contained in the part of HTTP request called `description` (because link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestForm.html[`@RestForm`] does not define a value, the field name is used), -while the `file` field will contain data about the uploaded file in the `image` part of HTTP request. +while the `file` parameter will contain data about the uploaded file in the `image` part of HTTP request, and +the `person` parameter will read the `Person` entity using the `JSON` <>. The size of every part in a multipart request must conform to the value of `quarkus.http.limits.max-form-attribute-size`, for which the default is 2048 bytes. Any request with a part size exceeding this configuration will result in HTTP status code 413. @@ -460,41 +522,13 @@ Any request with a part size exceeding this configuration will result in HTTP st NOTE: link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/multipart/FileUpload.html[`FileUpload`] provides access to various metadata of the uploaded file. If however all you need is a handle to the uploaded file, `java.nio.file.Path` or `java.io.File` could be used. -NOTE: When access to all uploaded files without specifying the form names is needed, RESTEasy Reactive allows the use of `@RestForm List`, where it is important to **not** set a name for the link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/RestForm.html[`@RestForm`] annotation. +If you need access to all uploaded files for all parts regardless of their names, you can do it with `@RestForm(FileUpload.ALL) List`. NOTE: link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/PartType.html[`@PartType`] is used to aid -in deserialization of the corresponding part of the request into the desired Java type. It is very useful when -for example the corresponding body part is JSON and needs to be converted to a POJO. - -This POJO could be used in a Resource method like so: - -[source,java] ----- -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import org.jboss.resteasy.reactive.MultipartForm; - -@Path("multipart") -public class Endpoint { - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Path("form") - public String form(@MultipartForm FormData formData) { - // return something - } -} ----- - -The use of link:{resteasy-reactive-common-api}/org/jboss/resteasy/reactive/MultipartForm.html[`@MultipartForm`] as -method parameter makes RESTEasy Reactive handle the request as a multipart form request. +in deserialization of the corresponding part of the request into the desired Java type. It is only required if +you need to use <> for that particular parameter. -TIP: The use of `@MultipartForm` is actually unnecessary as RESTEasy Reactive can infer this information from the use of `@Consumes(MediaType.MULTIPART_FORM_DATA)` +NOTE: Just like for any other request parameter type, you can also group them into a <>. WARNING: When handling file uploads, it is very important to move the file to permanent storage (like a database, a dedicated file system or a cloud storage) in your code that handles the POJO. Otherwise, the file will no longer be accessible when the request terminates. diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index edf3f2cbe5885..7e3b8759fcd92 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletionStage; @@ -154,6 +153,7 @@ import io.quarkus.jaxrs.client.reactive.runtime.ToObjectArray; import io.quarkus.jaxrs.client.reactive.runtime.impl.MultipartResponseDataBase; import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem; +import io.quarkus.resteasy.reactive.common.deployment.ParameterContainersBuildItem; import io.quarkus.resteasy.reactive.common.deployment.QuarkusFactoryCreator; import io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames; import io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem; @@ -171,6 +171,12 @@ public class JaxrsClientReactiveProcessor { + private static final String MULTI_BYTE_SIGNATURE = "L" + Multi.class.getName().replace('.', '/') + ";"; + private static final String FILE_SIGNATURE = "L" + File.class.getName().replace('.', '/') + ";"; + private static final String PATH_SIGNATURE = "L" + java.nio.file.Path.class.getName().replace('.', '/') + ";"; + private static final String BUFFER_SIGNATURE = "L" + Buffer.class.getName().replace('.', '/') + ";"; + private static final String BYTE_ARRAY_SIGNATURE = "[B"; + private static final Logger log = Logger.getLogger(JaxrsClientReactiveProcessor.class); private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); @@ -245,7 +251,8 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, BuildProducer bytecodeTransformerBuildItemBuildProducer, List defaultConsumes, List defaultProduces, - List disableSmartDefaultProduces) { + List disableSmartDefaultProduces, + List parameterContainersBuildItems) { String defaultConsumesType = defaultMediaType(defaultConsumes, MediaType.APPLICATION_OCTET_STREAM); String defaultProducesType = defaultMediaType(defaultProduces, MediaType.TEXT_PLAIN); @@ -255,6 +262,14 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, messageBodyWriterBuildItems, messageBodyReaderOverrideBuildItems, messageBodyWriterOverrideBuildItems, beanContainerBuildItem, applicationResultBuildItem, serialisers, RuntimeType.CLIENT); + Set scannedParameterContainers = new HashSet<>(); + + for (ParameterContainersBuildItem parameterContainersBuildItem : parameterContainersBuildItems) { + scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); + } + reflectiveClassBuildItemBuildProducer.produce(new ReflectiveClassBuildItem(false, true, + scannedParameterContainers.stream().map(name -> name.toString()).collect(Collectors.toSet()) + .toArray(new String[0]))); if (resourceScanningResultBuildItem.isEmpty() || resourceScanningResultBuildItem.get().getResult().getClientInterfaces().isEmpty()) { @@ -271,6 +286,7 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, .setIndex(index) .setApplicationIndex(applicationIndexBuildItem.getIndex()) .setExistingConverters(new HashMap<>()) + .addParameterContainerTypes(scannedParameterContainers) .setScannedResourcePaths(result.getScannedResourcePaths()) .setConfig(createRestReactiveConfig(config)) .setAdditionalReaders(additionalReaders) @@ -840,7 +856,9 @@ A more full example of generated client (with sub-resource) can is at the bottom Integer bodyParameterIdx = null; Map invocationBuilderEnrichers = new HashMap<>(); - ResultHandle multipartForm = null; + String[] consumes = extractProducesConsumesValues( + jandexMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); + boolean multipart = isMultipart(consumes, method.getParameters()); AssignableResultHandle formParams = null; @@ -857,7 +875,8 @@ A more full example of generated client (with sub-resource) can is at the bottom methodCreator.readStaticField(methodGenericParametersField.get()), methodCreator.readStaticField(methodParamAnnotationsField.get()), paramIdx)); - } else if (param.parameterType == ParameterType.BEAN) { + } else if (param.parameterType == ParameterType.BEAN + || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications // The web target changes have to be done on the method level. // Invocation.Builder changes are offloaded to a separate method @@ -880,7 +899,8 @@ A more full example of generated client (with sub-resource) can is at the bottom restClientInterface.getClassName(), methodCreator.getThis(), handleBeanParamMethod.getThis(), - formParams, methodGenericParametersField, methodParamAnnotationsField, paramIdx); + formParams, methodGenericParametersField, methodParamAnnotationsField, paramIdx, multipart, + beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, methodCreator.getMethodParam(paramIdx)); @@ -931,19 +951,16 @@ A more full example of generated client (with sub-resource) can is at the bottom handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleHeaderDescriptor, methodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(methodCreator, formParams); - addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.type, + formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); + // NOTE: don't use type here, because we're not going through the collection converters and stuff + addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.declaredType, + param.signature, restClientInterface.getClassName(), methodCreator.getThis(), formParams, methodCreator.readStaticField(methodGenericParametersField.get()), methodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexMethod.declaringClass().name() + "#" + jandexMethod.name()); - } - multipartForm = createMultipartForm(methodCreator, methodCreator.getMethodParam(paramIdx), - jandexMethod.parameterType(paramIdx), index); + paramIdx, multipart, + param.mimeType, param.partFileName, + jandexMethod.declaringClass().name() + "." + jandexMethod.name()); } } @@ -977,8 +994,9 @@ A more full example of generated client (with sub-resource) can is at the bottom } handleReturn(interfaceClass, defaultMediaType, method.getHttpMethod(), - method.getConsumes(), jandexMethod, methodCreator, formParams, multipartForm, - bodyParameterIdx == null ? null : methodCreator.getMethodParam(bodyParameterIdx), builder); + method.getConsumes(), jandexMethod, methodCreator, formParams, + bodyParameterIdx == null ? null : methodCreator.getMethodParam(bodyParameterIdx), builder, + multipart); } } @@ -1011,6 +1029,57 @@ A more full example of generated client (with sub-resource) can is at the bottom } + private boolean isMultipart(String[] consumes, MethodParameter[] methodParameters) { + if (consumes != null) { + for (String mimeType : consumes) { + if (mimeType.startsWith(MediaType.MULTIPART_FORM_DATA)) { + return true; + } + } + } + // see if the parameters require a multipart form + for (MethodParameter methodParameter : methodParameters) { + if (methodParameter.parameterType == ParameterType.FORM) { + if (isMultipartRequiringType(methodParameter.signature, methodParameter.mimeType)) { + return true; + } + } else if (methodParameter.parameterType == ParameterType.BEAN + || methodParameter.parameterType == ParameterType.MULTI_PART_FORM) { + ClientBeanParamInfo beanParam = (ClientBeanParamInfo) methodParameter; + if (isMultipartRequiringBeanParam(beanParam.getItems())) { + return true; + } + } + } + return false; + } + + private boolean isMultipartRequiringType(String signature, String partType) { + return (signature.equals(FILE_SIGNATURE) + || signature.equals(PATH_SIGNATURE) + || signature.equals(BUFFER_SIGNATURE) + || signature.equals(BYTE_ARRAY_SIGNATURE) + || signature.equals(MULTI_BYTE_SIGNATURE) + || partType != null); + } + + private boolean isMultipartRequiringBeanParam(List beanItems) { + for (Item beanItem : beanItems) { + if (beanItem instanceof FormParamItem) { + FormParamItem formParamItem = (FormParamItem) beanItem; + if (isMultipartRequiringType(formParamItem.getParamSignature(), formParamItem.getMimeType())) { + return true; + } + } else if (beanItem instanceof BeanParamItem) { + BeanParamItem beanParamItem = (BeanParamItem) beanItem; + if (isMultipartRequiringBeanParam(beanParamItem.items())) { + return true; + } + } + } + return false; + } + private void addResponseTypeIfMultipart(Set multipartResponseTypes, MethodInfo method, IndexView index) { AnnotationInstance produces = method.annotation(ResteasyReactiveDotNames.PRODUCES); if (produces == null) { @@ -1123,8 +1192,6 @@ private void handleSubResourceMethod(List i)); } - ResultHandle multipartForm = null; - int subMethodIndex = 0; for (ResourceMethod subMethod : method.getSubResourceMethods()) { MethodInfo jandexSubMethod = getJavaMethod(subInterface, subMethod, @@ -1134,6 +1201,10 @@ private void handleSubResourceMethod(List + subInterface + ". It may have unresolved parameter types (generics)")); subMethodIndex++; + String[] consumes = extractProducesConsumesValues( + jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); + consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); + boolean multipart = isMultipart(consumes, subMethod.getParameters()); boolean isSubResourceMethod = subMethod.getHttpMethod() == null; if (!isSubResourceMethod) { @@ -1187,7 +1258,8 @@ private void handleSubResourceMethod(List subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), subParamField.paramIndex)); - } else if (param.parameterType == ParameterType.BEAN) { + } else if (param.parameterType == ParameterType.BEAN + || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications // The web target changes have to be done on the method level. // Invocation.Builder changes are offloaded to a separate method @@ -1212,7 +1284,8 @@ private void handleSubResourceMethod(List subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), formParams, - methodGenericParametersField, methodParamAnnotationsField, subParamField.paramIndex); + methodGenericParametersField, methodParamAnnotationsField, subParamField.paramIndex, + multipart, beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); @@ -1272,15 +1345,10 @@ private void handleSubResourceMethod(List handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); + formParams = createFormDataIfAbsent(subMethodCreator, formParams, multipart); + // FIXME: this is weird, it doesn't go via converter nor multipart, looks like a bug subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, subMethodCreator.load(param.name), paramValue); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); - } - multipartForm = createMultipartForm(subMethodCreator, paramValue, subParamField.type, index); } } // handle sub-method parameters: @@ -1298,7 +1366,8 @@ private void handleSubResourceMethod(List subMethodCreator.readStaticField(subMethodGenericParametersField.get()), subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), paramIdx)); - } else if (param.parameterType == ParameterType.BEAN) { + } else if (param.parameterType == ParameterType.BEAN + || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications // The web target changes have to be done on the method level. // Invocation.Builder changes are offloaded to a separate method @@ -1322,7 +1391,8 @@ private void handleSubResourceMethod(List subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), formParams, - subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx); + subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx, multipart, + beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, @@ -1375,18 +1445,11 @@ private void handleSubResourceMethod(List handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleCookieDescriptor, subMethodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); + formParams = createFormDataIfAbsent(subMethodCreator, formParams, multipart); + // FIXME: this is weird, it doesn't go via converter nor multipart, looks like a bug subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, subMethodCreator.load(param.name), subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); - } - multipartForm = createMultipartForm(subMethodCreator, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameterType(paramIdx), index); } } @@ -1428,13 +1491,10 @@ private void handleSubResourceMethod(List generatedClasses, methodIndex, subMethodIndex, subMethodField); } - String[] consumes = extractProducesConsumesValues( - jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); - consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); handleReturn(subInterface, defaultMediaType, getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod), - consumes, jandexSubMethod, subMethodCreator, formParams, multipartForm, bodyParameterValue, - builder); + consumes, jandexSubMethod, subMethodCreator, formParams, bodyParameterValue, + builder, multipart); } else { // finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field // to later fill in context with corresponding java.lang.reflect.Method) @@ -1501,132 +1561,62 @@ private FieldDescriptor createRestClientField(String name, ClassCreator c, Metho return clientField; } - /* - * Translate the class to be sent as multipart to Vertx Web MultipartForm. - */ - private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHandle methodParam, Type formClassType, - IndexView index) { - AssignableResultHandle multipartForm = methodCreator.createVariable(QuarkusMultipartForm.class); - methodCreator.assign(multipartForm, - methodCreator.newInstance(MethodDescriptor.ofConstructor(QuarkusMultipartForm.class))); - - ClassInfo formClass = index.getClassByName(formClassType.name()); - - for (FieldInfo field : formClass.fields()) { - // go field by field, ignore static fields and fail on non-public fields, only public fields are supported ATM - if (Modifier.isStatic(field.flags())) { - continue; - } - - String fieldName = field.name(); - ResultHandle fieldValue = null; - String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); - for (MethodInfo method : formClass.methods()) { - if (method.name().equals(getterName) && method.returnType().name().equals(field.type().name()) - && method.parameterTypes().isEmpty() && Modifier.isPublic(method.flags()) - && !Modifier.isStatic(method.flags())) { - fieldValue = methodCreator.invokeVirtualMethod(method, methodParam); - break; - } - } - if ((fieldValue == null) && Modifier.isPublic(field.flags())) { - fieldValue = methodCreator.readInstanceField(field, methodParam); - - } - if (fieldValue == null) { - throw new IllegalArgumentException("Non-public field '" + fieldName - + "' without a getter, found in a multipart form data class '" - + formClassType.name() - + "'. Rest Client Reactive only supports multipart form classes with fields that are public or have public getters."); - } - - String formParamName = formParamName(field); - String partType = formPartType(field); - String partFilename = formPartFilename(field); - - Type fieldType = field.type(); - - BytecodeCreator ifValueNotNull = methodCreator.ifNotNull(fieldValue).trueBranch(); - - switch (fieldType.kind()) { - case CLASS: - // we support string, and send it as an attribute - ClassInfo fieldClass = index.getClassByName(fieldType.name()); - if (DotNames.STRING.equals(fieldClass.name())) { - addString(ifValueNotNull, multipartForm, formParamName, partFilename, fieldValue); - } else if (is(FILE, fieldClass, index)) { - // file is sent as file :) - if (partType == null) { - throw new IllegalArgumentException( - "No @PartType annotation found on multipart form field of type File: " + - formClass.name() + "." + field.name()); - } - ResultHandle filePath = ifValueNotNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(File.class, "toPath", Path.class), fieldValue); - addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, filePath); - } else if (is(PATH, fieldClass, index)) { - // and so is path - if (partType == null) { - throw new IllegalArgumentException( - "No @PartType annotation found on multipart form field of type Path: " + - formClass.name() + "." + field.name()); - } - addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue); - } else if (is(BUFFER, fieldClass, index)) { - // and buffer - addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, field); - } else { // assume POJO: - addPojo(ifValueNotNull, multipartForm, formParamName, partType, fieldValue, field); - } - break; - case ARRAY: - // byte[] can be sent as file too - Type componentType = fieldType.asArrayType().component(); - if (componentType.kind() != Type.Kind.PRIMITIVE - || !byte.class.getName().equals(componentType.name().toString())) { - throw new IllegalArgumentException("Array of unsupported type: " + componentType.name() - + " on " + formClassType.name() + "." + field.name()); - } - ResultHandle buffer = ifValueNotNull.invokeStaticInterfaceMethod( - MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class), - fieldValue); - addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, buffer, field); - break; - case PRIMITIVE: - // primitives are converted to text and sent as attribute - ResultHandle string = primitiveToString(ifValueNotNull, fieldValue, field); - addString(ifValueNotNull, multipartForm, formParamName, partFilename, string); - break; - case PARAMETERIZED_TYPE: - ParameterizedType parameterizedType = fieldType.asParameterizedType(); - List args = parameterizedType.arguments(); - if (parameterizedType.name().equals(MULTI) && args.size() == 1 && args.get(0).name().equals(BYTE)) { - addMultiAsFile(ifValueNotNull, multipartForm, formParamName, partType, field, fieldValue); - break; - } - throw new IllegalArgumentException("Unsupported multipart form field type: " + parameterizedType + "<" - + args.stream().map(a -> a.name().toString()).collect(Collectors.joining(",")) - + "> in field class " + formClassType.name()); - case VOID: - case TYPE_VARIABLE: - case UNRESOLVED_TYPE_VARIABLE: - case TYPE_VARIABLE_REFERENCE: - case WILDCARD_TYPE: - throw new IllegalArgumentException("Unsupported multipart form field type: " + fieldType + " in " + - "field class " + formClassType.name()); + private void handleMultipartField(String formParamName, String partType, String partFilename, + String type, + String parameterGenericType, ResultHandle fieldValue, AssignableResultHandle multipartForm, + BytecodeCreator methodCreator, + ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations, int methodIndex, + ResultHandle genericType, String errorLocation) { + + BytecodeCreator ifValueNotNull = methodCreator.ifNotNull(fieldValue).trueBranch(); + + // we support string, and send it as an attribute unconverted + if (type.equals(String.class.getName())) { + addString(ifValueNotNull, multipartForm, formParamName, partFilename, fieldValue); + } else if (type.equals(File.class.getName())) { + // file is sent as file :) + ResultHandle filePath = ifValueNotNull.invokeVirtualMethod( + MethodDescriptor.ofMethod(File.class, "toPath", Path.class), fieldValue); + addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, filePath); + } else if (type.equals(Path.class.getName())) { + // and so is path + addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue); + } else if (type.equals(Buffer.class.getName())) { + // and buffer + addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, errorLocation); + } else if (type.startsWith("[")) { + // byte[] can be sent as file too + if (!type.equals("[B")) { + throw new IllegalArgumentException("Array of unsupported type: " + type + + " on " + errorLocation); } + ResultHandle buffer = ifValueNotNull.invokeStaticInterfaceMethod( + MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class), + fieldValue); + addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, buffer, errorLocation); + } else if (parameterGenericType.equals(MULTI_BYTE_SIGNATURE)) { + addMultiAsFile(ifValueNotNull, multipartForm, formParamName, partType, fieldValue, errorLocation); + } else if (partType != null) { + // assume POJO: + addPojo(ifValueNotNull, multipartForm, formParamName, partType, fieldValue, type); + } else { + // go via converter + ResultHandle convertedFormParam = convertParamToString(ifValueNotNull, client, fieldValue, type, genericType, + parameterAnnotations, methodIndex); + BytecodeCreator parameterIsStringBranch = checkStringParam(ifValueNotNull, convertedFormParam, + restClientInterfaceClassName, errorLocation); + addString(parameterIsStringBranch, multipartForm, formParamName, partFilename, convertedFormParam); } - - return multipartForm; } private void addPojo(BytecodeCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, - String partType, ResultHandle fieldValue, FieldInfo field) { + String partType, ResultHandle fieldValue, String type) { methodCreator.assign(multipartForm, methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "entity", QuarkusMultipartForm.class, String.class, Object.class, String.class, Class.class), - multipartForm, methodCreator.load(field.name()), fieldValue, methodCreator.load(partType), - methodCreator.loadClassFromTCCL(field.type().name().toString()))); + multipartForm, methodCreator.load(formParamName), fieldValue, methodCreator.load(partType), + // FIXME: doesn't support generics + methodCreator.loadClassFromTCCL(type))); } /** @@ -1639,6 +1629,10 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi ResultHandle fileName = partFilename != null ? methodCreator.load(partFilename) : methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, fileNamePath); ResultHandle pathString = methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, filePath); + // they all default to plain/text except buffers/byte[]/Multi/File/Path + if (partType == null) { + partType = MediaType.APPLICATION_OCTET_STREAM; + } if (partType.equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM)) { methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); @@ -1704,12 +1698,11 @@ private void addString(BytecodeCreator methodCreator, AssignableResultHandle mul } private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, - String partType, FieldInfo field, - ResultHandle multi) { + String partType, + ResultHandle multi, String errorLocation) { + // they all default to plain/text except buffers/byte[]/Multi/File/Path if (partType == null) { - throw new IllegalArgumentException( - "No @PartType annotation found on multipart form field " + - field.declaringClass().name() + "." + field.name()); + partType = MediaType.APPLICATION_OCTET_STREAM; } if (partType.equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM)) { methodCreator.assign(multipartForm, @@ -1735,13 +1728,12 @@ private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandl } private void addBuffer(BytecodeCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, - String partType, String partFilename, ResultHandle buffer, FieldInfo field) { + String partType, String partFilename, ResultHandle buffer, String errorLocation) { ResultHandle filenameHandle = partFilename != null ? methodCreator.load(partFilename) : methodCreator.load(formParamName); + // they all default to plain/text except buffers/byte[]/Multi/File/Path if (partType == null) { - throw new IllegalArgumentException( - "No @PartType annotation found on multipart form field " + - field.declaringClass().name() + "." + field.name()); + partType = MediaType.APPLICATION_OCTET_STREAM; } if (partType.equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM)) { methodCreator.assign(multipartForm, @@ -1764,61 +1756,18 @@ private void addBuffer(BytecodeCreator methodCreator, AssignableResultHandle mul } } - private String formPartType(FieldInfo field) { - AnnotationInstance partType = field.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME); - if (partType != null) { - return partType.value().asString(); - } - return null; - } - - private String formPartFilename(FieldInfo field) { - AnnotationInstance partType = field.annotation(ResteasyReactiveDotNames.PART_FILE_NAME); - if (partType != null) { - return partType.value().asString(); - } - return null; - } - - private String formParamName(FieldInfo field) { - AnnotationInstance restFormParam = field.annotation(ResteasyReactiveDotNames.REST_FORM_PARAM); - AnnotationInstance formParam = field.annotation(ResteasyReactiveDotNames.FORM_PARAM); - if (restFormParam != null && formParam != null) { - throw new IllegalArgumentException("Only one of @RestFormParam, @FormParam annotations expected on a field. " + - "Found both on " + field.declaringClass() + "." + field.name()); - } - if (restFormParam != null) { - AnnotationValue value = restFormParam.value(); - if (value == null || "".equals(value.asString())) { - return field.name(); + private AssignableResultHandle createFormDataIfAbsent(BytecodeCreator methodCreator, AssignableResultHandle formValues, + boolean multipart) { + if (formValues == null) { + if (multipart) { + formValues = methodCreator.createVariable(QuarkusMultipartForm.class); + methodCreator.assign(formValues, + methodCreator.newInstance(MethodDescriptor.ofConstructor(QuarkusMultipartForm.class))); } else { - return value.asString(); + formValues = methodCreator.createVariable(MultivaluedMap.class); + methodCreator.assign(formValues, + methodCreator.newInstance(MethodDescriptor.ofConstructor(MultivaluedHashMap.class))); } - } else if (formParam != null) { - return formParam.value().asString(); - } else { - throw new IllegalArgumentException("One of @RestFormParam, @FormParam annotations expected on a field. " + - "No annotation found on " + field.declaringClass() + "." + field.name()); - } - } - - private boolean is(DotName desiredClass, ClassInfo fieldClass, IndexView index) { - if (fieldClass.name().equals(desiredClass)) { - return true; - } - ClassInfo superClass; - if (fieldClass.name().toString().equals(Object.class.getName()) || - (superClass = index.getClassByName(fieldClass.superName())) == null) { - return false; - } - return is(desiredClass, superClass, index); - } - - private AssignableResultHandle createIfAbsent(BytecodeCreator methodCreator, AssignableResultHandle formValues) { - if (formValues == null) { - formValues = methodCreator.createVariable(MultivaluedMap.class); - methodCreator.assign(formValues, - methodCreator.newInstance(MethodDescriptor.ofConstructor(MultivaluedHashMap.class))); } return formValues; } @@ -1851,8 +1800,8 @@ private String getHttpMethod(MethodInfo subMethod, String defaultMethod, Map 1) { - throw new IllegalArgumentException("Attempt to pass at least two of form, multipart form " + + if (bodyValue != null || formParams != null) { + if (countNonNulls(bodyValue, formParams) > 1) { + throw new IllegalArgumentException("Attempt to pass at least two of form " + "or regular entity as a request body in " + restClientInterface.name().toString() + "#" + jandexMethod.name()); } @@ -1968,6 +1917,8 @@ private void handleReturn(ClassInfo restClientInterface, String defaultMediaType + " Unable to determine a single `Content-Type`."); } mediaTypeValue = consumes[0]; + } else if (formParams != null) { + mediaTypeValue = multipart ? MediaType.MULTIPART_FORM_DATA : MediaType.APPLICATION_FORM_URLENCODED; } ResultHandle mediaType = tryBlock.invokeStaticMethod( MethodDescriptor.ofMethod(MediaType.class, "valueOf", MediaType.class, String.class), @@ -1975,7 +1926,7 @@ private void handleReturn(ClassInfo restClientInterface, String defaultMediaType ResultHandle entity = tryBlock.invokeStaticMethod( MethodDescriptor.ofMethod(Entity.class, "entity", Entity.class, Object.class, MediaType.class), - bodyValue != null ? bodyValue : (formParams != null ? formParams : multipartForm), + bodyValue != null ? bodyValue : formParams, mediaType); if (returnCategory == ReturnCategory.COMPLETION_STAGE) { @@ -2215,16 +2166,16 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, AssignableResultHandle formParams, Supplier methodGenericTypeField, Supplier methodParamAnnotationsField, - int paramIdx) { + int paramIdx, boolean multipart, String beanParamClass) { // Form params collector must be initialized at method root level before any inner blocks that may use it if (areFormParamsDefinedIn(beanParamItems)) { - formParams = createIfAbsent(methodCreator, formParams); + formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); } addSubBeanParamData(jandexMethod, methodCreator, invocationBuilderEnricher, invocationBuilder, beanParamItems, param, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, - methodGenericTypeField, methodParamAnnotationsField, paramIdx); + methodGenericTypeField, methodParamAnnotationsField, paramIdx, multipart, beanParamClass); return formParams; } @@ -2245,7 +2196,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method AssignableResultHandle formParams, Supplier methodGenericTypeField, Supplier methodParamAnnotationsField, - int paramIdx) { + int paramIdx, boolean multipart, String beanParamClass) { BytecodeCreator creator = methodCreator.ifNotNull(param).trueBranch(); BytecodeCreator invoEnricher = invocationBuilderEnricher.ifNotNull(invocationBuilderEnricher.getMethodParam(1)) .trueBranch(); @@ -2258,7 +2209,8 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method addSubBeanParamData(jandexMethod, creator, invoEnricher, invocationBuilder, beanParamItem.items(), beanParamElementHandle, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, - methodGenericTypeField, methodParamAnnotationsField, paramIdx); + methodGenericTypeField, methodParamAnnotationsField, paramIdx, multipart, + beanParamItem.className()); break; case QUERY_PARAM: QueryParamItem queryParam = (QueryParamItem) item; @@ -2299,10 +2251,12 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), - formParam.getParamType(), restClientInterfaceClassName, client, formParams, + formParam.getParamType(), formParam.getParamSignature(), restClientInterfaceClassName, client, + formParams, creator.readStaticField(methodGenericTypeField.get()), creator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); + paramIdx, multipart, formParam.getMimeType(), formParam.getFileName(), + beanParamClass + "." + formParam.getSourceName()); break; default: throw new IllegalStateException("Unimplemented"); @@ -2536,48 +2490,50 @@ private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle } private void addFormParam(BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, - String parameterType, String restClientInterfaceClassName, - ResultHandle client, AssignableResultHandle formParams, ResultHandle genericType, - ResultHandle parameterAnnotations, int methodIndex) { - BytecodeCreator notNullValue = methodCreator.ifNull(formParamHandle).falseBranch(); - ResultHandle convertedFormParam = notNullValue.invokeVirtualMethod( + String parameterType, String parameterGenericType, + String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams, + ResultHandle genericType, + ResultHandle parameterAnnotations, int methodIndex, boolean multipart, + String partType, String partFilename, String errorLocation) { + if (multipart) { + handleMultipartField(paramName, partType, partFilename, parameterType, parameterGenericType, formParamHandle, + formParams, methodCreator, + client, restClientInterfaceClassName, parameterAnnotations, methodIndex, genericType, + errorLocation); + } else { + BytecodeCreator notNullValue = methodCreator.ifNull(formParamHandle).falseBranch(); + ResultHandle convertedFormParam = convertParamToString(notNullValue, client, formParamHandle, parameterType, + genericType, parameterAnnotations, methodIndex); + BytecodeCreator parameterIsStringBranch = checkStringParam(notNullValue, convertedFormParam, + restClientInterfaceClassName, errorLocation); + parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + notNullValue.load(paramName), convertedFormParam); + } + } + + private BytecodeCreator checkStringParam(BytecodeCreator notNullValue, ResultHandle convertedFormParam, + String restClientInterfaceClassName, String errorLocation) { + ResultHandle isString = notNullValue.instanceOf(convertedFormParam, String.class); + BranchResult isStringBranch = notNullValue.ifTrue(isString); + isStringBranch.falseBranch().throwException(IllegalStateException.class, + "Form element '" + errorLocation + + "' could not be converted to 'String' for REST Client interface '" + + restClientInterfaceClassName + "'. A proper implementation of '" + + ParamConverter.class.getName() + "' needs to be returned by a '" + + ParamConverterProvider.class.getName() + + "' that is registered with the client via the @RegisterProvider annotation on the REST Client interface."); + return isStringBranch.trueBranch(); + } + + private ResultHandle convertParamToString(BytecodeCreator notNullValue, ResultHandle client, + ResultHandle formParamHandle, String parameterType, + ResultHandle genericType, ResultHandle parameterAnnotations, int methodIndex) { + return notNullValue.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, Object.class, Class.class, Supplier.class, Supplier.class, int.class), client, formParamHandle, notNullValue.loadClassFromTCCL(parameterType), genericType, parameterAnnotations, notNullValue.load(methodIndex)); - ResultHandle isString = notNullValue.instanceOf(convertedFormParam, String.class); - BranchResult isStringBranch = notNullValue.ifTrue(isString); - isStringBranch.trueBranch().invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - notNullValue.load(paramName), convertedFormParam); - - // if the converted value is not a string, then: - // - if it's a primitive type, use the valueOf() method. - if (EndpointIndexer.primitiveTypes.containsKey(parameterType)) { - ResultHandle convertedFormParamAsString = isStringBranch.falseBranch().invokeStaticMethod( - MethodDescriptor.ofMethod(Objects.class, "toString", String.class, Object.class), - convertedFormParam); - isStringBranch.falseBranch().invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - notNullValue.load(paramName), convertedFormParamAsString); - } else { - // - if it's an enum, use the name() method. - ResultHandle isEnum = isStringBranch.falseBranch().instanceOf(convertedFormParam, Enum.class); - BranchResult isEnumBranch = isStringBranch.falseBranch().ifTrue(isEnum); - ResultHandle enumAsString = isEnumBranch.trueBranch().invokeVirtualMethod( - MethodDescriptor.ofMethod(Enum.class, "name", String.class), - isEnumBranch.trueBranch().checkCast(convertedFormParam, Enum.class)); - isEnumBranch.trueBranch().invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - notNullValue.load(paramName), enumAsString); - - // - Otherwise, return exception - isEnumBranch.falseBranch().throwException(IllegalStateException.class, - "Form parameter '" + paramName - + "' could not be converted to 'String' for REST Client interface '" - + restClientInterfaceClassName + "'. A proper implementation of '" - + ParamConverter.class.getName() + "' needs to be returned by a '" - + ParamConverterProvider.class.getName() - + "' that is registered with the client via the @RegisterProvider annotation on the REST Client interface."); - } } private void addCookieParam(BytecodeCreator invoBuilderEnricher, AssignableResultHandle invocationBuilder, diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java index 52bc09d71e5f8..ce86ac8f57897 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java @@ -12,6 +12,95 @@ import javax.ws.rs.ext.ParamConverterProvider; public abstract class RestClientBase implements Closeable { + private static final ParamConverter BYTE_CONVERTER = new ParamConverter() { + @Override + public Byte fromString(String value) { + return value == null ? null : Byte.valueOf(value); + } + + @Override + public String toString(Byte value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter SHORT_CONVERTER = new ParamConverter() { + @Override + public Short fromString(String value) { + return value == null ? null : Short.valueOf(value); + } + + @Override + public String toString(Short value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter INTEGER_CONVERTER = new ParamConverter() { + @Override + public Integer fromString(String value) { + return value == null ? null : Integer.valueOf(value); + } + + @Override + public String toString(Integer value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter LONG_CONVERTER = new ParamConverter() { + @Override + public Long fromString(String value) { + return value == null ? null : Long.valueOf(value); + } + + @Override + public String toString(Long value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter FLOAT_CONVERTER = new ParamConverter() { + @Override + public Float fromString(String value) { + return value == null ? null : Float.valueOf(value); + } + + @Override + public String toString(Float value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter DOUBLE_CONVERTER = new ParamConverter() { + @Override + public Double fromString(String value) { + return value == null ? null : Double.valueOf(value); + } + + @Override + public String toString(Double value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter CHARACTER_CONVERTER = new ParamConverter() { + @Override + public Character fromString(String value) { + // this will throw if not enough chars, but that's an error anyway + return value == null ? null : value.charAt(0); + } + + @Override + public String toString(Character value) { + return value == null ? null : value.toString(); + } + }; + private static final ParamConverter BOOLEAN_CONVERTER = new ParamConverter() { + @Override + public Boolean fromString(String value) { + return value == null ? null : Boolean.valueOf(value); + } + + @Override + public String toString(Boolean value) { + return value == null ? null : value.toString(); + } + }; private final List paramConverterProviders; private final Map, ParamConverterProvider> providerForClass = new ConcurrentHashMap<>(); @@ -44,6 +133,10 @@ public Object convertParam(T value, Class type, Supplier genericT if (converter != null) { return converter.toString(value); } else { + // FIXME: cheating, we should generate a converter for this enum + if (value instanceof Enum) { + return ((Enum) value).name(); + } return value; } } @@ -62,6 +155,13 @@ private ParamConverter getConverter(Class type, Supplier gener return converter; } } + // FIXME: this should go in favour of generating them, so we can generate them only if used for dead-code elimination + ParamConverter converter = DEFAULT_PROVIDER.getConverter(type, genericType.get()[paramIndex], + methodAnnotations.get()[paramIndex]); + if (converter != null) { + providerForClass.put(type, DEFAULT_PROVIDER); + return converter; + } providerForClass.put(type, NO_PROVIDER); } else if (converterProvider != NO_PROVIDER) { return converterProvider.getConverter(type, genericType.get()[paramIndex], methodAnnotations.get()[paramIndex]); @@ -69,6 +169,39 @@ private ParamConverter getConverter(Class type, Supplier gener return null; } + private static final ParamConverterProvider DEFAULT_PROVIDER = new ParamConverterProvider() { + + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (rawType == byte.class || rawType == Byte.class) { + return (ParamConverter) BYTE_CONVERTER; + } + if (rawType == short.class || rawType == Short.class) { + return (ParamConverter) SHORT_CONVERTER; + } + if (rawType == int.class || rawType == Integer.class) { + return (ParamConverter) INTEGER_CONVERTER; + } + if (rawType == long.class || rawType == Long.class) { + return (ParamConverter) LONG_CONVERTER; + } + if (rawType == float.class || rawType == Float.class) { + return (ParamConverter) FLOAT_CONVERTER; + } + if (rawType == double.class || rawType == Double.class) { + return (ParamConverter) DOUBLE_CONVERTER; + } + if (rawType == char.class || rawType == Character.class) { + return (ParamConverter) CHARACTER_CONVERTER; + } + if (rawType == boolean.class || rawType == Boolean.class) { + return (ParamConverter) BOOLEAN_CONVERTER; + } + return null; + } + + }; + private static final ParamConverterProvider NO_PROVIDER = new ParamConverterProvider() { @Override public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ParameterContainersBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ParameterContainersBuildItem.java new file mode 100644 index 0000000000000..7d02fc31ff414 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ParameterContainersBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.common.deployment; + +import java.util.Set; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class ParameterContainersBuildItem extends MultiBuildItem { + + private final Set classNames; + + public ParameterContainersBuildItem(Set classNames) { + this.classNames = classNames; + } + + public Set getClassNames() { + return classNames; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index b18a5fc738ae5..64fcc46071310 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -18,6 +18,7 @@ import javax.ws.rs.ext.RuntimeDelegate; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; @@ -32,6 +33,7 @@ import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveInterceptorScanner; +import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveParameterContainerScanner; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner; import org.jboss.resteasy.reactive.common.processor.scanning.SerializerScanningResult; @@ -339,4 +341,14 @@ public static Set getExcludedClasses(List b .map(target -> target.asClass().toString()) .collect(Collectors.toSet()); } + + @BuildStep + public void scanForParameterContainers(CombinedIndexBuildItem combinedIndexBuildItem, + ApplicationResultBuildItem applicationResultBuildItem, + BuildProducer parameterContainersBuildItemBuildProducer) { + IndexView index = combinedIndexBuildItem.getComputingIndex(); + Set res = ResteasyReactiveParameterContainerScanner.scanParameterContainers(index, + applicationResultBuildItem.getResult()); + parameterContainersBuildItemBuildProducer.produce(new ParameterContainersBuildItem(res)); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java index a111f6ee1763e..e36389025e9de 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java @@ -1,16 +1,21 @@ package io.quarkus.resteasy.reactive.jackson.deployment.test; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.validation.Valid; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.smallrye.common.annotation.Blocking; @@ -22,7 +27,7 @@ public class MultipartResource { @Consumes(MediaType.MULTIPART_FORM_DATA) @Blocking @Path("/json") - public Map greeting(@Valid @MultipartForm FormData formData) { + public Map greeting(@Valid @BeanParam FormData formData) { Map result = new HashMap<>(formData.map); result.put("person", formData.person); result.put("htmlFileSize", formData.getHtmlPart().size()); @@ -35,4 +40,38 @@ public Map greeting(@Valid @MultipartForm FormData formData) { result.put("persons2", formData.persons2); return result; } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Blocking + @Path("/param/json") + public Map greeting( + @RestForm @PartType(MediaType.APPLICATION_JSON) Map map, + + @FormParam("names") @PartType(MediaType.TEXT_PLAIN) List names, + + @RestForm @PartType(MediaType.TEXT_PLAIN) int[] numbers, + + @RestForm @PartType(MediaType.TEXT_PLAIN) List numbers2, + + @RestForm @PartType(MediaType.APPLICATION_JSON) @Valid Person person, + + @RestForm @PartType(MediaType.APPLICATION_JSON) Person[] persons, + + @RestForm @PartType(MediaType.APPLICATION_JSON) List persons2, + + @RestForm("htmlFile") FileUpload htmlPart) { + Map result = new HashMap<>(map); + result.put("person", person); + result.put("htmlFileSize", htmlPart.size()); + result.put("htmlFilePath", htmlPart.uploadedFile().toAbsolutePath().toString()); + result.put("htmlFileContentType", htmlPart.contentType()); + result.put("names", names); + result.put("numbers", numbers); + result.put("numbers2", numbers2); + result.put("persons", persons); + result.put("persons2", persons2); + return result; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java index 169063a78deba..3daab7970555c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java @@ -84,6 +84,56 @@ public void testValid() throws IOException { .body("persons2[1].last", equalTo("Last2")); } + @Test + public void testValidParam() throws IOException { + RestAssured.given() + .multiPart("map", + "{\n" + + " \"foo\": \"bar\",\n" + + " \"sub\": {\n" + + " \"foo2\": \"bar2\"\n" + + " }\n" + + "}") + .multiPart("person", "{\"first\": \"Bob\", \"last\": \"Builder\"}", "application/json") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("names", "name1") + .multiPart("names", "name2") + .multiPart("numbers", 1) + .multiPart("numbers", 2) + .multiPart("numbers2", 1) + .multiPart("numbers2", 2) + .multiPart("persons", "{\"first\": \"First1\", \"last\": \"Last1\"}", "application/json") + .multiPart("persons", "{\"first\": \"First2\", \"last\": \"Last2\"}", "application/json") + .multiPart("persons2", "{\"first\": \"First1\", \"last\": \"Last1\"}", "application/json") + .multiPart("persons2", "{\"first\": \"First2\", \"last\": \"Last2\"}", "application/json") + .accept("application/json") + .when() + .post("/multipart/param/json") + .then() + .statusCode(200) + .body("foo", equalTo("bar")) + .body("sub.foo2", equalTo("bar2")) + .body("person.first", equalTo("Bob")) + .body("person.last", equalTo("Builder")) + .body("htmlFileSize", equalTo(Files.readAllBytes(HTML_FILE.toPath()).length)) + .body("htmlFilePath", not(equalTo(HTML_FILE.toPath().toAbsolutePath().toString()))) + .body("htmlFileContentType", equalTo("text/html")) + .body("names[0]", equalTo("name1")) + .body("names[1]", equalTo("name2")) + .body("numbers[0]", equalTo(1)) + .body("numbers[1]", equalTo(2)) + .body("numbers2[0]", equalTo(1)) + .body("numbers2[1]", equalTo(2)) + .body("persons[0].first", equalTo("First1")) + .body("persons[0].last", equalTo("Last1")) + .body("persons[1].first", equalTo("First2")) + .body("persons[1].last", equalTo("Last2")) + .body("persons2[0].first", equalTo("First1")) + .body("persons2[0].last", equalTo("Last1")) + .body("persons2[1].first", equalTo("First2")) + .body("persons2[1].last", equalTo("Last2")); + } + @Test public void testInvalid() { RestAssured.given() @@ -106,4 +156,27 @@ public void testInvalid() { .then() .statusCode(400); } + + @Test + public void testInvalidParam() { + RestAssured.given() + .multiPart("map", + "{\n" + + " \"foo\": \"bar\",\n" + + " \"sub\": {\n" + + " \"foo2\": \"bar2\"\n" + + " }\n" + + "}") + .multiPart("person", "{\"first\": \"Bob\"}", "application/json") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("names", "name1") + .multiPart("names", "name2") + .multiPart("numbers", 1) + .multiPart("numbers", 2) + .accept("application/json") + .when() + .post("/multipart/param/json") + .then() + .statusCode(400); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java index 0be6a017e31a1..07b180f69827e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.Files; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; @@ -14,7 +15,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; @@ -75,6 +75,20 @@ public void testInput() { assertThat(response).isEqualTo("John-Divino Pastor"); } + @Test + public void testInputParam() { + String response = RestAssured + .given() + .multiPart("name", "John") + .multiPart("school", SCHOOL, MediaType.APPLICATION_XML) + .post("/multipart/param/input") + .then() + .statusCode(200) + .extract().asString(); + + assertThat(response).isEqualTo("John-Divino Pastor"); + } + @Test public void testInputFile() throws IOException { String response = RestAssured @@ -113,14 +127,22 @@ public MultipartOutputResponse output() { @POST @Path("/input") @Consumes(MediaType.MULTIPART_FORM_DATA) - public String input(@MultipartForm MultipartInput input) { + public String input(@BeanParam MultipartInput input) { return input.name + "-" + input.school.name; } + @POST + @Path("/param/input") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String input(@RestForm String name, + @RestForm @PartType(MediaType.APPLICATION_XML) School school) { + return name + "-" + school.name; + } + @POST @Path("/input/file") @Consumes(MediaType.MULTIPART_FORM_DATA) - public int inputFile(@MultipartForm FileUploadData data) throws IOException { + public int inputFile(@BeanParam FileUploadData data) throws IOException { return Files.readAllBytes(data.fileUpload.filePath()).length; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartParamHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartParamHandler.java deleted file mode 100644 index 6ecfc1ed7b43b..0000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartParamHandler.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.quarkus.resteasy.reactive.server.deployment; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Predicate; - -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; -import org.jboss.jandex.IndexView; -import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; -import org.jboss.resteasy.reactive.server.processor.generation.multipart.MultipartPopulatorGenerator; -import org.jboss.resteasy.reactive.server.processor.generation.multipart.MultipartTransformer; - -import io.quarkus.deployment.GeneratedClassGizmoAdaptor; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; -import io.quarkus.deployment.builditem.GeneratedClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; - -public class QuarkusMultipartParamHandler implements EndpointIndexer.MultipartParameterIndexerExtension { - private final Map multipartInputGeneratedPopulators = new HashMap<>(); - final BuildProducer generatedClassBuildItemBuildProducer; - final Predicate applicationClassPredicate; - final BuildProducer reflectiveClassProducer; - final BuildProducer bytecodeTransformerBuildProducer; - - public QuarkusMultipartParamHandler(BuildProducer generatedClassBuildItemBuildProducer, - Predicate applicationClassPredicate, BuildProducer reflectiveClassProducer, - BuildProducer bytecodeTransformerBuildProducer) { - this.generatedClassBuildItemBuildProducer = generatedClassBuildItemBuildProducer; - this.applicationClassPredicate = applicationClassPredicate; - this.reflectiveClassProducer = reflectiveClassProducer; - this.bytecodeTransformerBuildProducer = bytecodeTransformerBuildProducer; - } - - @Override - public void handleMultipartParameter(ClassInfo multipartClassInfo, IndexView index) { - String className = multipartClassInfo.name().toString(); - if (multipartInputGeneratedPopulators.containsKey(className)) { - // we've already seen this class before and have done all we need to make it work - return; - } - reflectiveClassProducer.produce(new ReflectiveClassBuildItem(false, false, className)); - String populatorClassName = MultipartPopulatorGenerator.generate(multipartClassInfo, - new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer, applicationClassPredicate.test(className)), - index); - multipartInputGeneratedPopulators.put(className, populatorClassName); - - // transform the multipart pojo (and any super-classes) so we can access its fields no matter what - ClassInfo currentClassInHierarchy = multipartClassInfo; - while (true) { - bytecodeTransformerBuildProducer - .produce(new BytecodeTransformerBuildItem(currentClassInHierarchy.name().toString(), - new MultipartTransformer(populatorClassName))); - - DotName superClassDotName = currentClassInHierarchy.superName(); - if (superClassDotName.equals(DotNames.OBJECT_NAME)) { - break; - } - ClassInfo newCurrentClassInHierarchy = index.getClassByName(superClassDotName); - if (newCurrentClassInHierarchy == null) { - break; - } - currentClassInHierarchy = newCurrentClassInHierarchy; - } - - } -} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 2003c730243fd..6bb15d1a20b61 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -98,7 +98,6 @@ import org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator; import org.jboss.resteasy.reactive.server.processor.generation.injection.TransformedFieldInjectionIndexerExtension; import org.jboss.resteasy.reactive.server.processor.generation.multipart.GeneratedHandlerMultipartReturnTypeIndexerExtension; -import org.jboss.resteasy.reactive.server.processor.generation.multipart.GeneratedMultipartParamIndexerExtension; import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; import org.jboss.resteasy.reactive.server.processor.scanning.ResponseHeaderMethodScanner; import org.jboss.resteasy.reactive.server.processor.scanning.ResponseStatusMethodScanner; @@ -149,6 +148,7 @@ import io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.FactoryUtils; +import io.quarkus.resteasy.reactive.common.deployment.ParameterContainersBuildItem; import io.quarkus.resteasy.reactive.common.deployment.QuarkusFactoryCreator; import io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames; import io.quarkus.resteasy.reactive.common.deployment.ResourceInterceptorsBuildItem; @@ -387,6 +387,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, BuildProducer reflectiveHierarchy, ApplicationResultBuildItem applicationResultBuildItem, ParamConverterProvidersBuildItem paramConverterProvidersBuildItem, + List parameterContainersBuildItems, List applicationClassPredicateBuildItems, List methodScanners, List annotationTransformerBuildItems, @@ -418,6 +419,11 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, AdditionalWriters additionalWriters = new AdditionalWriters(); Map injectableBeans = new HashMap<>(); QuarkusServerEndpointIndexer serverEndpointIndexer; + Set scannedParameterContainers = new HashSet<>(); + + for (ParameterContainersBuildItem parameterContainersBuildItem : parameterContainersBuildItems) { + scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); + } ParamConverterProviders paramConverterProviders = paramConverterProvidersBuildItem.getParamConverterProviders(); Function> factoryFunction = s -> FactoryUtils.factory(s, singletonClasses, recorder, @@ -453,6 +459,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList())) .setIndex(index) .setApplicationIndex(applicationIndexBuildItem.getIndex()) + .addParameterContainerTypes(scannedParameterContainers) .addContextTypes(additionalContextTypes(contextTypeBuildItems)) .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue())) .setEndpointInvokerFactory( @@ -467,8 +474,6 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, .setAdditionalWriters(additionalWriters) .setDefaultBlocking(appResult.getBlockingDefault()) .setApplicationScanningResult(appResult) - .setMultipartParameterIndexerExtension( - new GeneratedMultipartParamIndexerExtension(transformationConsumer, classOutput)) .setMultipartReturnTypeIndexerExtension( new GeneratedHandlerMultipartReturnTypeIndexerExtension(classOutput)) .setFieldInjectionIndexerExtension( @@ -601,9 +606,6 @@ public Status isJava19OrHigher() { serverEndpointIndexerBuilder.setMultipartReturnTypeIndexerExtension(new QuarkusMultipartReturnTypeHandler( generatedClassBuildItemBuildProducer, applicationClassPredicate, reflectiveClassBuildItemBuildProducer)); - serverEndpointIndexerBuilder.setMultipartParameterIndexerExtension(new QuarkusMultipartParamHandler( - generatedClassBuildItemBuildProducer, applicationClassPredicate, reflectiveClassBuildItemBuildProducer, - bytecodeTransformerBuildItemBuildProducer)); serverEndpointIndexer = serverEndpointIndexerBuilder.build(); Map> allMethods = new HashMap<>(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/ErroneousFieldMultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/ErroneousFieldMultipartInputTest.java deleted file mode 100644 index a855056950fab..0000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/ErroneousFieldMultipartInputTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.quarkus.resteasy.reactive.server.test.multipart; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.function.Supplier; - -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import org.jboss.resteasy.reactive.MultipartForm; -import org.jboss.resteasy.reactive.RestForm; -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; - -public class ErroneousFieldMultipartInputTest { - - @RegisterExtension - static QuarkusUnitTest test = new QuarkusUnitTest() - .setArchiveProducer(new Supplier<>() { - @Override - public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class) - .addClasses(Input.class); - } - - }).setExpectedException(IllegalArgumentException.class); - - @Test - public void testSimple() { - fail("Should never be called"); - } - - @Path("test") - public static class TestEndpoint { - - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - @POST - public int test(@MultipartForm Input formData) { - return formData.txtFile.length; - } - } - - public static class Input { - @RestForm - private String name; - - @RestForm - public byte[] txtFile; - } - -} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/FormDataWithAllUploads.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/FormDataWithAllUploads.java index 1a81dae3b2539..ba2b27ca7f125 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/FormDataWithAllUploads.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/FormDataWithAllUploads.java @@ -18,7 +18,7 @@ public class FormDataWithAllUploads extends FormDataBase { @PartType(MediaType.TEXT_PLAIN) private Status status; - @RestForm + @RestForm(FileUpload.ALL) private List uploads; public String getName() { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java index 4e37ee7a38a35..c0557014e9c97 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/InvalidEncodingTest.java @@ -5,13 +5,13 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.RestForm; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; @@ -60,7 +60,7 @@ public static class FeedbackResource { @Path("/multipart-encoding") @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA + ";charset=UTF-8") - public String postForm(@MultipartForm final FeedbackBody feedback) { + public String postForm(@BeanParam final FeedbackBody feedback) { return feedback.content; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java index c7220c2b7eba3..f57c3e51f97eb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.util.function.Supplier; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.POST; @@ -16,7 +17,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -73,7 +73,7 @@ public static class Resource { @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) - public String hello(@MultipartForm Data data) { + public String hello(@BeanParam Data data) { return data.getText(); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MalformedMultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MalformedMultipartInputTest.java index 40f97f83d2581..7c18820ecf6eb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MalformedMultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MalformedMultipartInputTest.java @@ -8,6 +8,7 @@ import java.lang.reflect.Type; import java.util.function.Supplier; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -19,7 +20,6 @@ import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.common.providers.serialisers.PrimitiveBodyHandler; @@ -77,7 +77,7 @@ public static class TestEndpoint { @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) @POST - public MyEnum test(@MultipartForm Input formData) { + public MyEnum test(@BeanParam Input formData) { return formData.format; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java index 88617b88dbfb2..a0d69d00d2c64 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java @@ -82,6 +82,48 @@ public void testSimple() { Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); } + @Test + public void testSimpleImplicit() { + RestAssured.given() + .multiPart("name", "Alice") + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart/implicit/simple/2") + .then() + .statusCode(200) + .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); + } + + @Test + public void testSimpleParam() { + RestAssured.given() + .multiPart("name", "Alice") + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart/param/simple/2") + .then() + .statusCode(200) + .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); + } + @Test public void testBlocking() throws IOException { RestAssured.given() @@ -144,6 +186,26 @@ public void testSameName() { Assertions.assertEquals(4, uploadDir.toFile().listFiles().length); } + @Test + public void testSameNameParam() { + RestAssured.given() + .multiPart("active", "false") + .multiPart("status", "EATING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("htmlFile", HTML_FILE2, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart/param/same-name") + .then() + .statusCode(200) + .body(equalTo("EATING - 2 - 1 - 1")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(4, uploadDir.toFile().listFiles().length); + } + private String filePath(File file) { return file.toPath().toAbsolutePath().toString(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java index 48cdec080f1ca..c9529c094e823 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java @@ -76,4 +76,25 @@ public void testSimple() throws IOException { // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); } + + @Test + public void testSimpleParam() throws IOException { + RestAssured.given() + .multiPart("name", "Alice") + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart-all/param/simple/2") + .then() + .statusCode(200) + .body(equalTo("Alice - true - 50 - WORKING - 3 - text/plain")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java index 58edf0ceae3a7..5548a439f98fa 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java @@ -1,8 +1,11 @@ package io.quarkus.resteasy.reactive.server.test.multipart; +import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.List; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.POST; @@ -11,8 +14,10 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.runtime.BlockingOperationControl; import io.smallrye.common.annotation.Blocking; @@ -26,7 +31,7 @@ public class MultipartResource { @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/simple/{times}") @NonBlocking - public String simple(@MultipartForm FormData formData, Integer times) { + public String simple(@BeanParam FormData formData, Integer times) { if (BlockingOperationControl.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } @@ -36,6 +41,43 @@ public String simple(@MultipartForm FormData formData, Integer times) { + formData.txtFile.exists(); } + @POST + @Path("/implicit/simple/{times}") + @NonBlocking + public String simpleImplicit(FormData formData, Integer times) { + if (BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should not have dispatched"); + } + return formData.getName() + " - " + formData.active + " - " + times * formData.getNum() + " - " + formData.getStatus() + + " - " + + formData.getHtmlPart().contentType() + " - " + Files.exists(formData.xmlPart) + " - " + + formData.txtFile.exists(); + } + + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/param/simple/{times}") + @NonBlocking + public String simple( + // don't set a part type, use the default + @RestForm String name, + @RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm("htmlFile") FileUpload htmlPart, + @RestForm("xmlFile") java.nio.file.Path xmlPart, + @RestForm File txtFile, + @RestForm @PartType(MediaType.TEXT_PLAIN) boolean active, + @RestForm @PartType(MediaType.TEXT_PLAIN) int num, + Integer times) { + if (BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should not have dispatched"); + } + return name + " - " + active + " - " + times * num + " - " + status + + " - " + + htmlPart.contentType() + " - " + Files.exists(xmlPart) + " - " + + txtFile.exists(); + } + @POST @Blocking @Produces(MediaType.TEXT_PLAIN) @@ -67,12 +109,27 @@ public String sameName(FormDataSameFileName formData) { + formData.xmlFiles.size(); } + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/param/same-name") + public String sameName(@RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm("htmlFile") List htmlFiles, + @RestForm("txtFile") List txtFiles, + @RestForm("xmlFile") List xmlFiles) { + if (!BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should have dispatched"); + } + return status + " - " + htmlFiles.size() + " - " + txtFiles.size() + " - " + + xmlFiles.size(); + } + @POST @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/optional") @NonBlocking - public String optional(@MultipartForm FormData formData) { + public String optional(FormData formData) { if (BlockingOperationControl.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java index baec08f7f9c76..0f06e28291b8d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java @@ -1,12 +1,16 @@ package io.quarkus.resteasy.reactive.server.test.multipart; +import java.util.List; + +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.runtime.BlockingOperationControl; @@ -20,7 +24,7 @@ public class MultipartResourceWithAllUploads { @Consumes(MediaType.MULTIPART_FORM_DATA) @NonBlocking @Path("/simple/{times}") - public String simple(@MultipartForm FormDataWithAllUploads formData, Integer times) { + public String simple(@BeanParam FormDataWithAllUploads formData, Integer times) { if (BlockingOperationControl.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } @@ -29,4 +33,25 @@ public String simple(@MultipartForm FormDataWithAllUploads formData, Integer tim + " - " + formData.getUploads().size() + " - " + txtFile.contentType(); } + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @NonBlocking + @Path("/param/simple/{times}") + public String simple( + @RestForm + // don't set a part type, use the default + String name, + @RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm(FileUpload.ALL) List uploads, + @RestForm @PartType(MediaType.TEXT_PLAIN) boolean active, + @RestForm @PartType(MediaType.TEXT_PLAIN) int num, + Integer times) { + if (BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should not have dispatched"); + } + FileUpload txtFile = uploads.stream().filter(f -> f.name().equals("txtFile")).findFirst().get(); + return name + " - " + active + " - " + times * num + " - " + status + + " - " + uploads.size() + " - " + txtFile.contentType(); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/OtherMultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/OtherMultipartResource.java index 1e4df1fcc2336..f768414f5d39d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/OtherMultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/OtherMultipartResource.java @@ -1,13 +1,12 @@ package io.quarkus.resteasy.reactive.server.test.multipart; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; - @Path("/otherMultipart") public class OtherMultipartResource { @@ -15,7 +14,7 @@ public class OtherMultipartResource { @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) @POST - public String simple(@MultipartForm OtherFormData formData) { + public String simple(@BeanParam OtherFormData formData) { return formData.first + " - " + formData.last + " - " + formData.finalField + " - " + OtherFormData.staticField; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java index d1b69fab1874f..f71ef64db649a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java @@ -9,13 +9,13 @@ import java.nio.file.Paths; import java.util.function.Supplier; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -101,7 +101,7 @@ public static class Resource { @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) - public String hello(@MultipartForm FormData data) { + public String hello(@BeanParam FormData data) { return data.getName(); } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java new file mode 100644 index 0000000000000..58bd4d6ceb106 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java @@ -0,0 +1,152 @@ +package io.quarkus.rest.client.reactive.multipart; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileUpload; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.smallrye.mutiny.Multi; +import io.vertx.core.buffer.Buffer; + +public class MultipartDetectionTest { + + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Resource.class, Client.class, Person.class)); + + @Test + void shouldCallExplicitEndpoints() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + byte[] contents = "Hello".getBytes(StandardCharsets.UTF_8); + Files.write(file.toPath(), contents); + file.deleteOnExit(); + + assertThat(client.postMultipartExplicit(file.getName(), file)) + .isEqualTo(file.getName() + " " + file.getName() + " Hello"); + assertThat(client.postUrlencodedExplicit(file.getName())).isEqualTo(file.getName()); + } + + @Test + void shouldCallImplicitEndpoints() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + byte[] contents = "Hello".getBytes(StandardCharsets.UTF_8); + Files.write(file.toPath(), contents); + file.deleteOnExit(); + Byte[] contentsForMulti = new Byte[contents.length]; + for (int i = 0; i < contents.length; i++) { + contentsForMulti[i] = contents[i]; + } + Person person = new Person(); + person.firstName = "Stef"; + person.lastName = "Epardaud"; + + assertThat(client.postMultipartImplicit(file.getName(), file)) + .isEqualTo(file.getName() + " " + file.getName() + " Hello"); + assertThat(client.postMultipartImplicit(file.getName(), file.toPath())) + .isEqualTo(file.getName() + " " + file.getName() + " Hello"); + assertThat(client.postMultipartImplicit(file.getName(), contents)).isEqualTo(file.getName() + " file Hello"); + assertThat(client.postMultipartImplicit(file.getName(), Buffer.buffer(contents))) + .isEqualTo(file.getName() + " file Hello"); + assertThat(client.postMultipartImplicit(file.getName(), Multi.createFrom().items(contentsForMulti))) + .isEqualTo(file.getName() + " file Hello"); + assertThat(client.postMultipartEntityImplicit(file.getName(), person)) + .isEqualTo(file.getName() + " Stef:Epardaud"); + } + + @Path("form") + @ApplicationScoped + public static class Resource { + @Path("multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String uploadMultipart(@RestForm String name, @RestForm FileUpload file) throws IOException { + return name + " " + file.fileName() + " " + Files.readString(file.filePath()); + } + + @Path("multipart-entity") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String uploadMultipart(@RestForm String name, @PartType(MediaType.APPLICATION_JSON) @RestForm Person entity) { + return name + " " + entity.firstName + ":" + entity.lastName; + } + + @Path("urlencoded") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String uploadMultipart(@RestForm String name) { + return name; + } + } + + @Path("form") + public interface Client { + @Path("multipart") + @POST + String postMultipartImplicit(@RestForm String name, @RestForm File file); + + @Path("multipart") + @POST + String postMultipartImplicit(@RestForm String name, @RestForm java.nio.file.Path file); + + @Path("multipart") + @POST + String postMultipartImplicit(@RestForm String name, @RestForm byte[] file); + + @Path("multipart") + @POST + String postMultipartImplicit(@RestForm String name, @RestForm Multi file); + + @Path("multipart") + @POST + String postMultipartImplicit(@RestForm String name, @RestForm Buffer file); + + @Path("multipart-entity") + @POST + String postMultipartEntityImplicit(@RestForm String name, + @PartType(MediaType.APPLICATION_JSON) @RestForm Person entity); + + @Path("multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipartExplicit(@RestForm String name, @RestForm File file); + + @Path("urlencoded") + @POST + String postUrlencodedImplicit(@RestForm String name); + + @Path("urlencoded") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + String postUrlencodedExplicit(@RestForm String name); + } + + public static class Person { + public String firstName; + public String lastName; + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResource.java new file mode 100644 index 0000000000000..57ccd44263248 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResource.java @@ -0,0 +1,24 @@ +package io.quarkus.rest.client.reactive.multipart; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileUpload; + +@Path("multipart") +public class MultipartResource { + public static class Person { + public String firstName; + public String lastName; + } + + @POST + public void multipart(@RestForm String description, + @RestForm FileUpload upload, + @RestForm @PartType(MediaType.APPLICATION_JSON) Person person) { + // do something + } +} diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java index 6e8da45aeb95e..4a31a0346ef47 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java @@ -4,13 +4,19 @@ public class BeanParamItem extends Item { private final List items; + private final String className; + + public String className() { + return className; + } public List items() { return items; } - public BeanParamItem(List items, ValueExtractor extractor) { + public BeanParamItem(List items, String className, ValueExtractor extractor) { super(ItemType.BEAN_PARAM, extractor); this.items = items; + this.className = className; } } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java index 3136f8fd2041c..965884288136f 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java @@ -15,6 +15,8 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import javax.ws.rs.core.MediaType; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; @@ -23,7 +25,9 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; +import org.jboss.resteasy.reactive.common.processor.AsmUtil; import org.jboss.resteasy.reactive.common.processor.JandexUtil; +import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; public class BeanParamParser { @@ -60,9 +64,10 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde (annotationValue, fieldInfo) -> { Type type = fieldInfo.type(); if (type.kind() == Type.Kind.CLASS) { - List subBeanParamItems = parseInternal(index.getClassByName(type.asClassType().name()), index, + DotName beanParamClassName = type.asClassType().name(); + List subBeanParamItems = parseInternal(index.getClassByName(beanParamClassName), index, processedBeanParamClasses); - return new BeanParamItem(subBeanParamItems, + return new BeanParamItem(subBeanParamItems, beanParamClassName.toString(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())); } else { throw new IllegalArgumentException("BeanParam annotation used on a field that is not an object: " @@ -73,7 +78,7 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde Type returnType = getterMethod.returnType(); List items = parseInternal(index.getClassByName(returnType.name()), index, processedBeanParamClasses); - return new BeanParamItem(items, new GetterExtractor(getterMethod)); + return new BeanParamItem(items, beanParamClass.name().toString(), new GetterExtractor(getterMethod)); })); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, COOKIE_PARAM, @@ -100,10 +105,15 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, FORM_PARAM, (annotationValue, fieldInfo) -> new FormParamItem(annotationValue, - fieldInfo.type().name().toString(), + fieldInfo.type().name().toString(), AsmUtil.getSignature(fieldInfo.type(), arg -> arg), + fieldInfo.name(), + partType(fieldInfo), fileName(fieldInfo), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), (annotationValue, getterMethod) -> new FormParamItem(annotationValue, getterMethod.returnType().name().toString(), + AsmUtil.getSignature(getterMethod.returnType(), arg -> arg), + getterMethod.name(), + partType(getterMethod), fileName(getterMethod), new GetterExtractor(getterMethod)))); return resultList; @@ -113,6 +123,38 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde } } + private static String partType(FieldInfo annotated) { + return partType(annotated.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME)); + } + + private static String partType(MethodInfo annotated) { + return partType(annotated.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME)); + } + + private static String partType(AnnotationInstance annotation) { + if (annotation == null || annotation.value() == null) + return null; + String mimeType = annotation.value().asString(); + // nullify default value + if (!mimeType.equals(MediaType.TEXT_PLAIN)) + return mimeType; + return null; + } + + private static String fileName(FieldInfo annotated) { + return fileName(annotated.annotation(ResteasyReactiveDotNames.PART_FILE_NAME)); + } + + private static String fileName(MethodInfo annotated) { + return fileName(annotated.annotation(ResteasyReactiveDotNames.PART_FILE_NAME)); + } + + private static String fileName(AnnotationInstance annotation) { + if (annotation == null || annotation.value() == null) + return null; + return annotation.value().asString(); + } + private static MethodInfo getGetterMethod(ClassInfo beanParamClass, MethodInfo methodInfo) { MethodInfo getter = null; if (methodInfo.parametersCount() > 0) { // should be setter diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java index 4a138bf61ff74..ebf9f08ccad23 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java @@ -4,11 +4,22 @@ public class FormParamItem extends Item { private final String formParamName; private final String paramType; + private final String paramSignature; + private final String mimeType; + private final String fileName; + private final String sourceName; - public FormParamItem(String formParamName, String paramType, ValueExtractor valueExtractor) { + public FormParamItem(String formParamName, String paramType, String paramSignature, + String sourceName, + String mimeType, String fileName, + ValueExtractor valueExtractor) { super(ItemType.FORM_PARAM, valueExtractor); this.formParamName = formParamName; this.paramType = paramType; + this.paramSignature = paramSignature; + this.mimeType = mimeType; + this.fileName = fileName; + this.sourceName = sourceName; } public String getFormParamName() { @@ -18,4 +29,20 @@ public String getFormParamName() { public String getParamType() { return paramType; } + + public String getParamSignature() { + return paramSignature; + } + + public String getFileName() { + return fileName; + } + + public String getMimeType() { + return mimeType; + } + + public String getSourceName() { + return sourceName; + } } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index f3b2e27bfc172..ba58ddbc17f5b 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -137,10 +137,21 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas Type paramType, ClientIndexedParam parameterResult, String name, String defaultValue, ParameterType type, String elementType, boolean single, String signature) { DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); + String mimePart = getPartMime(parameterResult.getAnns()); + String partFileName = getPartFileName(parameterResult.getAnns()); return new MethodParameter(name, elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), signature, type, single, - defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded); + defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, + mimePart, partFileName); + } + + private String getPartFileName(Map annotations) { + AnnotationInstance partFileName = annotations.get(ResteasyReactiveDotNames.PART_FILE_NAME); + if (partFileName != null && partFileName.value() != null) { + return partFileName.value().asString(); + } + return null; } @Override diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index c318c2cd826d5..eb0d2f2ad0807 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -109,6 +109,7 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType.Primitive; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; import org.jboss.jandex.TypeVariable; @@ -228,6 +229,7 @@ public abstract class EndpointIndexer contextTypes; + private final Set parameterContainerTypes; private final MultipartReturnTypeIndexerExtension multipartReturnTypeIndexerExtension; private final MultipartParameterIndexerExtension multipartParameterIndexerExtension; private final TargetJavaVersion targetJavaVersion; @@ -250,6 +252,7 @@ protected EndpointIndexer(Builder builder) { this.annotationStore = new AnnotationStore(builder.annotationsTransformers); this.applicationScanningResult = builder.applicationScanningResult; this.contextTypes = builder.contextTypes; + this.parameterContainerTypes = builder.parameterContainerTypes; this.multipartReturnTypeIndexerExtension = builder.multipartReturnTypeIndexerExtension; this.multipartParameterIndexerExtension = builder.multipartParameterIndexerExtension; this.targetJavaVersion = builder.targetJavaVersion; @@ -524,8 +527,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf boolean suspended = false; boolean sse = false; boolean formParamRequired = false; - boolean multipart = false; - boolean hasBodyParam = false; + Type bodyParamType = null; TypeArgMapper typeArgMapper = new TypeArgMapper(currentMethodInfo.declaringClass(), index); for (int i = 0; i < methodParameters.length; ++i) { Map anns = parameterAnnotations[i]; @@ -545,11 +547,11 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf String defaultValue = parameterResult.getDefaultValue(); ParameterType type = parameterResult.getType(); if (type == ParameterType.BODY) { - if (hasBodyParam) + if (bodyParamType != null) throw new RuntimeException( "Resource method " + currentMethodInfo + " can only have a single body parameter: " + currentMethodInfo.parameterName(i)); - hasBodyParam = true; + bodyParamType = paramType; } String elementType = parameterResult.getElementType(); boolean single = parameterResult.isSingle(); @@ -560,38 +562,38 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf parameterResult, name, defaultValue, type, elementType, single, AsmUtil.getSignature(paramType, typeArgMapper)); - if (type == ParameterType.BEAN) { + if (type == ParameterType.BEAN + || type == ParameterType.MULTI_PART_FORM) { // transform the bean param formParamRequired |= handleBeanParam(actualEndpointInfo, paramType, methodParameters, i); } else if (type == ParameterType.FORM) { formParamRequired = true; - } else if (type == ParameterType.MULTI_PART_FORM) { - multipart = true; - ClassInfo multipartClassInfo = index.getClassByName(paramType.name()); - multipartParameterIndexerExtension.handleMultipartParameter(multipartClassInfo, index); } } - if (multipart) { - if (hasBodyParam) { + if (formParamRequired) { + if (bodyParamType != null + && !bodyParamType.name().equals(ResteasyReactiveDotNames.MULTI_VALUED_MAP) + && !bodyParamType.name().equals(ResteasyReactiveDotNames.STRING)) { throw new RuntimeException( - "'@MultipartForm' cannot be used in a resource method that contains a body parameter. Offending method is '" + "'@FormParam' and '@RestForm' cannot be used in a resource method that contains a body parameter. Offending method is '" + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo + "'"); } boolean validConsumes = false; - if (consumes != null) { + if (consumes != null && consumes.length > 0) { for (String c : consumes) { - if (c.startsWith(MediaType.MULTIPART_FORM_DATA)) { + if (c.startsWith(MediaType.MULTIPART_FORM_DATA) + || c.startsWith(MediaType.APPLICATION_FORM_URLENCODED)) { validConsumes = true; break; } } - } - // TODO: does it make sense to default to MediaType.MULTIPART_FORM_DATA when no consumes is set? - if (!validConsumes) { - throw new RuntimeException( - "'@MultipartForm' can only be used on methods annotated with '@Consumes(MediaType.MULTIPART_FORM_DATA)'. Offending method is '" - + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo + "'"); + // TODO: does it make sense to default to MediaType.MULTIPART_FORM_DATA when no consumes is set? + if (!validConsumes) { + throw new RuntimeException( + "'@FormParam' and '@RestForm' can only be used on methods annotated with '@Consumes(MediaType.MULTIPART_FORM_DATA)' '@Consumes(MediaType.APPLICATION_FORM_URLENCODED)'. Offending method is '" + + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo + "'"); + } } } @@ -685,7 +687,6 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf .setSse(sse) .setStreamElementType(streamElementType) .setFormParamRequired(formParamRequired) - .setMultipart(multipart) .setParameters(methodParameters) .setSimpleReturnType( toClassName(currentMethodInfo.returnType(), currentClassInfo, actualEndpointInfo, index)) @@ -1189,11 +1190,11 @@ public PARAM extractParameterInfo(ClassInfo currentClassInfo, ClassInfo actualEn } else if (formParam != null) { builder.setName(formParam.value().asString()); builder.setType(ParameterType.FORM); - convertible = true; + convertible = isFormParamConvertible(paramType); } else if (restFormParam != null) { builder.setName(valueOrDefault(restFormParam.value(), sourceName)); builder.setType(ParameterType.FORM); - convertible = true; + convertible = isFormParamConvertible(paramType); } else if (matrixParam != null) { builder.setName(matrixParam.value().asString()); builder.setType(ParameterType.MATRIX); @@ -1225,6 +1226,11 @@ public PARAM extractParameterInfo(ClassInfo currentClassInfo, ClassInfo actualEn && isContextType(paramType.asClassType())) { // no name required builder.setType(ParameterType.CONTEXT); + } else if (!field + && paramType.kind() == Kind.CLASS + && isParameterContainerType(paramType.asClassType())) { + // auto @BeanParam/@MultipartForm parameters + builder.setType(ParameterType.BEAN); } else if (!field && pathParameters.contains(sourceName)) { builder.setName(sourceName); builder.setType(ParameterType.PATH); @@ -1237,22 +1243,7 @@ && isContextType(paramType.asClassType())) { if (field) { return builder; } - if ((declaredConsumes != null) && (declaredConsumes.length == 1) - && (MediaType.MULTIPART_FORM_DATA.equals(declaredConsumes[0]))) { - // in this case it is safe to assume that we are consuming multipart data - // we already don't allow multipart to be used along with body in the same method, - // so this is completely safe - var type = toClassName(paramType, currentClassInfo, actualEndpointInfo, index); - var typeInfo = index.getClassByName(DotName.createSimple(type)); - if (typeInfo != null && typeInfo.annotationsMap().containsKey(REST_FORM_PARAM)) { - builder.setType(ParameterType.MULTI_PART_FORM); - } else { - //if the paramater does not have @RestForm annotations we treat it as a normal body - builder.setType(ParameterType.BODY); - } - } else { - builder.setType(ParameterType.BODY); - } + builder.setType(ParameterType.BODY); } } builder.setSingle(true); @@ -1325,7 +1316,11 @@ && isContextType(paramType.asClassType())) { } else if (paramType.kind() == Kind.ARRAY) { ArrayType at = paramType.asArrayType(); typeHandled = true; - builder.setSingle(false); + // special case do not treat byte[] for multipart form values + if (builder.type != ParameterType.FORM + || convertible) { + builder.setSingle(false); + } elementType = toClassName(at.component(), currentClassInfo, actualEndpointInfo, index); if (convertible) { handleArrayParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType); @@ -1351,6 +1346,17 @@ && isContextType(paramType.asClassType())) { return builder; } + private boolean isFormParamConvertible(Type paramType) { + // let's not call the array converter for byte[] for multipart + if (paramType.kind() == Kind.ARRAY + && paramType.asArrayType().component().kind() == Kind.PRIMITIVE + && paramType.asArrayType().component().asPrimitiveType().primitive() == Primitive.BYTE) { + return false; + } else { + return true; + } + } + protected boolean handleCustomParameter(Map anns, PARAM builder, Type paramType, boolean field, Map methodContext) { return false; @@ -1414,6 +1420,10 @@ final boolean isContextType(ClassType klass) { return contextTypes.contains(klass.name()); } + final boolean isParameterContainerType(ClassType klass) { + return parameterContainerTypes.contains(klass.name()); + } + private String valueOrDefault(AnnotationValue annotation, String defaultValue) { if (annotation == null) return defaultValue; @@ -1433,6 +1443,19 @@ protected AnnotationStore getAnnotationStore() { return annotationStore; } + protected String getPartMime(Map annotations) { + AnnotationInstance partType = annotations.get(ResteasyReactiveDotNames.PART_TYPE_NAME); + String mimeType = null; + if (partType != null && partType.value() != null) { + mimeType = partType.value().asString(); + // remove what ends up being the default + if (MediaType.TEXT_PLAIN.equals(mimeType)) { + mimeType = null; + } + } + return mimeType; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) public static abstract class Builder, B extends Builder, METHOD extends ResourceMethod> { private Function> factoryCreator; @@ -1452,6 +1475,7 @@ public static abstract class Builder, B private Collection annotationsTransformers; private ApplicationScanningResult applicationScanningResult; private Set contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); + private Set parameterContainerTypes = new HashSet<>(); private MultipartReturnTypeIndexerExtension multipartReturnTypeIndexerExtension = new MultipartReturnTypeIndexerExtension() { @Override public boolean handleMultipartForReturnType(AdditionalWriters additionalWriters, ClassInfo multipartClassInfo, @@ -1506,6 +1530,16 @@ public B addContextTypes(Collection contextTypes) { return (B) this; } + public B addParameterContainerType(DotName parameterContainerType) { + this.parameterContainerTypes.add(parameterContainerType); + return (B) this; + } + + public B addParameterContainerTypes(Collection parameterContainerTypes) { + this.parameterContainerTypes.addAll(parameterContainerTypes); + return (B) this; + } + public B setExistingConverters(Map existingConverters) { this.existingConverters = existingConverters; return (B) this; diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java new file mode 100644 index 0000000000000..59be140fa0e7c --- /dev/null +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java @@ -0,0 +1,29 @@ +package org.jboss.resteasy.reactive.common.processor.scanning; + +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; + +public class ResteasyReactiveParameterContainerScanner { + public static Set scanParameterContainers(IndexView index, ApplicationScanningResult result) { + Set res = new HashSet(); + for (DotName fieldAnnotation : ResteasyReactiveDotNames.JAX_RS_ANNOTATIONS_FOR_FIELDS) { + for (AnnotationInstance annotationInstance : index.getAnnotations(fieldAnnotation)) { + // FIXME: this only supports fields, not properties, but not sure beanparam supports them anyway + if (annotationInstance.target().kind() == Kind.FIELD) { + ClassInfo klass = annotationInstance.target().asField().declaringClass(); + if (result.keepClass(klass.name().toString())) { + res.add(klass.name()); + } + } + } + } + return res; + } +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/MultipartForm.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/MultipartForm.java index fa612a7ad549f..514940670f7ae 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/MultipartForm.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/MultipartForm.java @@ -5,6 +5,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.ws.rs.BeanParam; + /** * Annotation to be used on POJOs meant to map to the various parts * of {@code multipart/form-data} HTTP bodies. @@ -15,7 +17,13 @@ * It's important to take caution when using such POJOs to read via {@link org.jboss.resteasy.reactive.multipart.FileUpload}, * {@code java.io.File} or {@code java.nio.file.Path} uploaded files in a blocking manner, that the resource method should be * annotated with {@link io.smallrye.common.annotation.Blocking}. + * + * @deprecated this annotation is not required anymore: you can use {@link BeanParam} or just omit it entirely, as long as your + * container class holds any annotated fields with {@link RestForm}, {@link RestCookie}, {@link RestHeader}, + * {@link RestPath}, + * {@link RestMatrix}, {@link RestQuery} or their JAX-RS equivalents. */ +@Deprecated(forRemoval = true) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.PARAMETER, ElementType.TYPE }) public @interface MultipartForm { diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java index 8b1cc932a9ed7..e627ce3bb50d3 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java @@ -6,9 +6,10 @@ import java.lang.annotation.Target; /** - * Used on fields of {@link MultipartForm} POJOs to designate the media type the corresponding body part maps to. + * Used on fields of {@link MultipartForm} POJOs or form parameters to designate the media type the corresponding body part maps + * to. */ -@Target({ ElementType.FIELD }) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface PartType { String value(); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java index 2d17f503afc70..3490ee77bde90 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java @@ -23,6 +23,8 @@ public class MethodParameter { private String defaultValue; private boolean optional; private boolean isObtainedAsCollection; + public String mimeType; + public String partFileName; public MethodParameter() { } @@ -30,7 +32,8 @@ public MethodParameter() { public MethodParameter(String name, String type, String declaredType, String declaredUnresolvedType, String signature, ParameterType parameterType, boolean single, - String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded) { + String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded, + String mimeType, String partFileName) { this.name = name; this.type = type; this.declaredType = declaredType; @@ -42,6 +45,8 @@ public MethodParameter(String name, String type, String declaredType, String dec this.isObtainedAsCollection = isObtainedAsCollection; this.optional = optional; this.encoded = encoded; + this.mimeType = mimeType; + this.partFileName = partFileName; } public String getName() { @@ -132,12 +137,14 @@ public boolean equals(Object o) { && Objects.equals(type, that.type) && Objects.equals(declaredType, that.declaredType) && Objects.equals(declaredUnresolvedType, that.declaredUnresolvedType) && Objects.equals(signature, that.signature) && parameterType == that.parameterType - && Objects.equals(defaultValue, that.defaultValue); + && Objects.equals(defaultValue, that.defaultValue) + && Objects.equals(mimeType, that.mimeType) + && Objects.equals(partFileName, that.partFileName); } @Override public int hashCode() { return Objects.hash(name, type, declaredType, declaredUnresolvedType, signature, parameterType, encoded, single, - defaultValue, optional, isObtainedAsCollection); + defaultValue, optional, isObtainedAsCollection, mimeType, partFileName); } } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java index 5c53c010a61b4..7725306fefc1b 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java @@ -68,7 +68,6 @@ public class ResourceMethod { private boolean isFormParamRequired; - private boolean isMultipart; private List subResourceMethods; public ResourceMethod() { @@ -76,7 +75,7 @@ public ResourceMethod() { public ResourceMethod(String httpMethod, String path, String[] produces, String streamElementType, String[] consumes, Set nameBindingNames, String name, String returnType, String simpleReturnType, MethodParameter[] parameters, - boolean blocking, boolean suspended, boolean isSse, boolean isFormParamRequired, boolean isMultipart, + boolean blocking, boolean suspended, boolean isSse, boolean isFormParamRequired, List subResourceMethods) { this.httpMethod = httpMethod; this.path = path; @@ -92,7 +91,6 @@ public ResourceMethod(String httpMethod, String path, String[] produces, String this.suspended = suspended; this.isSse = isSse; this.isFormParamRequired = isFormParamRequired; - this.isMultipart = isMultipart; this.subResourceMethods = subResourceMethods; } @@ -226,15 +224,6 @@ public ResourceMethod setFormParamRequired(boolean isFormParamRequired) { return this; } - public boolean isMultipart() { - return isMultipart; - } - - public ResourceMethod setMultipart(boolean isMultipart) { - this.isMultipart = isMultipart; - return this; - } - public ResourceMethod setStreamElementType(String streamElementType) { this.streamElementType = streamElementType; return this; diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java index de58afdeb503d..f9955f940c370 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java @@ -6,6 +6,7 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; @@ -207,6 +208,28 @@ public static Type getEffectiveReturnType(Type returnType) { throw new UnsupportedOperationException("Endpoint return type not supported yet: " + returnType); } + public static Type getMultipartElementType(Type paramType) { + if (paramType instanceof Class) { + if (((Class) paramType).isArray()) { + return ((Class) paramType).getComponentType(); + } + return paramType; + } + if (paramType instanceof ParameterizedType) { + ParameterizedType type = (ParameterizedType) paramType; + Type firstTypeArgument = type.getActualTypeArguments()[0]; + if (type.getRawType() == List.class) { + return firstTypeArgument; + } + return paramType; + } + if (paramType instanceof GenericArrayType) { + GenericArrayType type = (GenericArrayType) paramType; + return type.getGenericComponentType(); + } + throw new UnsupportedOperationException("Endpoint return type not supported yet: " + paramType); + } + public static Class getRawType(Type type) { if (type instanceof Class) return (Class) type; diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 419555c144dba..7576168d51aa6 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java @@ -4,6 +4,12 @@ public interface FileUpload extends FilePart { + /** + * Use this constant as form parameter name in order to get all file uploads from a multipart form, regardless of their + * names + */ + public final static String ALL = "*"; + default Path uploadedFile() { return filePath(); } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java index bea36bfcf31fa..cad47734a1ec4 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java @@ -36,6 +36,7 @@ import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveInterceptorScanner; +import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveParameterContainerScanner; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner; import org.jboss.resteasy.reactive.common.processor.scanning.SerializerScanningResult; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; @@ -194,6 +195,8 @@ public ScanResult scan() { additionalResourcePaths); SerializerScanningResult serializerScanningResult = ResteasyReactiveScanner.scanForSerializers(index, applicationScanningResult); + Set parameterContainers = ResteasyReactiveParameterContainerScanner.scanParameterContainers(index, + applicationScanningResult); AdditionalReaders readers = new AdditionalReaders(); AdditionalWriters writers = new AdditionalWriters(); @@ -207,6 +210,7 @@ public ScanResult scan() { .addContextTypes(contextTypes) .setAnnotationsTransformers(annotationsTransformers) .setScannedResourcePaths(resources.getScannedResourcePaths()) + .addParameterContainerTypes(parameterContainers) .setClassLevelExceptionMappers(new HashMap<>()) .setAdditionalReaders(readers) .setAdditionalWriters(writers) diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index b64b2b9ddab57..247375f997f6b 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -22,6 +22,9 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.SORTED_SET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ZONED_DATE_TIME; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -56,6 +59,7 @@ import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import org.jboss.resteasy.reactive.server.core.parameters.converters.ArrayConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.CharParamConverter; @@ -299,7 +303,7 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf currentInjectableBean.setFieldExtractorsCount(fieldExtractors.size()); if ((fieldInjectionHandler != null) && (!fieldExtractors.isEmpty() || superTypeIsInjectable)) { - fieldInjectionHandler.handleFieldInjection(currentTypeName, fieldExtractors, superTypeIsInjectable); + fieldInjectionHandler.handleFieldInjection(currentTypeName, fieldExtractors, superTypeIsInjectable, this.index); } currentInjectableBean.setInjectionRequired(!fieldExtractors.isEmpty() || superTypeIsInjectable); return currentInjectableBean; @@ -310,18 +314,19 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas String elementType, boolean single, String signature) { ParameterConverterSupplier converter = parameterResult.getConverter(); DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); + String mimeType = getPartMime(parameterResult.getAnns()); return new ServerMethodParameter(name, elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), type, single, signature, converter, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - parameterResult.getCustomParameterExtractor()); + parameterResult.getCustomParameterExtractor(), mimeType); } protected void handleOtherParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { try { builder.setConverter(extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters)); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns())); } catch (Throwable throwable) { throw new RuntimeException("Could not create converter for " + elementType + " for " + builder.getErrorLocation() + " of type " + builder.getType(), throwable); @@ -331,7 +336,7 @@ protected void handleOtherParam(Map existingConverters, String e protected void handleSortedSetParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new SortedSetConverter.SortedSetSupplier(converter)); } @@ -344,7 +349,7 @@ protected void handleOptionalParam(Map existingConverters, if (genericElementType != null) { ParameterConverterSupplier genericTypeConverter = extractConverter(genericElementType, index, existingConverters, - errorLocation, hasRuntimeConverters); + errorLocation, hasRuntimeConverters, builder.getAnns()); if (LIST.toString().equals(elementType)) { converter = new ListConverter.ListSupplier(genericTypeConverter); builder.setSingle(false); @@ -362,7 +367,8 @@ protected void handleOptionalParam(Map existingConverters, if (converter == null) { // If no generic type provided or element type is not supported, then we try to use a custom runtime converter: - converter = extractConverter(elementType, index, existingConverters, errorLocation, hasRuntimeConverters); + converter = extractConverter(elementType, index, existingConverters, errorLocation, hasRuntimeConverters, + builder.getAnns()); } builder.setConverter(new OptionalConverter.OptionalSupplier(converter)); @@ -371,21 +377,21 @@ protected void handleOptionalParam(Map existingConverters, protected void handleSetParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new SetConverter.SetSupplier(converter)); } protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new ListConverter.ListSupplier(converter)); } protected void handleArrayParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new ArrayConverter.ArraySupplier(converter, elementType)); } @@ -485,7 +491,11 @@ private String contextualizeErrorMessage(String errorMessage, MethodInfo current } private ParameterConverterSupplier extractConverter(String elementType, IndexView indexView, - Map existingConverters, String errorLocation, boolean hasRuntimeConverters) { + Map existingConverters, String errorLocation, boolean hasRuntimeConverters, + Map annotations) { + // no converter if we have a RestForm mime type: this goes via message body readers in MultipartFormParamExtractor + if (getPartMime(annotations) != null) + return null; if (elementType.equals(String.class.getName())) { if (hasRuntimeConverters) return new RuntimeResolvedConverter.Supplier().setDelegate(new NoopParameterConverter.Supplier()); @@ -509,6 +519,12 @@ private ParameterConverterSupplier extractConverter(String elementType, IndexVie return new CharParamConverter.Supplier(); } else if (elementType.equals(Character.class.getName())) { return new CharacterParamConverter.Supplier(); + } else if (elementType.equals(FileUpload.class.getName()) + || elementType.equals(Path.class.getName()) + || elementType.equals(File.class.getName()) + || elementType.equals(InputStream.class.getName())) { + // this is handled by MultipartFormParamExtractor + return null; } return converterSupplierIndexerExtension.extractConverterImpl(elementType, indexView, existingConverters, errorLocation, hasRuntimeConverters); @@ -565,7 +581,7 @@ public static class Builder extends AbstractBuilder { public interface FieldInjectionIndexerExtension { void handleFieldInjection(String currentTypeName, Map fieldExtractors, - boolean superTypeIsInjectable); + boolean superTypeIsInjectable, IndexView indexView); } public interface ConverterSupplierIndexerExtension { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/injection/TransformedFieldInjectionIndexerExtension.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/injection/TransformedFieldInjectionIndexerExtension.java index 54cae9faad2ba..01ca3859d583a 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/injection/TransformedFieldInjectionIndexerExtension.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/injection/TransformedFieldInjectionIndexerExtension.java @@ -6,6 +6,7 @@ import java.util.function.Consumer; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; import org.jboss.resteasy.reactive.server.processor.ServerEndpointIndexer; import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter; import org.jboss.resteasy.reactive.server.processor.scanning.ClassInjectorTransformer; @@ -27,7 +28,7 @@ public TransformedFieldInjectionIndexerExtension( @Override public void handleFieldInjection(String currentTypeName, Map fieldExtractors, - boolean superTypeIsInjectable) { + boolean superTypeIsInjectable, IndexView indexView) { for (Map.Entry entry : fieldExtractors.entrySet()) { if (entry.getValue().getConverter() != null) { injectedClassConverterFieldConsumer @@ -36,6 +37,6 @@ public void handleFieldInjection(String currentTypeName, Map multipartInputGeneratedPopulators = new HashMap<>(); - final BiConsumer> transformations; - final ClassOutput classOutput; - - public GeneratedMultipartParamIndexerExtension(Map> transformations, - ClassOutput classOutput) { - this.transformations = transformations::put; - this.classOutput = classOutput; - } - - public GeneratedMultipartParamIndexerExtension( - BiConsumer> transformations, - ClassOutput classOutput) { - this.transformations = transformations; - this.classOutput = classOutput; - } - - @Override - public void handleMultipartParameter(ClassInfo multipartClassInfo, IndexView index) { - String className = multipartClassInfo.name().toString(); - if (multipartInputGeneratedPopulators.containsKey(className)) { - // we've already seen this class before and have done all we need to make it work - return; - } - String populatorClassName = MultipartPopulatorGenerator.generate(multipartClassInfo, classOutput, index); - multipartInputGeneratedPopulators.put(className, populatorClassName); - - // transform the multipart pojo (and any super-classes) so we can access its fields no matter what - ClassInfo currentClassInHierarchy = multipartClassInfo; - while (true) { - transformations.accept(currentClassInHierarchy.name().toString(), new MultipartTransformer(populatorClassName)); - - DotName superClassDotName = currentClassInHierarchy.superName(); - if (superClassDotName.equals(DotNames.OBJECT_NAME)) { - break; - } - ClassInfo newCurrentClassInHierarchy = index.getClassByName(superClassDotName); - if (newCurrentClassInHierarchy == null) { - break; - } - currentClassInHierarchy = newCurrentClassInHierarchy; - } - - } -} diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartFeature.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartFeature.java index bc48f7c8f9d11..9a854ef1671d2 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartFeature.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartFeature.java @@ -9,7 +9,5 @@ public class MultipartFeature extends AbstractFeatureScanner { @Override public void integrateWithIndexer(ServerEndpointIndexer.Builder builder, IndexView index) { builder.setMultipartReturnTypeIndexerExtension(new GeneratedHandlerMultipartReturnTypeIndexerExtension(classOutput)); - builder.setMultipartParameterIndexerExtension( - new GeneratedMultipartParamIndexerExtension(transformations, classOutput)); } } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java deleted file mode 100644 index 59817cd67691a..0000000000000 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java +++ /dev/null @@ -1,585 +0,0 @@ -package org.jboss.resteasy.reactive.server.processor.generation.multipart; - -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM; - -import java.io.File; -import java.io.InputStream; -import java.lang.reflect.Modifier; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import javax.ws.rs.core.MediaType; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.ParameterizedType; -import org.jboss.jandex.Type; -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.common.processor.AsmUtil; -import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; -import org.jboss.resteasy.reactive.common.processor.TypeArgMapper; -import org.jboss.resteasy.reactive.common.util.DeploymentUtils; -import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser; -import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.core.multipart.DefaultFileUpload; -import org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport; -import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext; -import org.jboss.resteasy.reactive.server.processor.util.JavaBeanUtil; -import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; - -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; - -public final class MultipartPopulatorGenerator { - - private static final Logger LOGGER = Logger.getLogger(MultipartPopulatorGenerator.class); - - private MultipartPopulatorGenerator() { - } - - /** - * Generates a class that populates a Pojo that is used as a @MultipartForm parameter in a resource method. - * The generated class is called at runtime by the {@code __quarkus_rest_inject} method of the class - * (which is added by {@link MultipartTransformer}). - * - *

- * For example for a pojo like: - * - *

-     * public class FormData {
-     *
-     *     @RestForm
-     *     @PartType(MediaType.TEXT_PLAIN)
-     *     private String foo;
-     *
-     *     @RestForm
-     *     @PartType(MediaType.APPLICATION_JSON)
-     *     public Map map;
-     *
-     *     @RestForm
-     *     @PartType(MediaType.APPLICATION_JSON)
-     *     public Person person;
-     *
-     *     @RestForm
-     *     @PartType(MediaType.TEXT_PLAIN)
-     *     private Status status;
-     *
-     *     @RestForm("htmlFile")
-     *     private FileUpload htmlPart;
-     *
-     *     @RestForm("htmlFile")
-     *     public Path xmlPart;
-     *
-     *     @RestForm
-     *     public File txtFile;
-     *
-     *     public String getFoo() {
-     *         return foo;
-     *     }
-     *
-     *     public void setFoo(String foo) {
-     *         this.foo = foo;
-     *     }
-     *
-     *     public Status getStatus() {
-     *         return status;
-     *     }
-     *
-     *     public void setStatus(Status status) {
-     *         this.status = status;
-     *     }
-     *
-     *     public FileUpload getHtmlPart() {
-     *         return htmlPart;
-     *     }
-     *
-     *     public void setHtmlPart(FileUpload htmlPart) {
-     *         this.htmlPart = htmlPart;
-     *     }
-     * }
-     * 
- * - *

- * the generated populator would look like: - * - *

-     * public class FormData_generated_populator {
-     *     private static Class map_type;
-     *     private static Type map_genericType;
-     *     private static MediaType map_mediaType;
-     *     private static Class person_type;
-     *     private static Type person_genericType;
-     *     private static MediaType person_mediaType;
-     *     private static Class status_type;
-     *     private static Type status_genericType;
-     *     private static MediaType status_mediaType;
-     *
-     *     static {
-     *         map_type = DeploymentUtils.loadClassFromTCCL("java.util.Map");
-     *         map_genericType = TypeSignatureParser.parse("Ljava/util/Map;");
-     *         map_mediaType = MediaType.valueOf("application/json");
-     *         Class var0 = DeploymentUtils.loadClassFromTCCL("org.acme.getting.started.Person");
-     *         person_type = var0;
-     *         person_genericType = var0;
-     *         person_mediaType = MediaType.valueOf("application/json");
-     *         Class var1 = DeploymentUtils.loadClassFromTCCL("org.acme.getting.started.Status");
-     *         status_type = var1;
-     *         status_genericType = var1;
-     *         status_mediaType = MediaType.valueOf("text/plain");
-     *     }
-     *
-     *     public static void populate(FormData var0, ResteasyReactiveInjectionContext var1) {
-     *         ResteasyReactiveRequestContext var3 = (ResteasyReactiveRequestContext) var1;
-     *         ServerHttpRequest var5 = var3.serverRequest();
-     *         String var2 = var5.getFormAttribute("foo");
-     *         var0.setFoo((String) var2);
-     *         QuarkusFileUpload var4 = MultipartSupport.getFileUpload("htmlFile", var3);
-     *         var0.setHtmlPart((FileUpload) var4);
-     *         String var6 = var5.getFormAttribute("map");
-     *         Class var7 = map_type;
-     *         Type var8 = map_genericType;
-     *         MediaType var9 = map_mediaType;
-     *         Object var10 = MultipartSupport.convertFormAttribute(var6, var7, var8, var9, var3);
-     *         var0.map = (Map) var10;
-     *         String var11 = var5.getFormAttribute("person");
-     *         Class var12 = person_type;
-     *         Type var13 = person_genericType;
-     *         MediaType var14 = person_mediaType;
-     *         Object var15 = MultipartSupport.convertFormAttribute(var11, var12, var13, var14, var3);
-     *         var0.person = (Person) var15;
-     *         String var16 = var5.getFormAttribute("status");
-     *         Class var17 = status_type;
-     *         Type var18 = status_genericType;
-     *         MediaType var19 = status_mediaType;
-     *         Object var20 = MultipartSupport.convertFormAttribute(var16, var17, var18, var19, var3);
-     *         var0.setStatus((Status) var20);
-     *         QuarkusFileUpload var21 = MultipartSupport.getFileUpload("txtFile", var3);
-     *         File var22;
-     *         if (var21 != null) {
-     *             var22 = var21.uploadedFile().toFile();
-     *         } else {
-     *             var22 = null;
-     *         }
-     *
-     *         var0.txtFile = (File) var22;
-     *         QuarkusFileUpload var23 = MultipartSupport.getFileUpload("htmlFile", var3);
-     *         Path var24;
-     *         if (var23 != null) {
-     *             var24 = var23.uploadedFile();
-     *         } else {
-     *             var24 = null;
-     *         }
-     *
-     *         var0.xmlPart = (Path) var24;
-     *     }
-     * }
-     * 
- */ - public static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, IndexView index) { - if (!multipartClassInfo.hasNoArgsConstructor()) { - throw new IllegalArgumentException("Classes annotated with '@MultipartForm' must contain a no-args constructor. " + - "The constructor is missing on " + multipartClassInfo.name()); - } - - String multipartClassName = multipartClassInfo.name().toString(); - String generateClassName = multipartClassName + "_generated_populator"; - try (ClassCreator cc = new ClassCreator(classOutput, generateClassName, null, Object.class.getName())) { - MethodCreator populate = cc.getMethodCreator(DotNames.POPULATE_METHOD_NAME, void.class.getName(), - multipartClassName, - ResteasyReactiveInjectionContext.class.getName()); - populate.setModifiers(Modifier.PUBLIC | Modifier.STATIC); - - MethodCreator clinit = cc.getMethodCreator("", void.class); - clinit.setModifiers(Modifier.PUBLIC | Modifier.STATIC); - - ResultHandle instanceHandle = populate.getMethodParam(0); - ResultHandle rrCtxHandle = populate.checkCast(populate.getMethodParam(1), ResteasyReactiveRequestContext.class); - ResultHandle serverReqHandle = populate.invokeVirtualMethod( - MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, "serverRequest", ServerHttpRequest.class), - rrCtxHandle); - - // go up the class hierarchy until we reach Object - ClassInfo currentClassInHierarchy = multipartClassInfo; - while (true) { - List fields = currentClassInHierarchy.fields(); - for (FieldInfo field : fields) { - if (Modifier.isStatic(field.flags())) { // nothing we need to do about static fields - continue; - } - - AnnotationInstance formParamInstance = field.annotation(REST_FORM_PARAM); - if (formParamInstance == null) { - formParamInstance = field.annotation(FORM_PARAM); - } - if (formParamInstance == null) { // fields not annotated with @RestForm or @FormParam are completely ignored - continue; - } - - boolean useFieldAccess = false; - String setterName = JavaBeanUtil.getSetterName(field.name()); - Type fieldType = field.type(); - DotName fieldDotName = fieldType.name(); - MethodInfo setter = currentClassInHierarchy.method(setterName, fieldType); - if (setter == null) { - // even if the field is private, it will be transformed to be made public - useFieldAccess = true; - } - if (!useFieldAccess && !Modifier.isPublic(setter.flags())) { - throw new IllegalArgumentException( - "Setter '" + setterName + "' of class '" + multipartClassInfo + "' must be public"); - } - - if (fieldDotName.equals(DotNames.INPUT_STREAM_READER_NAME)) { - // don't support InputStream as it's too easy to get into trouble - throw new IllegalArgumentException( - "InputStreamReader are not supported as a field type of a Multipart POJO class. Offending field is '" - + field.name() + "' of class '" + multipartClassName + "'"); - } - - String formAttrName = field.name(); - boolean formAttrNameSet = false; - AnnotationValue formParamValue = formParamInstance.value(); - if (formParamValue != null) { - formAttrNameSet = true; - formAttrName = formParamValue.asString(); - } - - // TODO: not sure this is correct, but it seems to be what RESTEasy does and it also makes most sense in the context of a POJO - String partType = MediaType.TEXT_PLAIN; - AnnotationInstance partTypeInstance = field.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME); - if (partTypeInstance != null) { - AnnotationValue partTypeValue = partTypeInstance.value(); - if (partTypeValue != null) { - partType = partTypeValue.asString(); - } - } - - ResultHandle formAttrNameHandle = populate.load(formAttrName); - AssignableResultHandle resultVariableHandle = populate.createVariable(Object.class); - - if (isFileRelatedType(fieldDotName)) { - // uploaded file are present in the RoutingContext and are extracted using MultipartSupport#getFileUpload - - ResultHandle fileUploadHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getFileUpload", DefaultFileUpload.class, - String.class, ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle); - if (fieldDotName.equals(DotNames.FIELD_UPLOAD_NAME)) { - populate.assign(resultVariableHandle, fileUploadHandle); - } else if (fieldDotName.equals(DotNames.PATH_NAME) || fieldDotName.equals(DotNames.FILE_NAME)) { - BranchResult fileUploadNullBranch = populate.ifNull(fileUploadHandle); - BytecodeCreator fileUploadNullTrue = fileUploadNullBranch.trueBranch(); - fileUploadNullTrue.assign(resultVariableHandle, populate.loadNull()); - fileUploadNullTrue.breakScope(); - BytecodeCreator fileUploadFalse = fileUploadNullBranch.falseBranch(); - ResultHandle pathHandle = fileUploadFalse.invokeVirtualMethod( - MethodDescriptor.ofMethod(DefaultFileUpload.class, "uploadedFile", Path.class), - fileUploadHandle); - if (fieldDotName.equals(DotNames.PATH_NAME)) { - fileUploadFalse.assign(resultVariableHandle, pathHandle); - } else { - fileUploadFalse.assign(resultVariableHandle, fileUploadFalse.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Path.class, "toFile", File.class), pathHandle)); - } - fileUploadFalse.breakScope(); - } - } else if (isListOfFileUpload(fieldType)) { - // in this case we allow injection of all the uploaded file as long as a name - // was not provided in @RestForm (which makes no semantic sense) - if (formAttrNameSet) { - Type fieldGenericType = fieldType.asParameterizedType().arguments().get(0); - ResultHandle fileUploadHandle; - if (fieldGenericType.name().equals(DotNames.FIELD_UPLOAD_NAME)) { - fileUploadHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getFileUploads", - List.class, - String.class, ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle); - } else if (fieldGenericType.name().equals(DotNames.PATH_NAME)) { - fileUploadHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getJavaPathFileUploads", - List.class, - String.class, ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle); - } else if (fieldGenericType.name().equals(DotNames.FILE_NAME)) { - fileUploadHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getJavaIOFileUploads", - List.class, - String.class, ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle); - } else { - throw new IllegalArgumentException( - "Unhandled genetic type '" + fieldGenericType.name().toString() + "'"); - } - populate.assign(resultVariableHandle, fileUploadHandle); - } else { - ResultHandle allFileUploadsHandle = populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "getFileUploads", List.class, - ResteasyReactiveRequestContext.class), - rrCtxHandle); - populate.assign(resultVariableHandle, allFileUploadsHandle); - } - } else if (partType.equals(MediaType.APPLICATION_OCTET_STREAM)) { - if (fieldType.kind() == Type.Kind.ARRAY - && fieldType.asArrayType().component().name().equals(DotNames.BYTE_NAME)) { - populate.assign(resultVariableHandle, - populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class, - "getSingleFileUploadAsArrayBytes", byte[].class, String.class, - ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle)); - } else if (fieldDotName.equals(DotNames.INPUT_STREAM_NAME)) { - populate.assign(resultVariableHandle, - populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class, - "getSingleFileUploadAsInputStream", InputStream.class, String.class, - ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle)); - } else if (fieldDotName.equals(DotNames.STRING_NAME)) { - populate.assign(resultVariableHandle, - populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class, - "getSingleFileUploadAsString", String.class, String.class, - ResteasyReactiveRequestContext.class), - formAttrNameHandle, rrCtxHandle)); - } else { - throw new IllegalArgumentException( - "Unsupported type to read multipart file contents. Offending field is '" - + field.name() + "' of class '" - + field.declaringClass().name() - + "'. If you need to read the contents of the uploaded file, use 'Path' or 'File' as the field type and use File IO APIs to read the bytes, while making sure you annotate the endpoint with '@Blocking'"); - } - } else { - // this is a common enough mistake, so let's provide a good error message - failIfFileTypeUsedAsGenericType(field, fieldType, fieldDotName); - - if (fieldType.kind() == Type.Kind.ARRAY) { - if (fieldType.asArrayType().component().name().equals(DotNames.BYTE_NAME)) { - throw new IllegalArgumentException( - "'byte[]' cannot be used to read multipart file contents. Offending field is '" - + field.name() + "' of class '" - + field.declaringClass().name() - + "'. If you need to read the contents of the uploaded file, use 'Path' or 'File' as the field type and use File IO APIs to read the bytes, while making sure you annotate the endpoint with '@Blocking'"); - } - } - if (fieldDotName.equals(DotNames.STRING_NAME) && partType.equals(MediaType.TEXT_PLAIN)) { - // in this case all we need to do is read the value of the form attribute - - populate.assign(resultVariableHandle, - populate.invokeVirtualMethod(MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, - "getFormParameter", Object.class, String.class, boolean.class, boolean.class), - rrCtxHandle, - formAttrNameHandle, populate.load(true), populate.load(false))); - } else if ((fieldType.kind() == Type.Kind.ARRAY) || isList(fieldType)) { - // Media Type static field - FieldDescriptor mediaTypeField = cc.getFieldCreator(field.name() + "_mediaType", MediaType.class) - .setModifiers(Modifier.PRIVATE | Modifier.STATIC).getFieldDescriptor(); - clinit.writeStaticField(mediaTypeField, clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(MediaType.class, "valueOf", MediaType.class, String.class), - clinit.load(partType))); - - // Component Type static field - boolean isArray = fieldType.kind() == Type.Kind.ARRAY; - Type componentType = isArray ? fieldType.asArrayType().component() - : fieldType.asParameterizedType().arguments().get(0); - ClassInfo componentClassInfo = index.getClassByName(componentType.name()); - FieldDescriptor genericComponentTypeField = cc - .getFieldCreator(field.name() + "_genericComponentType", java.lang.reflect.Type.class) - .setModifiers(Modifier.PRIVATE | Modifier.STATIC).getFieldDescriptor(); - TypeArgMapper componentTypeArgMapper = new TypeArgMapper(componentClassInfo, index); - ResultHandle genericComponentTypeHandle = clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(TypeSignatureParser.class, "parse", - java.lang.reflect.Type.class, String.class), - clinit.load(AsmUtil.getSignature(componentType, componentTypeArgMapper))); - clinit.writeStaticField(genericComponentTypeField, genericComponentTypeHandle); - - ResultHandle formCollectionValueHandle = populate.invokeVirtualMethod( - MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, - "getFormParameter", Object.class, String.class, boolean.class, boolean.class), - rrCtxHandle, - formAttrNameHandle, populate.load(false), populate.load(false)); - - ResultHandle resultHandle; - ResultHandle formCollectionSizeHandle = populate.invokeInterfaceMethod(MethodDescriptor.ofMethod( - Collection.class, "size", int.class), formCollectionValueHandle); - if (isArray) { - resultHandle = populate.newArray(componentType.toString(), formCollectionSizeHandle); - } else { - resultHandle = populate.newInstance(MethodDescriptor.ofConstructor(ArrayList.class, int.class), - formCollectionSizeHandle); - } - - ResultHandle formValueIterator = populate.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Collection.class, "iterator", Iterator.class), - populate.checkCast(formCollectionValueHandle, Collection.class)); - AssignableResultHandle collectionIndex = populate.createVariable(int.class); - populate.assign(collectionIndex, populate.load(0)); - - BytecodeCreator loopIterator = populate.whileLoop(c -> c.ifTrue( - c.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class), - formValueIterator))) - .block(); - ResultHandle formValueAsString = loopIterator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Iterator.class, "next", Object.class), formValueIterator); - - ResultHandle convertedValueHandle = loopIterator.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "convertFormAttribute", - Object.class, - String.class, - Class.class, - java.lang.reflect.Type.class, MediaType.class, - ResteasyReactiveRequestContext.class, String.class), - loopIterator.checkCast(formValueAsString, String.class), - loopIterator.loadClass(componentType.toString()), - loopIterator.readStaticField(genericComponentTypeField), - loopIterator.readStaticField(mediaTypeField), - rrCtxHandle, formAttrNameHandle); - if (isArray) { - loopIterator.writeArrayValue(resultHandle, collectionIndex, convertedValueHandle); - } else { - loopIterator.invokeVirtualMethod( - MethodDescriptor.ofMethod(ArrayList.class, "add", boolean.class, - Object.class), - resultHandle, convertedValueHandle); - } - - loopIterator.assign(collectionIndex, loopIterator.increment(collectionIndex)); - - populate.assign(resultVariableHandle, resultHandle); - } else { - // we need to use the field type and the media type to locate a MessageBodyReader - - FieldDescriptor typeField = cc.getFieldCreator(field.name() + "_type", Class.class) - .setModifiers(Modifier.PRIVATE | Modifier.STATIC).getFieldDescriptor(); - FieldDescriptor genericTypeField = cc - .getFieldCreator(field.name() + "_genericType", java.lang.reflect.Type.class) - .setModifiers(Modifier.PRIVATE | Modifier.STATIC).getFieldDescriptor(); - FieldDescriptor mediaTypeField = cc.getFieldCreator(field.name() + "_mediaType", MediaType.class) - .setModifiers(Modifier.PRIVATE | Modifier.STATIC).getFieldDescriptor(); - - ResultHandle typeHandle = clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(DeploymentUtils.class, "loadClass", Class.class, String.class), - clinit.load(fieldDotName.toString())); - clinit.writeStaticField(typeField, typeHandle); - if ((fieldType.kind() != Type.Kind.CLASS) && (fieldType.kind() != Type.Kind.PRIMITIVE)) { - // in order to pass the generic type we use the same trick with capturing and at runtime - // parsing the signature - TypeArgMapper typeArgMapper = new TypeArgMapper(field.declaringClass(), index); - ResultHandle genericTypeHandle = clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(TypeSignatureParser.class, "parse", - java.lang.reflect.Type.class, String.class), - clinit.load(AsmUtil.getSignature(fieldType, typeArgMapper))); - clinit.writeStaticField(genericTypeField, genericTypeHandle); - } else { - clinit.writeStaticField(genericTypeField, typeHandle); - } - clinit.writeStaticField(mediaTypeField, clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(MediaType.class, "valueOf", MediaType.class, String.class), - clinit.load(partType))); - - ResultHandle formStrValueHandle = populate.invokeVirtualMethod( - MethodDescriptor.ofMethod(ResteasyReactiveRequestContext.class, - "getFormParameter", Object.class, String.class, boolean.class, boolean.class), - rrCtxHandle, - formAttrNameHandle, populate.load(true), populate.load(false)); - - populate.assign(resultVariableHandle, populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartSupport.class, "convertFormAttribute", Object.class, - String.class, - Class.class, - java.lang.reflect.Type.class, MediaType.class, - ResteasyReactiveRequestContext.class, String.class), - formStrValueHandle, populate.readStaticField(typeField), - populate.readStaticField(genericTypeField), - populate.readStaticField(mediaTypeField), - rrCtxHandle, formAttrNameHandle)); - } - } - - if (useFieldAccess) { - BytecodeCreator bc = populate; - if (fieldType.kind() == Type.Kind.PRIMITIVE) { - bc = populate.ifNull(resultVariableHandle).falseBranch(); - } - bc.writeInstanceField(FieldDescriptor.of(currentClassInHierarchy.name().toString(), field.name(), - fieldDotName.toString()), instanceHandle, resultVariableHandle); - } else { - BytecodeCreator bc = populate; - if (fieldType.kind() == Type.Kind.PRIMITIVE) { - bc = populate.ifNull(resultVariableHandle).falseBranch(); - } - bc.invokeVirtualMethod(MethodDescriptor.ofMethod(currentClassInHierarchy.name().toString(), - setterName, void.class, fieldDotName.toString()), instanceHandle, resultVariableHandle); - } - } - - DotName superClassDotName = currentClassInHierarchy.superName(); - if (superClassDotName.equals(DotNames.OBJECT_NAME)) { - break; - } - ClassInfo newCurrentClassInHierarchy = index.getClassByName(superClassDotName); - if (newCurrentClassInHierarchy == null) { - if (!superClassDotName.toString().startsWith("java.")) { - LOGGER.warn("Class '" + superClassDotName + "' which is a parent class of '" - + currentClassInHierarchy.name() - + "' is not part of the Jandex index so its fields will be ignored. If you intended to include these fields, consider making the dependency part of the Jandex index by following the advice at: https://quarkus.io/guides/cdi-reference#bean_discovery"); - } - break; - } - currentClassInHierarchy = newCurrentClassInHierarchy; - } - clinit.returnValue(null); - populate.returnValue(null); - } - return generateClassName; - } - - private static boolean isFileRelatedType(DotName type) { - return type.equals(DotNames.FIELD_UPLOAD_NAME) || type.equals(DotNames.PATH_NAME) || type.equals(DotNames.FILE_NAME); - } - - private static boolean isListOfFileUpload(Type fieldType) { - if ((fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) && fieldType.name().equals(ResteasyReactiveDotNames.LIST)) { - ParameterizedType parameterizedType = fieldType.asParameterizedType(); - if (!parameterizedType.arguments().isEmpty()) { - DotName argTypeDotName = parameterizedType.arguments().get(0).name(); - return isFileRelatedType(argTypeDotName); - } - } - return false; - } - - private static boolean isList(Type fieldType) { - return (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) && fieldType.name() - .equals(ResteasyReactiveDotNames.LIST); - } - - private static void failIfFileTypeUsedAsGenericType(FieldInfo field, Type fieldType, DotName fieldDotName) { - if (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) { - ParameterizedType parameterizedType = fieldType.asParameterizedType(); - if (!parameterizedType.arguments().isEmpty()) { - DotName argTypeDotName = parameterizedType.arguments().get(0).name(); - if (isFileRelatedType(argTypeDotName)) { - throw new IllegalArgumentException("Type '" + argTypeDotName.withoutPackagePrefix() - + "' cannot be used as a generic type of '" - + fieldDotName.withoutPackagePrefix() + "'. Offending field is '" + field.name() + "' of class '" - + field.declaringClass().name() + "'"); - } - } - } - } -} diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java index 212ebfa4489ca..d93ebec5de522 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java @@ -1,5 +1,10 @@ package org.jboss.resteasy.reactive.server.processor.scanning; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.BiFunction; @@ -7,13 +12,27 @@ import javax.ws.rs.BadRequestException; import javax.ws.rs.NotFoundException; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.PrimitiveType.Primitive; import org.jboss.jandex.Type.Kind; +import org.jboss.resteasy.reactive.common.model.ParameterType; import org.jboss.resteasy.reactive.common.processor.AsmUtil; import org.jboss.resteasy.reactive.common.processor.IndexedParameter; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; +import org.jboss.resteasy.reactive.common.processor.TypeArgMapper; +import org.jboss.resteasy.reactive.common.util.DeploymentUtils; +import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.Deployment; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.DefaultFileUpload; +import org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport; +import org.jboss.resteasy.reactive.server.core.parameters.MultipartFormParamExtractor; +import org.jboss.resteasy.reactive.server.core.parameters.converters.ArrayConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.DelegatingParameterConverterSupplier; import org.jboss.resteasy.reactive.server.core.parameters.converters.ParameterConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.ParameterConverterSupplier; @@ -58,6 +77,53 @@ public class ClassInjectorTransformer implements BiFunction fieldExtractors; private final boolean superTypeIsInjectable; @@ -65,34 +131,54 @@ public class ClassInjectorTransformer implements BiFunction fieldExtractors, boolean superTypeIsInjectable, - boolean requireCreateBeanParams) { + boolean requireCreateBeanParams, IndexView indexView) { this.fieldExtractors = fieldExtractors; this.superTypeIsInjectable = superTypeIsInjectable; this.requireCreateBeanParams = requireCreateBeanParams; + this.indexView = indexView; } @Override public ClassVisitor apply(String classname, ClassVisitor visitor) { - return new ClassInjectorVisitor(Gizmo.ASM_API_VERSION, visitor, fieldExtractors, superTypeIsInjectable, - requireCreateBeanParams); + return new ClassInjectorVisitor(Gizmo.ASM_API_VERSION, visitor, + fieldExtractors, superTypeIsInjectable, + requireCreateBeanParams, indexView); } static class ClassInjectorVisitor extends ClassVisitor { private Map fieldExtractors; + private Map partTypes; private String thisName; private boolean superTypeIsInjectable; private String superTypeName; private final boolean requireCreateBeanParams; + private IndexView indexView; + private boolean seenClassInit; public ClassInjectorVisitor(int api, ClassVisitor classVisitor, Map fieldExtractors, - boolean superTypeIsInjectable, boolean requireCreateBeanParams) { + boolean superTypeIsInjectable, boolean requireCreateBeanParams, IndexView indexView) { super(api, classVisitor); this.fieldExtractors = fieldExtractors; this.superTypeIsInjectable = superTypeIsInjectable; this.requireCreateBeanParams = requireCreateBeanParams; + this.indexView = indexView; + this.partTypes = new HashMap<>(); + // collect part types + for (Entry entry : fieldExtractors.entrySet()) { + FieldInfo fieldInfo = entry.getKey(); + ServerIndexedParameter extractor = entry.getValue(); + switch (extractor.getType()) { + case FORM: + MultipartFormParamExtractor.Type multipartFormType = getMultipartFormType(extractor); + if (multipartFormType == MultipartFormParamExtractor.Type.PartType) { + this.partTypes.put(fieldInfo, extractor); + } + } + } } @Override @@ -113,6 +199,26 @@ public void visit(int version, int access, String name, String signature, String thisName = name; } + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + // if we have part types and we're doing the class init method, add our init for their special fields + if (!partTypes.isEmpty() && name.equals("")) { + this.seenClassInit = true; + return new MethodVisitor(Gizmo.ASM_API_VERSION, mv) { + @Override + public void visitEnd() { + for (Entry entry : partTypes.entrySet()) { + generateMultipartFormStaticInit(this, entry.getKey(), entry.getValue()); + } + super.visitEnd(); + } + }; + } + return mv; + } + @Override public void visitEnd() { // FIXME: handle setters @@ -199,11 +305,17 @@ public void visitEnd() { injectMethod.visitMaxs(0, 0); // now generate initialisers for every field converter + // and type info for Form bodies for (Entry entry : fieldExtractors.entrySet()) { FieldInfo fieldInfo = entry.getKey(); ServerIndexedParameter extractor = entry.getValue(); switch (extractor.getType()) { case FORM: + MultipartFormParamExtractor.Type multipartFormType = getMultipartFormType(extractor); + if (multipartFormType == MultipartFormParamExtractor.Type.PartType) { + generateMultipartFormFields(fieldInfo, extractor); + } + // fall-through on purpose case HEADER: case MATRIX: case COOKIE: @@ -219,9 +331,79 @@ public void visitEnd() { } } + if (!seenClassInit && !partTypes.isEmpty()) { + // add a class init method for the part types special fields + MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); + for (Entry entry : partTypes.entrySet()) { + generateMultipartFormStaticInit(mv, entry.getKey(), entry.getValue()); + } + mv.visitInsn(Opcodes.RETURN); + mv.visitEnd(); + mv.visitMaxs(0, 0); + } super.visitEnd(); } + private void generateMultipartFormFields(FieldInfo fieldInfo, ServerIndexedParameter extractor) { + /* + * private static Class map_type; + * private static Type map_genericType; + * private static MediaType map_mediaType; + */ + super.visitField(Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE, fieldInfo.name() + "_type", CLASS_DESCRIPTOR, null, null) + .visitEnd(); + super.visitField(Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE, fieldInfo.name() + "_genericType", TYPE_DESCRIPTOR, null, + null).visitEnd(); + super.visitField(Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE, fieldInfo.name() + "_mediaType", MEDIA_TYPE_DESCRIPTOR, + null, null).visitEnd(); + } + + private void generateMultipartFormStaticInit(MethodVisitor mv, FieldInfo fieldInfo, ServerIndexedParameter extractor) { + /* + * generic: + * map_type = DeploymentUtils.loadClass("java.util.Map"); + * map_genericType = TypeSignatureParser.parse("Ljava/util/Map;"); + * map_mediaType = MediaType.valueOf("application/json"); + * dumb class: + * Class var0 = DeploymentUtils.loadClass("org.acme.getting.started.Person"); + * person_type = var0; + * person_genericType = var0; + * person_mediaType = MediaType.valueOf("application/json"); + */ + org.jboss.jandex.Type type = fieldInfo.type(); + // extract the component type if not single + if (!extractor.isSingle()) { + boolean isArray = type.kind() == org.jboss.jandex.Type.Kind.ARRAY; + // it's T[] or List + type = isArray ? type.asArrayType().component() + : type.asParameterizedType().arguments().get(0); + } + // type + mv.visitLdcInsn(type.name().toString()); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, DEPLOYMENT_UTILS_BINARY_NAME, "loadClass", + "(" + STRING_DESCRIPTOR + ")" + CLASS_DESCRIPTOR, false); + mv.visitFieldInsn(Opcodes.PUTSTATIC, this.thisName, fieldInfo.name() + "_type", CLASS_DESCRIPTOR); + // generic type + if ((type.kind() != org.jboss.jandex.Type.Kind.CLASS) && (type.kind() != org.jboss.jandex.Type.Kind.PRIMITIVE)) { + TypeArgMapper typeArgMapper = new TypeArgMapper(fieldInfo.declaringClass(), indexView); + String signature = AsmUtil.getSignature(type, typeArgMapper); + mv.visitLdcInsn(signature); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, TYPE_DESCRIPTOR_PARSER_BINARY_NAME, "parse", + "(" + STRING_DESCRIPTOR + ")" + TYPE_DESCRIPTOR, false); + mv.visitFieldInsn(Opcodes.PUTSTATIC, this.thisName, fieldInfo.name() + "_genericType", TYPE_DESCRIPTOR); + } else { + mv.visitFieldInsn(Opcodes.GETSTATIC, this.thisName, fieldInfo.name() + "_type", CLASS_DESCRIPTOR); + mv.visitFieldInsn(Opcodes.PUTSTATIC, this.thisName, fieldInfo.name() + "_genericType", TYPE_DESCRIPTOR); + } + // media type + // this must not be null, otherwise it's not a part type + String mediaType = extractor.getAnns().get(ResteasyReactiveDotNames.PART_TYPE_NAME).value().asString(); + mv.visitLdcInsn(mediaType); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, MEDIA_TYPE_BINARY_NAME, "valueOf", + "(" + STRING_DESCRIPTOR + ")" + MEDIA_TYPE_DESCRIPTOR, false); + mv.visitFieldInsn(Opcodes.PUTSTATIC, this.thisName, fieldInfo.name() + "_mediaType", MEDIA_TYPE_DESCRIPTOR); + } + private void generateConverterInitMethod(FieldInfo fieldInfo, ParameterConverterSupplier converter, boolean single) { String converterFieldName = INIT_CONVERTER_FIELD_NAME + fieldInfo.name(); @@ -263,7 +445,6 @@ private void generateConverterInitMethod(FieldInfo fieldInfo, ParameterConverter "(Ljava/lang/Class;Ljava/lang/String;Z)" + PARAMETER_CONVERTER_DESCRIPTOR, false); // now if we have a backup delegate, let's call it - // stack: [converter] if (delegateBinaryName != null) { // check if we have a delegate @@ -304,8 +485,17 @@ private void generateConverterInitMethod(FieldInfo fieldInfo, ParameterConverter // [instance, converter, instance] initConverterMethod.visitInsn(Opcodes.SWAP); // [instance, instance, converter] - initConverterMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, delegatorBinaryName, "", - "(" + PARAMETER_CONVERTER_DESCRIPTOR + ")V", false); + // array converter wants the array instance type + if (converter instanceof ArrayConverter.ArraySupplier) { + org.jboss.jandex.Type componentType = fieldInfo.type().asArrayType().component(); + initConverterMethod.visitLdcInsn(componentType.name().toString('.')); + // [instance, instance, converter, componentType] + initConverterMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, delegatorBinaryName, "", + "(" + PARAMETER_CONVERTER_DESCRIPTOR + STRING_DESCRIPTOR + ")V", false); + } else { + initConverterMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, delegatorBinaryName, "", + "(" + PARAMETER_CONVERTER_DESCRIPTOR + ")V", false); + } } // store the converter in the static field @@ -328,6 +518,7 @@ private ParameterConverterSupplier removeRuntimeResolvedConverterDelegate(Parame private void injectParameterWithConverter(MethodVisitor injectMethod, String methodName, FieldInfo fieldInfo, ServerIndexedParameter extractor, boolean extraSingleParameter, boolean extraEncodedParam, boolean encoded) { + // spec says: /* * 3.2 Fields and Bean Properties @@ -358,7 +549,12 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met break; } // push the parameter value - loadParameter(injectMethod, methodName, extractor, extraSingleParameter, extraEncodedParam, encoded); + MultipartFormParamExtractor.Type multipartType = getMultipartFormType(extractor); + if (multipartType == null) { + loadParameter(injectMethod, methodName, extractor, extraSingleParameter, extraEncodedParam, encoded); + } else { + loadMultipartParameter(injectMethod, fieldInfo, extractor, multipartType); + } Label valueWasNull = null; if (!extractor.isOptional()) { valueWasNull = new Label(); @@ -375,7 +571,8 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met AsmUtil.unboxIfRequired(injectMethod, fieldInfo.type()); } else { // FIXME: this is totally wrong wrt. generics - injectMethod.visitTypeInsn(Opcodes.CHECKCAST, fieldInfo.type().name().toString('/')); + // Do not replace this with toString('/') because it doesn't use the given separator for array types + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, fieldInfo.type().name().toString().replace('.', '/')); } // store our param field injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), @@ -433,6 +630,259 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met injectMethod.visitLabel(endLabel); } + private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldInfo, ServerIndexedParameter param, + MultipartFormParamExtractor.Type multipartType) { + switch (multipartType) { + case String: + /* + * return single ? MultipartSupport.getString(name, context) + * : MultipartSupport.getStrings(name, context); + */ + invokeMultipartSupport(param, injectMethod, "getString", STRING_DESCRIPTOR); + break; + case ByteArray: + /* + * return single ? MultipartSupport.getByteArray(name, context) + * : MultipartSupport.getByteArrays(name, context); + */ + invokeMultipartSupport(param, injectMethod, "getByteArray", BYTE_ARRAY_DESCRIPTOR); + break; + case InputStream: + /* + * return single ? MultipartSupport.getInputStream(name, context) + * : MultipartSupport.getInputStreams(name, context); + */ + invokeMultipartSupport(param, injectMethod, "getInputStream", INPUT_STREAM_DESCRIPTOR); + break; + case FileUpload: + /* + * // special case + * if (name.equals(FileUpload.ALL)) + * return MultipartSupport.getFileUploads(context); + * return single ? MultipartSupport.getFileUpload(name, context) + * : MultipartSupport.getFileUploads(name, context); + */ + if (param.getName().equals(FileUpload.ALL)) { + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUploads", + "(" + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, false); + } else { + invokeMultipartSupport(param, injectMethod, "getFileUpload", DEFAULT_FILE_UPLOAD_DESCRIPTOR); + } + break; + case File: + /* + * if (single) { + * FileUpload upload = MultipartSupport.getFileUpload(name, context); + * return upload != null ? upload.uploadedFile().toFile() : null; + * } else { + * return MultipartSupport.getJavaIOFileUploads(name, context); + * } + */ + if (param.isSingle()) { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUpload", + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + + DEFAULT_FILE_UPLOAD_DESCRIPTOR, + false); + Label ifNull = new Label(); + Label endIf = new Label(); + // dup for the ifnull + injectMethod.visitInsn(Opcodes.DUP); + injectMethod.visitJumpInsn(Opcodes.IFNULL, ifNull); + // if not null + injectMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, DEFAULT_FILE_UPLOAD_BINARY_NAME, "uploadedFile", + "()" + PATH_DESCRIPTOR, false); + injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, PATH_BINARY_NAME, "toFile", + "()" + FILE_DESCRIPTOR, true); + injectMethod.visitJumpInsn(Opcodes.GOTO, endIf); + // else + injectMethod.visitLabel(ifNull); + // get rid of the null file upload + injectMethod.visitInsn(Opcodes.POP); + injectMethod.visitInsn(Opcodes.ACONST_NULL); + injectMethod.visitLabel(endIf); + } else { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, + "getJavaIOFileUploads", + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, + false); + } + break; + case Path: + /* + * if (single) { + * FileUpload upload = MultipartSupport.getFileUpload(name, context); + * return upload != null ? upload.uploadedFile() : null; + * } else { + * return MultipartSupport.getJavaPathFileUploads(name, context); + * } + */ + if (param.isSingle()) { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUpload", + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + + DEFAULT_FILE_UPLOAD_DESCRIPTOR, + false); + Label ifNull = new Label(); + Label endIf = new Label(); + injectMethod.visitInsn(Opcodes.DUP); + injectMethod.visitJumpInsn(Opcodes.IFNULL, ifNull); + // if not null + injectMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, DEFAULT_FILE_UPLOAD_BINARY_NAME, "uploadedFile", + "()" + PATH_DESCRIPTOR, false); + injectMethod.visitJumpInsn(Opcodes.GOTO, endIf); + // else + injectMethod.visitLabel(ifNull); + // get rid of the null file upload + injectMethod.visitInsn(Opcodes.POP); + injectMethod.visitInsn(Opcodes.ACONST_NULL); + injectMethod.visitLabel(endIf); + } else { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, + "getJavaPathFileUploads", + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, + false); + } + break; + case PartType: + /* + * if (single) { + * String param = (String) context.getFormParameter(name, true, false); + * return MultipartSupport.convertFormAttribute(param, typeClass, genericType, MediaType.valueOf(mimeType), + * context, + * name); + * } else { + * List params = (List) context.getFormParameter(name, false, false); + * return MultipartSupport.convertFormAttributes(params, typeClass, genericType, + * MediaType.valueOf(mimeType), context, name); + * } + */ + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + // name + injectMethod.visitLdcInsn(param.getName()); + // single + injectMethod.visitLdcInsn(param.isSingle()); + // encoded + injectMethod.visitLdcInsn(false); + injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, QUARKUS_REST_INJECTION_CONTEXT_BINARY_NAME, + "getFormParameter", + "(Ljava/lang/String;ZZ)Ljava/lang/Object;", true); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, param.isSingle() ? STRING_BINARY_NAME : LIST_BINARY_NAME); + // class, generic type, media type, context, name + injectMethod.visitFieldInsn(Opcodes.GETSTATIC, this.thisName, fieldInfo.name() + "_type", CLASS_DESCRIPTOR); + injectMethod.visitFieldInsn(Opcodes.GETSTATIC, this.thisName, fieldInfo.name() + "_genericType", + TYPE_DESCRIPTOR); + injectMethod.visitFieldInsn(Opcodes.GETSTATIC, this.thisName, fieldInfo.name() + "_mediaType", + MEDIA_TYPE_DESCRIPTOR); + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitLdcInsn(param.getName()); + String firstParamDescriptor; + String returnDescriptor; + String methodName; + if (param.isSingle()) { + firstParamDescriptor = STRING_DESCRIPTOR; + returnDescriptor = OBJECT_DESCRIPTOR; + methodName = "convertFormAttribute"; + } else { + firstParamDescriptor = LIST_DESCRIPTOR; + returnDescriptor = LIST_DESCRIPTOR; + methodName = "convertFormAttributes"; + } + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, methodName, + "(" + firstParamDescriptor + CLASS_DESCRIPTOR + TYPE_DESCRIPTOR + MEDIA_TYPE_DESCRIPTOR + + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + STRING_DESCRIPTOR + ")" + returnDescriptor, + false); + break; + default: + throw new RuntimeException("Unknown multipart type: " + multipartType); + } + } + + private void invokeMultipartSupport(ServerIndexedParameter param, MethodVisitor injectMethod, + String singleOperationName, String singleOperationReturnDescriptor) { + if (param.isSingle()) { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, singleOperationName, + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + + singleOperationReturnDescriptor, + false); + + } else { + // name param + injectMethod.visitLdcInsn(param.getName()); + // ctx param + injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, singleOperationName + "s", + "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, + false); + } + } + + private MultipartFormParamExtractor.Type getMultipartFormType(ServerIndexedParameter param) { + if (param.getType() != ParameterType.FORM) { + // not a multipart type + return null; + } + AnnotationInstance partType = param.getAnns().get(ResteasyReactiveDotNames.PART_TYPE_NAME); + String mimeType = null; + if (partType != null && partType.value() != null) { + mimeType = partType.value().asString(); + // remove what ends up being the default + if (MediaType.TEXT_PLAIN.equals(mimeType)) { + mimeType = null; + } + } + // FileUpload/File/Path have more importance than part type + if (param.getElementType().equals(FileUpload.class.getName())) { + return MultipartFormParamExtractor.Type.FileUpload; + } else if (param.getElementType().equals(File.class.getName())) { + return MultipartFormParamExtractor.Type.File; + } else if (param.getElementType().equals(Path.class.getName())) { + return MultipartFormParamExtractor.Type.Path; + } else if (param.getElementType().equals(String.class.getName())) { + return MultipartFormParamExtractor.Type.String; + } else if (param.getElementType().equals(InputStream.class.getName())) { + return MultipartFormParamExtractor.Type.InputStream; + } else if (param.getParamType().kind() == Kind.ARRAY + && param.getParamType().asArrayType().component().kind() == Kind.PRIMITIVE + && param.getParamType().asArrayType().component().asPrimitiveType().primitive() == Primitive.BYTE) { + return MultipartFormParamExtractor.Type.ByteArray; + } else if (mimeType != null && !mimeType.equals(MediaType.TEXT_PLAIN)) { + return MultipartFormParamExtractor.Type.PartType; + } else { + // not a multipart type + return null; + } + } + private void convertParameter(MethodVisitor injectMethod, ServerIndexedParameter extractor, FieldInfo fieldInfo) { ParameterConverterSupplier converter = extractor.getConverter(); if (converter != null) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java index 8a97d461d9544..5506f976f1d92 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.List; import javax.ws.rs.BadRequestException; @@ -23,6 +24,7 @@ import javax.ws.rs.ext.MessageBodyReader; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.util.Encode; import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; @@ -108,43 +110,146 @@ public static Object convertFormAttribute(String value, Class type, Type generic throw new NotSupportedException("Media type '" + mediaType + "' in multipart request is not supported"); } - public static String getSingleFileUploadAsString(String formName, ResteasyReactiveRequestContext context) { - DefaultFileUpload upload = getSingleFileUpload(formName, context); - if (upload != null) { + private static FormData.FormValue getFirstValue(String formName, ResteasyReactiveRequestContext context) { + Deque values = getValues(formName, context); + if (values != null && !values.isEmpty()) { + return values.getFirst(); + } + return null; + } + + private static Deque getValues(String formName, ResteasyReactiveRequestContext context) { + FormData form = context.getFormData(); + if (form != null) { + return form.get(formName); + } + return null; + } + + public static String getString(String formName, ResteasyReactiveRequestContext context) { + return getString(formName, context, false); + } + + public static String getString(String formName, ResteasyReactiveRequestContext context, boolean encoded) { + FormData.FormValue value = getFirstValue(formName, context); + if (value == null) { + return null; + } + // NOTE: we're not encoding it in case of file items, because multipart doesn't even use urlencoding, + // this is only for the TCK and regular form params + if (value.isFileItem()) { try { - return Files.readString(upload.filePath(), Charset.defaultCharset()); + return Files.readString(value.getFileItem().getFile(), Charset.defaultCharset()); } catch (IOException e) { throw new MultipartPartReadingException(e); } + } else { + if (encoded) + return Encode.encodeQueryParam(value.getValue()); + return value.getValue(); } + } - return null; + public static List getStrings(String formName, ResteasyReactiveRequestContext context) { + return getStrings(formName, context, false); + } + + public static List getStrings(String formName, ResteasyReactiveRequestContext context, boolean encoded) { + Deque values = getValues(formName, context); + if (values == null) { + return Collections.emptyList(); + } + List ret = new ArrayList(); + // NOTE: we're not encoding it in case of file items, because multipart doesn't even use urlencoding, + // this is only for the TCK and regular form params + for (FormValue value : values) { + if (value.isFileItem()) { + try { + ret.add(Files.readString(value.getFileItem().getFile(), Charset.defaultCharset())); + } catch (IOException e) { + throw new MultipartPartReadingException(e); + } + } else { + if (encoded) { + ret.add(Encode.encodeQueryParam(value.getValue())); + } else { + ret.add(value.getValue()); + } + } + } + return ret; } - public static byte[] getSingleFileUploadAsArrayBytes(String formName, ResteasyReactiveRequestContext context) { - DefaultFileUpload upload = getSingleFileUpload(formName, context); - if (upload != null) { + public static byte[] getByteArray(String formName, ResteasyReactiveRequestContext context) { + FormData.FormValue value = getFirstValue(formName, context); + if (value == null) { + return null; + } + if (value.isFileItem()) { try { - return Files.readAllBytes(upload.filePath()); + return Files.readAllBytes(value.getFileItem().getFile()); } catch (IOException e) { throw new MultipartPartReadingException(e); } + } else { + return value.getValue().getBytes(Charset.defaultCharset()); } + } - return null; + public static List getByteArrays(String formName, ResteasyReactiveRequestContext context) { + Deque values = getValues(formName, context); + if (values == null) { + return Collections.emptyList(); + } + List ret = new ArrayList(); + for (FormValue value : values) { + if (value.isFileItem()) { + try { + ret.add(Files.readAllBytes(value.getFileItem().getFile())); + } catch (IOException e) { + throw new MultipartPartReadingException(e); + } + } else { + ret.add(value.getValue().getBytes(Charset.defaultCharset())); + } + } + return ret; } - public static InputStream getSingleFileUploadAsInputStream(String formName, ResteasyReactiveRequestContext context) { - DefaultFileUpload upload = getSingleFileUpload(formName, context); - if (upload != null) { + public static InputStream getInputStream(String formName, ResteasyReactiveRequestContext context) { + FormData.FormValue value = getFirstValue(formName, context); + if (value == null) { + return null; + } + if (value.isFileItem()) { try { - return new FileInputStream(upload.filePath().toFile()); + return new FileInputStream(value.getFileItem().getFile().toFile()); } catch (IOException e) { throw new MultipartPartReadingException(e); } + } else { + return new ByteArrayInputStream(value.getValue().getBytes(Charset.defaultCharset())); } + } - return null; + public static List getInputStreams(String formName, ResteasyReactiveRequestContext context) { + Deque values = getValues(formName, context); + if (values == null) { + return Collections.emptyList(); + } + List ret = new ArrayList(); + for (FormValue value : values) { + if (value.isFileItem()) { + try { + ret.add(new FileInputStream(value.getFileItem().getFile().toFile())); + } catch (IOException e) { + throw new MultipartPartReadingException(e); + } + } else { + ret.add(new ByteArrayInputStream(value.getValue().getBytes(Charset.defaultCharset()))); + } + } + return ret; } public static DefaultFileUpload getSingleFileUpload(String formName, ResteasyReactiveRequestContext context) { @@ -157,6 +262,17 @@ public static DefaultFileUpload getSingleFileUpload(String formName, ResteasyRea return null; } + public static List convertFormAttributes(List params, Class typeClass, Type genericType, + MediaType mimeType, ResteasyReactiveRequestContext context, String name) { + List ret = new ArrayList<>(params.size()); + for (String param : params) { + ret.add(MultipartSupport.convertFormAttribute(param, typeClass, genericType, + mimeType, context, + name)); + } + return ret; + } + public static DefaultFileUpload getFileUpload(String formName, ResteasyReactiveRequestContext context) { List uploads = getFileUploads(formName, context); if (!uploads.isEmpty()) { @@ -218,5 +334,4 @@ public static List getFileUploads(ResteasyReactiveRequestCont private static ByteArrayInputStream formAttributeValueToInputStream(String formAttributeValue) { return new ByteArrayInputStream(formAttributeValue.getBytes(StandardCharsets.UTF_8)); } - } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/MultipartFormParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/MultipartFormParamExtractor.java new file mode 100644 index 0000000000000..3b2a4b0473963 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/MultipartFormParamExtractor.java @@ -0,0 +1,99 @@ +package org.jboss.resteasy.reactive.server.core.parameters; + +import java.util.List; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.multipart.FileUpload; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport; + +public class MultipartFormParamExtractor implements ParameterExtractor { + + private final String name; + private final String mimeType; + private final Type type; + private final boolean single; + private final java.lang.reflect.Type genericType; + private final Class typeClass; + // Note that this is only used for the String type, due to the TCK requiring it + private final boolean encoded; + + public enum Type { + FileUpload, + File, + Path, + PartType, + String, + ByteArray, + InputStream; + } + + public MultipartFormParamExtractor(String name, boolean single, Type type, Class typeClass, + java.lang.reflect.Type genericType, String mimeType, boolean encoded) { + this.name = name; + this.single = single; + this.type = type; + this.mimeType = mimeType; + this.typeClass = typeClass; + this.genericType = genericType; + this.encoded = encoded; + } + + @Override + public Object extractParameter(ResteasyReactiveRequestContext context) { + switch (type) { + case String: + if (single) { + return MultipartSupport.getString(name, context, encoded); + } else { + return MultipartSupport.getStrings(name, context, encoded); + } + case ByteArray: + if (single) { + return MultipartSupport.getByteArray(name, context); + } else { + return MultipartSupport.getByteArrays(name, context); + } + case InputStream: + if (single) { + return MultipartSupport.getInputStream(name, context); + } else { + return MultipartSupport.getInputStreams(name, context); + } + case PartType: + if (single) { + String param = (String) context.getFormParameter(name, true, false); + return MultipartSupport.convertFormAttribute(param, typeClass, genericType, MediaType.valueOf(mimeType), + context, + name); + } else { + List params = (List) context.getFormParameter(name, false, false); + return MultipartSupport.convertFormAttributes(params, typeClass, genericType, MediaType.valueOf(mimeType), + context, name); + } + case FileUpload: + // special case + if (name.equals(FileUpload.ALL)) + return MultipartSupport.getFileUploads(context); + return single ? MultipartSupport.getFileUpload(name, context) + : MultipartSupport.getFileUploads(name, context); + case File: + if (single) { + FileUpload upload = MultipartSupport.getFileUpload(name, context); + return upload != null ? upload.uploadedFile().toFile() : null; + } else { + return MultipartSupport.getJavaIOFileUploads(name, context); + } + case Path: + if (single) { + FileUpload upload = MultipartSupport.getFileUpload(name, context); + return upload != null ? upload.uploadedFile() : null; + } else { + return MultipartSupport.getJavaPathFileUploads(name, context); + } + default: + throw new RuntimeException("Unknown multipart parameter type: " + type + " for parameter " + name); + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ArrayConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ArrayConverter.java index 65d94c40e0944..80b3778c705a9 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ArrayConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ArrayConverter.java @@ -54,6 +54,7 @@ public static class ArraySupplier implements DelegatingParameterConverterSupplie public ArraySupplier() { } + // invoked by reflection for BeanParam in ClassInjectorTransformer public ArraySupplier(ParameterConverterSupplier delegate, String elementType) { this.delegate = delegate; this.elementType = elementType; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ListConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ListConverter.java index 028c222291a1b..f5c67b4f62bb6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ListConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/ListConverter.java @@ -58,6 +58,7 @@ public static class ListSupplier implements DelegatingParameterConverterSupplier public ListSupplier() { } + // invoked by reflection for BeanParam in ClassInjectorTransformer public ListSupplier(ParameterConverterSupplier delegate) { this.delegate = delegate; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java index 3af7f3e8cb5bd..37335390e6656 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java @@ -46,6 +46,7 @@ public static class OptionalSupplier implements DelegatingParameterConverterSupp public OptionalSupplier() { } + // invoked by reflection for BeanParam in ClassInjectorTransformer public OptionalSupplier(ParameterConverterSupplier delegate) { this.delegate = delegate; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java index c91eb749a5df6..abecc2bfad6c0 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java @@ -44,6 +44,14 @@ public static class Supplier implements DelegatingParameterConverterSupplier { private ParameterConverterSupplier delegate; + public Supplier() { + } + + // invoked by reflection for BeanParam in ClassInjectorTransformer + public Supplier(ParameterConverterSupplier delegate) { + this.delegate = delegate; + } + @Override public String getClassName() { return RuntimeResolvedConverter.class.getName(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SetConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SetConverter.java index 96d920b1eb605..0d546932ae502 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SetConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SetConverter.java @@ -55,6 +55,7 @@ public static class SetSupplier implements DelegatingParameterConverterSupplier public SetSupplier() { } + // invoked by reflection for BeanParam in ClassInjectorTransformer public SetSupplier(ParameterConverterSupplier delegate) { this.delegate = delegate; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SortedSetConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SortedSetConverter.java index 9d53e9e8c5947..352a9600f2cd5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SortedSetConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/SortedSetConverter.java @@ -59,6 +59,7 @@ public static class SortedSetSupplier implements DelegatingParameterConverterSup public SortedSetSupplier() { } + // invoked by reflection for BeanParam in ClassInjectorTransformer public SortedSetSupplier(ParameterConverterSupplier delegate) { this.delegate = delegate; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 1ca909d8bd1b9..3c2710fef8cf4 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -4,12 +4,15 @@ import static org.jboss.resteasy.reactive.common.util.types.Types.getEffectiveReturnType; import static org.jboss.resteasy.reactive.common.util.types.Types.getRawType; +import java.io.File; +import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -33,12 +36,13 @@ import org.jboss.resteasy.reactive.common.model.MethodParameter; import org.jboss.resteasy.reactive.common.model.ParameterType; import org.jboss.resteasy.reactive.common.model.ResourceClass; -import org.jboss.resteasy.reactive.common.reflection.ReflectionBeanFactoryCreator; import org.jboss.resteasy.reactive.common.types.AllWriteableMarker; import org.jboss.resteasy.reactive.common.util.MediaTypeHelper; import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import org.jboss.resteasy.reactive.common.util.ServerMediaType; import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser; +import org.jboss.resteasy.reactive.common.util.types.Types; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.DeploymentInfo; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; import org.jboss.resteasy.reactive.server.core.parameters.AsyncResponseExtractor; @@ -50,6 +54,7 @@ import org.jboss.resteasy.reactive.server.core.parameters.InjectParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.LocatableResourcePathParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.MatrixParamExtractor; +import org.jboss.resteasy.reactive.server.core.parameters.MultipartFormParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.NullParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import org.jboss.resteasy.reactive.server.core.parameters.PathParamExtractor; @@ -276,7 +281,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } // form params can be everywhere (field, beanparam, param) boolean checkReadBodyRequestFilters = false; - if (method.isFormParamRequired() || method.isMultipart()) { + if (method.isFormParamRequired()) { // read the body as multipart in one go handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier)); checkReadBodyRequestFilters = true; @@ -333,10 +338,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.RESOLVE_METHOD_PARAMETERS); for (int i = 0; i < parameters.length; i++) { ServerMethodParameter param = (ServerMethodParameter) parameters[i]; - boolean single = param.isSingle(); - ParameterExtractor extractor = parameterExtractor(pathParameterIndexes, locatableResource, param.parameterType, - param.type, param.name, - single, param.encoded, param.customParameterExtractor); + ParameterExtractor extractor = parameterExtractor(pathParameterIndexes, locatableResource, param); ParameterConverter converter = null; ParamConverterProviders paramConverterProviders = info.getParamConverterProviders(); boolean userProviderConvertersExist = !paramConverterProviders.getParamConverterProviders().isEmpty(); @@ -618,49 +620,76 @@ private ServerRestHandler alternateInvoker(ServerResourceMethod method, Endpoint } public ParameterExtractor parameterExtractor(Map pathParameterIndexes, boolean locatableResource, - ParameterType type, String javaType, - String name, - boolean single, boolean encoded, ParameterExtractor customExtractor) { + ServerMethodParameter param) { ParameterExtractor extractor; - switch (type) { + switch (param.parameterType) { case HEADER: - return new HeaderParamExtractor(name, single); + return new HeaderParamExtractor(param.name, param.isSingle()); case COOKIE: - return new CookieParamExtractor(name, javaType); + return new CookieParamExtractor(param.name, param.type); case FORM: - return new FormParamExtractor(name, single, encoded); + MultipartFormParamExtractor.Type multiPartType = null; + Class typeClass = null; + Type genericType = null; + if (param.type.equals(FileUpload.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.FileUpload; + } else if (param.type.equals(File.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.File; + } else if (param.type.equals(Path.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.Path; + } else if (param.type.equals(String.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.String; + } else if (param.type.equals(InputStream.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.InputStream; + } else if (param.type.equals(byte[].class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.ByteArray; + } else if (param.mimeType != null && !param.mimeType.equals(MediaType.TEXT_PLAIN)) { + multiPartType = MultipartFormParamExtractor.Type.PartType; + // TODO: special primitive handling? + // FIXME: by using the element type, we're also getting converters for parameter collection types such as List/Array/Set + // but also others we may not want? + typeClass = loadClass(param.type); + genericType = TypeSignatureParser.parse(param.signature); + // strip the element type for the message body readers + genericType = Types.getMultipartElementType(genericType); + } + if (multiPartType != null) { + return new MultipartFormParamExtractor(param.name, param.isSingle(), multiPartType, typeClass, genericType, + param.mimeType, param.encoded); + } + // regular form + return new FormParamExtractor(param.name, param.isSingle(), param.encoded); case PATH: - Integer index = pathParameterIndexes.get(name); + Integer index = pathParameterIndexes.get(param.name); if (index == null) { if (locatableResource) { - extractor = new LocatableResourcePathParamExtractor(name); + extractor = new LocatableResourcePathParamExtractor(param.name); } else { extractor = new NullParamExtractor(); } } else { - extractor = new PathParamExtractor(index, encoded, single); + extractor = new PathParamExtractor(index, param.encoded, param.isSingle()); } return extractor; case CONTEXT: - return new ContextParamExtractor(javaType); + return new ContextParamExtractor(param.type); case ASYNC_RESPONSE: return new AsyncResponseExtractor(); case QUERY: - extractor = new QueryParamExtractor(name, single, encoded); + extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded); return extractor; case BODY: return new BodyParamExtractor(); case MATRIX: - extractor = new MatrixParamExtractor(name, single, encoded); + extractor = new MatrixParamExtractor(param.name, param.isSingle(), param.encoded); return extractor; case BEAN: - return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(loadClass(javaType))); case MULTI_PART_FORM: - return new InjectParamExtractor((BeanFactory) new ReflectionBeanFactoryCreator().apply(javaType)); + return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(loadClass(param.type))); case CUSTOM: - return customExtractor; + return param.customParameterExtractor; default: - throw new RuntimeException("Unknown param type: " + type); + throw new RuntimeException("Unknown param type: " + param.parameterType); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java index 82ffe380fe9e9..9a24d48043bba 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java @@ -20,10 +20,11 @@ public ServerMethodParameter(String name, String type, String declaredType, Stri String signature, ParameterConverterSupplier converter, String defaultValue, boolean obtainedAsCollection, boolean optional, boolean encoded, - ParameterExtractor customParameterExtractor) { + ParameterExtractor customParameterExtractor, + String mimeType) { super(name, type, declaredType, declaredUnresolvedType, signature, parameterType, single, defaultValue, obtainedAsCollection, optional, - encoded); + encoded, mimeType, null /* not useful for server params */); this.converter = converter; this.customParameterExtractor = customParameterExtractor; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerResourceMethod.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerResourceMethod.java index 8775cd8eb7980..89c406a8837e6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerResourceMethod.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerResourceMethod.java @@ -24,11 +24,11 @@ public ServerResourceMethod() { public ServerResourceMethod(String httpMethod, String path, String[] produces, String streamElementType, String[] consumes, Set nameBindingNames, String name, String returnType, String simpleReturnType, MethodParameter[] parameters, - boolean blocking, boolean suspended, boolean sse, boolean formParamRequired, boolean multipart, + boolean blocking, boolean suspended, boolean sse, boolean formParamRequired, List subResourceMethods, Supplier invoker, Set methodAnnotationNames, List handlerChainCustomizers, ParameterExtractor customerParameterExtractor) { super(httpMethod, path, produces, streamElementType, consumes, nameBindingNames, name, returnType, simpleReturnType, - parameters, blocking, suspended, sse, formParamRequired, multipart, subResourceMethods); + parameters, blocking, suspended, sse, formParamRequired, subResourceMethods); this.invoker = invoker; this.methodAnnotationNames = methodAnnotationNames; this.handlerChainCustomizers = handlerChainCustomizers; diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/ErroneousFieldMultipartInputTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/ErroneousFieldMultipartInputTest.java deleted file mode 100644 index 818f77381e57b..0000000000000 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/ErroneousFieldMultipartInputTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.jboss.resteasy.reactive.server.vertx.test.multipart; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.function.Supplier; - -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import org.jboss.resteasy.reactive.MultipartForm; -import org.jboss.resteasy.reactive.RestForm; -import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; -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; - -public class ErroneousFieldMultipartInputTest { - - @RegisterExtension - static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() - .setArchiveProducer(new Supplier<>() { - @Override - public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class) - .addClasses(Input.class); - } - - }).setExpectedException(IllegalArgumentException.class); - - @Test - public void testSimple() { - fail("Should never be called"); - } - - @Path("test") - public static class TestEndpoint { - - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.MULTIPART_FORM_DATA) - @POST - public int test(@MultipartForm Input formData) { - return formData.txtFile.length; - } - } - - public static class Input { - @RestForm - private String name; - - @RestForm - public byte[] txtFile; - } - -} diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/FormDataWithAllUploads.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/FormDataWithAllUploads.java index 4f389d8cdd044..176a77b613273 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/FormDataWithAllUploads.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/FormDataWithAllUploads.java @@ -18,7 +18,7 @@ public class FormDataWithAllUploads extends FormDataBase { @PartType(MediaType.TEXT_PLAIN) private Status status; - @RestForm + @RestForm(FileUpload.ALL) private List uploads; public String getName() { diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/InvalidEncodingTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/InvalidEncodingTest.java index 5dba8ce032c51..a5eaf0224fb79 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/InvalidEncodingTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/InvalidEncodingTest.java @@ -5,13 +5,13 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; import org.junit.jupiter.api.Test; @@ -57,7 +57,7 @@ public static class FeedbackResource { @Path("/multipart-encoding") @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA + ";charset=UTF-8") - public String postForm(@MultipartForm final FeedbackBody feedback) { + public String postForm(@BeanParam final FeedbackBody feedback) { return feedback.content; } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java index 37d3346559f12..ed1183ce2a34d 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.util.function.Supplier; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.POST; @@ -16,7 +17,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -70,7 +70,7 @@ public static class Resource { @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) - public String hello(@MultipartForm Data data) { + public String hello(@BeanParam Data data) { return data.getText(); } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResource.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResource.java index 85b118bcd809b..888aa1e0204dd 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResource.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResource.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.Files; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.POST; @@ -11,7 +12,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.server.core.BlockingOperationSupport; @@ -26,7 +26,7 @@ public class MultipartResource { @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/simple/{times}") @NonBlocking - public String simple(@MultipartForm FormData formData, Integer times) { + public String simple(@BeanParam FormData formData, Integer times) { if (BlockingOperationSupport.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } @@ -72,7 +72,7 @@ public String sameName(FormDataSameFileName formData) { @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/optional") @NonBlocking - public String optional(@MultipartForm FormData formData) { + public String optional(@BeanParam FormData formData) { if (BlockingOperationSupport.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResourceWithAllUploads.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResourceWithAllUploads.java index 00d1e61830779..379fd4a158926 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResourceWithAllUploads.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartResourceWithAllUploads.java @@ -1,12 +1,12 @@ package org.jboss.resteasy.reactive.server.vertx.test.multipart; +import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.BlockingOperationSupport; @@ -20,7 +20,7 @@ public class MultipartResourceWithAllUploads { @Consumes(MediaType.MULTIPART_FORM_DATA) @NonBlocking @Path("/simple/{times}") - public String simple(@MultipartForm FormDataWithAllUploads formData, Integer times) { + public String simple(@BeanParam FormDataWithAllUploads formData, Integer times) { if (BlockingOperationSupport.isBlockingAllowed()) { throw new RuntimeException("should not have dispatched"); } diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java index d6342ca92a280..39205fa58f290 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java @@ -32,59 +32,140 @@ public interface MultipartClient { @Path("/binary") String sendByteArrayAsBinaryFile(@MultipartForm WithByteArrayAsBinaryFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendByteArrayAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) byte[] file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/binary") String sendMultiByteAsBinaryFile(@MultipartForm WithMultiByteAsBinaryFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendMultiByteAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Multi file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/binary") String sendBufferAsBinaryFile(@MultipartForm WithBufferAsBinaryFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendBufferAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Buffer file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/binary") String sendFileAsBinaryFile(@MultipartForm WithFileAsBinaryFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendFileAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) File file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/binary") String sendPathAsBinaryFile(@MultipartForm WithPathAsBinaryFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendPathAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) java.nio.file.Path file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/text") String sendByteArrayAsTextFile(@MultipartForm WithByteArrayAsTextFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendByteArrayAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) byte[] file, + + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/text") String sendBufferAsTextFile(@MultipartForm WithBufferAsTextFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendBufferAsTextFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Buffer file, + + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/text") String sendFileAsTextFile(@MultipartForm WithFileAsTextFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendFileAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) File file, + + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @Path("/text") String sendPathAsTextFile(@MultipartForm WithPathAsTextFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendPathAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) java.nio.file.Path file, + + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("/with-pojo") String sendFileWithPojo(@MultipartForm FileWithPojo data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/with-pojo") + String sendFileWithPojo(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) byte[] file, + + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName, + + @FormParam("pojo") @PartType(MediaType.APPLICATION_JSON) Pojo pojo); + class FileWithPojo { @FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java index 5006915153eae..f314d7b305576 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java @@ -88,6 +88,29 @@ public String sendByteArrayWithPojo(@RestQuery @DefaultValue("true") Boolean wit } } + @GET + @Path("/client/params/byte-array-as-binary-file-with-pojo") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayWithPojoParams(@RestQuery @DefaultValue("true") Boolean withPojo) { + FileWithPojo data = new FileWithPojo(); + byte[] file = HELLO_WORLD.getBytes(UTF_8); + String fileName = GREETING_TXT; + Pojo pojo = null; + if (withPojo) { + pojo = new Pojo(); + pojo.setName("some-name"); + pojo.setValue("some-value"); + } + try { + return client.sendFileWithPojo(file, fileName, pojo); + } catch (WebApplicationException e) { + String responseAsString = e.getResponse().readEntity(String.class); + return String.format("Error: %s statusCode %s", responseAsString, e.getResponse().getStatus()); + } + } + @GET @Path("/client/byte-array-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -102,6 +125,20 @@ public String sendByteArray(@QueryParam("nullFile") @DefaultValue("false") boole return client.sendByteArrayAsBinaryFile(data); } + @GET + @Path("/client/params/byte-array-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + byte[] file = null; + if (!nullFile) { + file = HELLO_WORLD.getBytes(UTF_8); + } + String fileName = GREETING_TXT; + return client.sendByteArrayAsBinaryFile(file, fileName); + } + @GET @Path("/client/multi-byte-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -121,6 +158,25 @@ public String sendMultiByte(@QueryParam("nullFile") @DefaultValue("false") boole return client.sendMultiByteAsBinaryFile(data); } + @GET + @Path("/client/params/multi-byte-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendMultiByteParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + Multi file = null; + if (!nullFile) { + List bytes = new ArrayList<>(); + for (byte b : HELLO_WORLD.getBytes(UTF_8)) { + bytes.add(b); + } + + file = Multi.createFrom().iterable(bytes); + } + String fileName = GREETING_TXT; + return client.sendMultiByteAsBinaryFile(file, fileName); + } + @GET @Path("/client/buffer-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -135,6 +191,20 @@ public String sendBuffer(@QueryParam("nullFile") @DefaultValue("false") boolean return client.sendBufferAsBinaryFile(data); } + @GET + @Path("/client/params/buffer-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBufferParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + Buffer file = null; + if (!nullFile) { + file = Buffer.buffer(HELLO_WORLD); + } + String fileName = GREETING_TXT; + return client.sendBufferAsBinaryFile(file, fileName); + } + @GET @Path("/client/file-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -152,6 +222,22 @@ public String sendFileAsBinary(@QueryParam("nullFile") @DefaultValue("false") bo return client.sendFileAsBinaryFile(data); } + @GET + @Path("/client/params/file-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsBinaryParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + File file = null; + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + file = tempFile; + } + String fileName = GREETING_TXT; + return client.sendFileAsBinaryFile(file, fileName); + } + @GET @Path("/client/path-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -169,6 +255,22 @@ public String sendPathAsBinary(@QueryParam("nullFile") @DefaultValue("false") bo return client.sendPathAsBinaryFile(data); } + @GET + @Path("/client/params/path-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsBinaryParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + java.nio.file.Path file = null; + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + file = tempFile.toPath(); + } + String fileName = GREETING_TXT; + return client.sendPathAsBinaryFile(file, fileName); + } + @GET @Path("/client/byte-array-as-text-file") @Consumes(MediaType.TEXT_PLAIN) @@ -181,6 +283,17 @@ public String sendByteArrayAsTextFile() { return client.sendByteArrayAsTextFile(data); } + @GET + @Path("/client/params/byte-array-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayAsTextFileParams() { + byte[] file = HELLO_WORLD.getBytes(UTF_8); + int number = NUMBER; + return client.sendByteArrayAsTextFile(file, number); + } + @GET @Path("/client/buffer-as-text-file") @Consumes(MediaType.TEXT_PLAIN) @@ -193,6 +306,17 @@ public String sendBufferAsTextFile() { return client.sendBufferAsTextFile(data); } + @GET + @Path("/client/params/buffer-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBufferAsTextFileParams() { + Buffer file = Buffer.buffer(HELLO_WORLD); + int number = NUMBER; + return client.sendBufferAsTextFile(file, number); + } + @GET @Path("/client/file-as-text-file") @Consumes(MediaType.TEXT_PLAIN) @@ -207,6 +331,19 @@ public String sendFileAsText() throws IOException { return client.sendFileAsTextFile(data); } + @GET + @Path("/client/params/file-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsTextParams() throws IOException { + File tempFile = createTempHelloWorldFile(); + + File file = tempFile; + int number = NUMBER; + return client.sendFileAsTextFile(file, number); + } + @GET @Path("/client/path-as-text-file") @Consumes(MediaType.TEXT_PLAIN) @@ -221,6 +358,19 @@ public String sendPathAsText() throws IOException { return client.sendPathAsTextFile(data); } + @GET + @Path("/client/params/path-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsTextParams() throws IOException { + File tempFile = createTempHelloWorldFile(); + + java.nio.file.Path file = tempFile.toPath(); + int number = NUMBER; + return client.sendPathAsTextFile(file, number); + } + @POST @Path("/echo/octet-stream") @Consumes(MediaType.APPLICATION_OCTET_STREAM) diff --git a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java index da4fcb1882f54..feec2c5c565c1 100644 --- a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java +++ b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java @@ -29,6 +29,12 @@ public void shouldSendByteArrayAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/byte-array-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); // @formatter:on } @@ -42,6 +48,13 @@ public void shouldSendNullByteArrayAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") +.when().get("/client/params/byte-array-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); // @formatter:on } @@ -54,6 +67,12 @@ public void shouldSendBufferAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/buffer-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); // @formatter:on } @@ -67,6 +86,13 @@ public void shouldSendNullBufferAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") +.when().get("/client/params/buffer-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); // @formatter:on } @@ -79,6 +105,12 @@ public void shouldSendFileAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/file-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); // @formatter:on } @@ -91,6 +123,12 @@ public void shouldMultiAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/multi-byte-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); // @formatter:on } @@ -104,6 +142,13 @@ public void shouldSendNullFileAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") +.when().get("/client/params/file-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); // @formatter:on } @@ -116,6 +161,12 @@ public void shouldSendPathAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/path-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); // @formatter:on } @@ -129,6 +180,13 @@ public void shouldSendNullPathAsBinaryFile() { .then() .statusCode(200) .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") +.when().get("/client/params/path-as-binary-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); // @formatter:on } @@ -141,6 +199,12 @@ public void shouldSendByteArrayAsTextFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/byte-array-as-text-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); // @formatter:on } @@ -153,6 +217,12 @@ public void shouldSendBufferAsTextFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/buffer-as-text-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); // @formatter:on } @@ -165,6 +235,12 @@ public void shouldSendFileAsTextFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/file-as-text-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); // @formatter:on } @@ -177,6 +253,12 @@ public void shouldSendPathAsTextFile() { .then() .statusCode(200) .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/path-as-text-file") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); // @formatter:on } @@ -189,6 +271,12 @@ public void shouldSendByteArrayAndPojo() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true,pojoOk:true")); + given() + .header("Content-Type", "text/plain") +.when().get("/client/params/byte-array-as-binary-file-with-pojo") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true,pojoOk:true")); // @formatter:on } @@ -202,6 +290,13 @@ public void shouldSendByteArrayAndPojoWithNullPojo() { .then() .statusCode(200) .body(equalTo("fileOk:true,nameOk:true,pojoOk:null")); + given() + .queryParam("withPojo", "false") + .header("Content-Type", "text/plain") +.when().get("/client/params/byte-array-as-binary-file-with-pojo") +.then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true,pojoOk:null")); // @formatter:on }