From 6ad2be375acf7ae641bad8228251f47ea8f2327f Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 23 Jun 2022 10:49:18 +0200 Subject: [PATCH] Support downloading list of files in Resteasy Reactive Multipart Additionally, it adds support of DownloadFile objects. Fix https://github.com/quarkusio/quarkus/issues/26164 --- .../FormDataOutputMapperGenerator.java | 14 +++- ...artOutputMultipleFileDownloadResponse.java | 19 +++++ .../MultipartOutputMultipleFileResponse.java | 19 +++++ .../multipart/MultipartOutputResource.java | 30 ++++++++ ...ipartOutputSingleFileDownloadResponse.java | 14 ++++ ...ipartOutputUsingBlockingEndpointsTest.java | 51 ++++++++++++- .../test/multipart/PathFileDownload.java | 56 ++++++++++++++ .../FormDataOutputMapperGenerator.java | 14 +++- .../multipart/MultipartMessageBodyWriter.java | 76 +++++++++++++++---- .../server/core/multipart/PartItem.java | 15 +++- 10 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileDownloadResponse.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileResponse.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputSingleFileDownloadResponse.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathFileDownload.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java index b0ab3d49ff258..e591725ae5ed6 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java @@ -217,10 +217,20 @@ static String generate(ClassInfo returnTypeClassInfo, ClassOutput classOutput, I inputInstanceHandle)); } + // Get parameterized type if field type is a parameterized class + String firstParamType = ""; + if (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) { + List argumentTypes = fieldType.asParameterizedType().arguments(); + if (argumentTypes.size() > 0) { + firstParamType = argumentTypes.get(0).name().toString(); + } + } + // Create Part Item instance ResultHandle partItemInstanceHandle = populate.newInstance( - MethodDescriptor.ofConstructor(PartItem.class, String.class, MediaType.class, Object.class), - populate.load(formAttrName), partTypeHandle, resultHandle); + MethodDescriptor.ofConstructor(PartItem.class, + String.class, MediaType.class, Object.class, String.class), + populate.load(formAttrName), partTypeHandle, resultHandle, populate.load(firstParamType)); // Add it to the list populate.invokeVirtualMethod( diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileDownloadResponse.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileDownloadResponse.java new file mode 100644 index 0000000000000..a7f919b19b84f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileDownloadResponse.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import java.util.List; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileDownload; + +public class MultipartOutputMultipleFileDownloadResponse { + + @RestForm + String name; + + @RestForm + @PartType(MediaType.APPLICATION_OCTET_STREAM) + List files; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileResponse.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileResponse.java new file mode 100644 index 0000000000000..ce0c2496ccd93 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputMultipleFileResponse.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import java.io.File; +import java.util.List; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; + +public class MultipartOutputMultipleFileResponse { + + @RestForm + String name; + + @RestForm + @PartType(MediaType.APPLICATION_OCTET_STREAM) + List files; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java index 8fd628899a878..6ebcdeab6174c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java @@ -23,6 +23,7 @@ public class MultipartOutputResource { private static final long ONE_GIGA = 1024l * 1024l * 1024l * 1l; private final File TXT_FILE = new File("./src/test/resources/lorem.txt"); + private final File XML_FILE = new File("./src/test/resources/test.xml"); @GET @Path("/simple") @@ -67,6 +68,35 @@ public MultipartOutputFileResponse file() { return response; } + @GET + @Path("/with-multiple-file") + @Produces(MediaType.MULTIPART_FORM_DATA) + public MultipartOutputMultipleFileResponse multipleFile() { + MultipartOutputMultipleFileResponse response = new MultipartOutputMultipleFileResponse(); + response.name = RESPONSE_NAME; + response.files = List.of(TXT_FILE, XML_FILE); + return response; + } + + @GET + @Path("/with-single-file-download") + @Produces(MediaType.MULTIPART_FORM_DATA) + public MultipartOutputSingleFileDownloadResponse singleDownloadFile() { + MultipartOutputSingleFileDownloadResponse response = new MultipartOutputSingleFileDownloadResponse(); + response.file = new PathFileDownload("one", XML_FILE); + return response; + } + + @GET + @Path("/with-multiple-file-download") + @Produces(MediaType.MULTIPART_FORM_DATA) + public MultipartOutputMultipleFileDownloadResponse multipleDownloadFile() { + MultipartOutputMultipleFileDownloadResponse response = new MultipartOutputMultipleFileDownloadResponse(); + response.name = RESPONSE_NAME; + response.files = List.of(new PathFileDownload(TXT_FILE), new PathFileDownload(XML_FILE)); + return response; + } + @GET @Path("/with-large-file") @Produces(MediaType.MULTIPART_FORM_DATA) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputSingleFileDownloadResponse.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputSingleFileDownloadResponse.java new file mode 100644 index 0000000000000..7d09e750add6e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputSingleFileDownloadResponse.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileDownload; + +public class MultipartOutputSingleFileDownloadResponse { + + @RestForm + @PartType(MediaType.APPLICATION_OCTET_STREAM) + FileDownload file; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 80c1f131644a8..5795599df878c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -26,8 +26,9 @@ public class MultipartOutputUsingBlockingEndpointsTest extends AbstractMultipart static QuarkusUnitTest test = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(MultipartOutputResource.class, MultipartOutputResponse.class, - MultipartOutputFileResponse.class, - Status.class, FormDataBase.class, OtherPackageFormDataBase.class)); + MultipartOutputFileResponse.class, MultipartOutputMultipleFileResponse.class, + MultipartOutputMultipleFileDownloadResponse.class, MultipartOutputSingleFileDownloadResponse.class, + Status.class, FormDataBase.class, OtherPackageFormDataBase.class, PathFileDownload.class)); @Test public void testSimple() { @@ -71,20 +72,62 @@ public void testString() { } @Test - public void testWithFiles() { + public void testWithFile() { String response = RestAssured .given() .get("/multipart/output/with-file") .then() .contentType(ContentType.MULTIPART) .statusCode(200) - .log().all() .extract().asString(); assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); assertContainsFile(response, "file", MediaType.APPLICATION_OCTET_STREAM, "lorem.txt"); } + @Test + public void testWithSingleFileDownload() { + String response = RestAssured + .given() + .get("/multipart/output/with-single-file-download") + .then() + .contentType(ContentType.MULTIPART) + .statusCode(200) + .extract().asString(); + + assertContainsFile(response, "one", MediaType.APPLICATION_OCTET_STREAM, "test.xml"); + } + + @Test + public void testWithMultipleFiles() { + String response = RestAssured + .given() + .get("/multipart/output/with-multiple-file") + .then() + .contentType(ContentType.MULTIPART) + .statusCode(200) + .extract().asString(); + + assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsFile(response, "files", MediaType.APPLICATION_OCTET_STREAM, "lorem.txt"); + assertContainsFile(response, "files", MediaType.APPLICATION_OCTET_STREAM, "test.xml"); + } + + @Test + public void testWithMultipleFileDownload() { + String response = RestAssured + .given() + .get("/multipart/output/with-multiple-file-download") + .then() + .contentType(ContentType.MULTIPART) + .statusCode(200) + .extract().asString(); + + assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsFile(response, "files", MediaType.APPLICATION_OCTET_STREAM, "lorem.txt"); + assertContainsFile(response, "files", MediaType.APPLICATION_OCTET_STREAM, "test.xml"); + } + @EnabledIfSystemProperty(named = "test-resteasy-reactive-large-files", matches = "true") @Test public void testWithLargeFiles() { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathFileDownload.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathFileDownload.java new file mode 100644 index 0000000000000..dff0f1b6e8221 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/PathFileDownload.java @@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.server.test.multipart; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.jboss.resteasy.reactive.multipart.FileDownload; + +public class PathFileDownload implements FileDownload { + private final String partName; + private final File file; + + public PathFileDownload(File file) { + this(null, file); + } + + public PathFileDownload(String partName, File file) { + this.partName = partName; + this.file = file; + } + + @Override + public String name() { + return partName; + } + + @Override + public Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + try { + return Files.size(file.toPath()); + } catch (IOException e) { + throw new RuntimeException("Failed to get size", e); + } + } + + @Override + public String contentType() { + return null; + } + + @Override + public String charSet() { + return null; + } +} diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/FormDataOutputMapperGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/FormDataOutputMapperGenerator.java index 6ee9f779233d0..0b0bc232023c8 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/FormDataOutputMapperGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/FormDataOutputMapperGenerator.java @@ -214,10 +214,20 @@ static String generate(ClassInfo returnTypeClassInfo, ClassOutput classOutput, I inputInstanceHandle)); } + // Get parameterized type if field type is a parameterized class + String firstParamType = ""; + if (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) { + List argumentTypes = fieldType.asParameterizedType().arguments(); + if (argumentTypes.size() > 0) { + firstParamType = argumentTypes.get(0).name().toString(); + } + } + // Create Part Item instance ResultHandle partItemInstanceHandle = populate.newInstance( - MethodDescriptor.ofConstructor(PartItem.class, String.class, MediaType.class, Object.class), - populate.load(formAttrName), partTypeHandle, resultHandle); + MethodDescriptor.ofConstructor(PartItem.class, + String.class, MediaType.class, Object.class, String.class), + populate.load(formAttrName), partTypeHandle, resultHandle, populate.load(firstParamType)); // Add it to the list populate.invokeVirtualMethod( diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java index bbc96b83c38b5..9515d359ffa84 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java @@ -6,6 +6,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -17,6 +18,7 @@ import javax.ws.rs.ext.MessageBodyWriter; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.reflection.ReflectionBeanFactoryCreator; +import org.jboss.resteasy.reactive.multipart.FileDownload; import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; @@ -68,21 +70,18 @@ private void write(List parts, String boundary, OutputStream outputStr Charset charset = requestContext.getDeployment().getRuntimeConfiguration().body().defaultCharset(); String boundaryLine = "--" + boundary; for (PartItem part : parts) { - if (part.getValue() != null) { - // write boundary: --... - writeLine(outputStream, boundaryLine, charset); - // write content disposition header - writeLine(outputStream, HttpHeaders.CONTENT_DISPOSITION + ": form-data; name=\"" + part.getName() + "\"" - + getFileNameIfFile(part.getValue()), charset); - // write content content type - writeLine(outputStream, HttpHeaders.CONTENT_TYPE + ": " + part.getType(), charset); - // extra line - writeLine(outputStream, charset); - - // write content - writeEntity(outputStream, part.getValue(), part.getType(), requestContext); - // extra line - writeLine(outputStream, charset); + Object partValue = part.getValue(); + if (partValue != null) { + if (isListOf(part, File.class) || isListOf(part, FileDownload.class)) { + List list = (List) part.getValue(); + for (int i = 0; i < list.size(); i++) { + writePart(part.getName(), list.get(i), part.getType(), + boundaryLine, charset, outputStream, requestContext); + } + } else { + writePart(part.getName(), part.getValue(), part.getType(), + boundaryLine, charset, outputStream, requestContext); + } } } @@ -90,9 +89,44 @@ private void write(List parts, String boundary, OutputStream outputStr write(outputStream, boundaryLine + DOUBLE_DASH, charset); } + private void writePart(String partName, + Object partValue, + MediaType partType, + String boundaryLine, + Charset charset, + OutputStream outputStream, + ResteasyReactiveRequestContext requestContext) throws IOException { + + if (partValue instanceof FileDownload) { + FileDownload fileDownload = (FileDownload) partValue; + partValue = fileDownload.filePath().toFile(); + // overwrite properties if set + partName = isNotEmpty(fileDownload.name()) ? fileDownload.name() : partName; + partType = isNotEmpty(fileDownload.contentType()) ? MediaType.valueOf(fileDownload.contentType()) : partType; + charset = isNotEmpty(fileDownload.charSet()) ? Charset.forName(fileDownload.charSet()) : charset; + } + + // write boundary: --... + writeLine(outputStream, boundaryLine, charset); + // write content disposition header + writeLine(outputStream, HttpHeaders.CONTENT_DISPOSITION + ": form-data; name=\"" + partName + "\"" + + getFileNameIfFile(partValue), charset); + // write content content type + writeLine(outputStream, HttpHeaders.CONTENT_TYPE + ": " + partType, charset); + // extra line + writeLine(outputStream, charset); + + // write content + writeEntity(outputStream, partValue, partType, requestContext); + // extra line + writeLine(outputStream, charset); + } + private String getFileNameIfFile(Object value) { if (value instanceof File) { return "; filename=\"" + ((File) value).getName() + "\""; + } else if (value instanceof FileDownload) { + return "; filename=\"" + ((FileDownload) value).fileName() + "\""; } return ""; @@ -149,4 +183,16 @@ private void appendBoundaryIntoMediaType(ResteasyReactiveRequestContext requestC requestContext.setResponseContentType(new MediaType(mediaType.getType(), mediaType.getSubtype(), Collections.singletonMap(BOUNDARY_PARAM, boundary))); } + + private boolean isNotEmpty(String str) { + return str != null && !str.isEmpty(); + } + + private boolean isListOf(PartItem part, Class paramType) { + if (!(part.getValue() instanceof Collection)) { + return false; + } + + return paramType.getName().equals(part.getFirstParamType()); + } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/PartItem.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/PartItem.java index 9dd6bfc5c5aef..99c135ec2150d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/PartItem.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/PartItem.java @@ -6,11 +6,13 @@ public final class PartItem { private final String name; private final MediaType type; private final Object value; + private final String firstParamType; - public PartItem(String name, MediaType type, Object value) { + public PartItem(String name, MediaType type, Object value, String firstParamType) { this.name = name; this.type = type; this.value = value; + this.firstParamType = firstParamType; } public String getName() { @@ -24,4 +26,15 @@ public MediaType getType() { public Object getValue() { return value; } + + /** + * If the value is a parameterized class like a List, it will return the raw representation of the first parameter type. + * For example, if it's a List, it will return "java.lang.String". + * If the value is not a parameterized class, it will return an empty string. + * + * @return the raw representation of the first parameter type in parameterized classes. Otherwise, empty string. + */ + public String getFirstParamType() { + return firstParamType; + } }