From 3adb83840a443b01867ec87caeca5ed9967ba7e8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 16 Apr 2024 09:31:28 +0300 Subject: [PATCH 1/2] Support FileUpload as multipart type in REST Client Closes: #40052 --- .../JaxrsClientReactiveProcessor.java | 14 +++++ .../multipart/MultipartDetectionTest.java | 37 +++++++++++++ .../multipart/MultipartFilenameTest.java | 52 +++++++++++++++++++ .../client/api/ClientMultipartForm.java | 6 +++ .../reactive/multipart/FileUpload.java | 2 +- 5 files changed, 110 insertions(+), 1 deletion(-) diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 3816b652ac553..9529ffff88426 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -121,6 +121,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.multipart.FileDownload; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; @@ -190,6 +191,7 @@ public class JaxrsClientReactiveProcessor { 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 String FILE_UPLOAD_SIGNATURE = "L" + FileUpload.class.getName().replace('.', '/') + ";"; private static final Logger log = Logger.getLogger(JaxrsClientReactiveProcessor.class); @@ -1176,6 +1178,7 @@ private boolean isMultipartRequiringType(String signature, String partType) { || signature.equals(BUFFER_SIGNATURE) || signature.equals(BYTE_ARRAY_SIGNATURE) || signature.equals(MULTI_BYTE_SIGNATURE) + || signature.equals(FILE_UPLOAD_SIGNATURE) || partType != null); } @@ -1793,6 +1796,8 @@ private void handleMultipartField(String formParamName, String partType, String } else if (type.equals(Path.class.getName())) { // and so is path addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue); + } else if (type.equals(FileUpload.class.getName())) { + addFileUpload(fieldValue, multipartForm, methodCreator); } else if (type.equals(InputStream.class.getName())) { // and so is path addInputStream(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, type); @@ -1888,6 +1893,15 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi } } + private void addFileUpload(ResultHandle fieldValue, AssignableResultHandle multipartForm, + BytecodeCreator methodCreator) { + // MultipartForm#fileUpload(FileUpload fileUpload); + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(ClientMultipartForm.class, "fileUpload", + ClientMultipartForm.class, FileUpload.class), + multipartForm, fieldValue); + } + private ResultHandle primitiveToString(BytecodeCreator methodCreator, ResultHandle fieldValue, FieldInfo field) { PrimitiveType primitiveType = field.type().asPrimitiveType(); switch (primitiveType.primitive()) { diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java index ab0a0675b8de3..b3fd11d635c78 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java @@ -81,6 +81,39 @@ void shouldCallImplicitEndpoints() throws IOException { .isEqualTo(file.getName() + " file Hello"); assertThat(client.postMultipartEntityImplicit(file.getName(), person)) .isEqualTo(file.getName() + " Stef:Epardaud"); + + assertThat(client.postMultipartImplicitFileUpload("Foo", new FileUpload() { + @Override + public String name() { + return "file"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return -1; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + })) + .isEqualTo("Foo " + file.getName() + " Hello"); } @Path("form") @@ -142,6 +175,10 @@ String postMultipartEntityImplicit(@RestForm String name, @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartExplicit(@RestForm String name, @RestForm File file); + @Path("multipart") + @POST + String postMultipartImplicitFileUpload(@RestForm String name, @RestForm FileUpload file); + @Path("urlencoded") @POST String postUrlencodedImplicit(@RestForm String name); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java index 2a70e77f31ff7..69bec01287d09 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java @@ -60,6 +60,49 @@ void shouldPassOriginalFileName() throws IOException { assertThat(client.postMultipart(form)).isEqualTo(file.getName()); } + @Test + void shouldWorkWithFileUpload() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + file.deleteOnExit(); + + ClientFormUsingFileUpload form = new ClientFormUsingFileUpload(); + form.file = new FileUpload() { + + @Override + public String name() { + return "myFile"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return 0; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + }; + assertThat(client.postMultipartFileUpload(form)).isEqualTo(file.getName()); + } + @Test void shouldUseFileNameFromAnnotation() throws IOException { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); @@ -244,6 +287,10 @@ public interface Client { @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipart(@MultipartForm ClientForm clientForm); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipartFileUpload(ClientFormUsingFileUpload clientForm); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartWithPartFilename(@MultipartForm ClientFormUsingFile clientForm); @@ -324,6 +371,11 @@ public static class ClientForm { public File file; } + public static class ClientFormUsingFileUpload { + @RestForm + public FileUpload file; + } + public static class ClientFormUsingFile { @FormParam("myFile") @PartType(APPLICATION_OCTET_STREAM) diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java index 7b7bc74d9c220..b2fcbb5852922 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormDataPart; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.smallrye.mutiny.Multi; import io.vertx.core.buffer.Buffer; @@ -86,4 +87,9 @@ public ClientMultipartForm multiAsTextFileUpload(String name, String filename, M return this; } + public ClientMultipartForm fileUpload(FileUpload fileUpload) { + binaryFileUpload(fileUpload.name(), fileUpload.fileName(), fileUpload.filePath().toString(), fileUpload.contentType()); + return this; + } + } 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 57a2bc9996ffa..b844cf4eab250 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 @@ -5,7 +5,7 @@ /** * Represent a file that has been uploaded. *

- * WARNING: This type is currently only supported on the server + * This type is usually used on server, but it is also supported in the REST Client. */ public interface FileUpload extends FilePart { From b1cc21be8e22a1ec9d3fc33da3ea98ce10db4aed Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 16 Apr 2024 10:11:21 +0300 Subject: [PATCH 2/2] Update multipart support documentation --- docs/src/main/asciidoc/rest-client.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 7e587807e0f9e..3f06f8291c7e0 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1400,7 +1400,7 @@ To send data as a multipart form, you can just use the regular `@RestForm` (or ` String sendMultipart(@RestForm File file, @RestForm String otherField); ---- -Parameters specified as `File`, `Path`, `byte[]` or `Buffer` are sent as files and default to the +Parameters specified as `File`, `Path`, `byte[]`, `Buffer` or `FileUpload` 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. @@ -1421,7 +1421,7 @@ Naturally, you can also group these parameters into a containing class: String sendMultipart(Parameters parameters); ---- -Any `@RestForm` parameter of the type `File`, `Path`, `byte[]` or `Buffer`, as well as any +Any `@RestForm` parameter of the type `File`, `Path`, `byte[]`, `Buffer` or `FileUpload`, as well as any annotated with `@PartType` automatically imply a `@Consumes(MediaType.MULTIPART_FORM_DATA)` on the method if there is no `@Consumes` present. @@ -1533,7 +1533,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> ClientMultipartForm multiPartForm = ClientMultipartForm.create(); multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2> request.getFiles().forEach(fu -> { - multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> + multiPartForm.fileUpload(fu); // <3> }); return multiPartForm; } @@ -1541,7 +1541,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> <1> `Request` representing the request the server parts accepts <2> A `jsonPayload` attribute is added directly to `ClientMultipartForm` -<3> A `binaryFileUpload` is created from the request's `FileUpload` (which is a Quarkus REST (Server) type used to represent a binary file upload) +<3> A `fileUpload` is created from the request's `FileUpload` [NOTE] ====