From 6907679c3d41e3e55f0338c30fc1cc0eaebcaaea Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Jul 2024 20:06:05 +0300 Subject: [PATCH] Add support for downloading list of files in REST Client Closes: #41978 --- .../JaxrsClientReactiveProcessor.java | 30 +++++++- .../multipart/MultipartResponseTest.java | 74 ++++++++++++++++++- .../reactive/multipart/PathFileDownload.java | 56 ++++++++++++++ .../deployment/src/test/resources/lorem.txt | 5 ++ .../deployment/src/test/resources/test.html | 9 +++ .../ClientResponseCompleteRestHandler.java | 30 ++++---- 6 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/PathFileDownload.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/resources/lorem.txt create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/resources/test.html 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 528a59432d5f85..fd408776f634e0 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 @@ -13,6 +13,7 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.CONSUMES; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ENCODED; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LIST; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MAP; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT; @@ -221,6 +222,9 @@ public class JaxrsClientReactiveProcessor { public static final DotName BYTE = DotName.createSimple(Byte.class.getName()); public static final MethodDescriptor MULTIPART_RESPONSE_DATA_ADD_FILLER = MethodDescriptor .ofMethod(MultipartResponseDataBase.class, "addFiller", void.class, FieldFiller.class); + private static final MethodDescriptor ARRAY_LIST_CONSTRUCTOR = MethodDescriptor.ofConstructor(ArrayList.class); + private static final MethodDescriptor COLLECTION_ADD = MethodDescriptor.ofMethod(Collection.class, "add", boolean.class, + Object.class); private static final String MULTIPART_FORM_DATA = "multipart/form-data"; @@ -569,6 +573,9 @@ private String createFieldFillerForSetter(AnnotationInstance partType, MethodInf } private ResultHandle performValueConversion(Type parameter, MethodCreator set, ResultHandle value) { + if (treatMultipartDownloadFieldAsCollection(parameter)) { + parameter = parameter.asParameterizedType().arguments().get(0); + } if (parameter.kind() == CLASS) { if (parameter.asClassType().name().equals(FILE)) { // we should get a FileDownload type, let's convert it to File @@ -586,23 +593,40 @@ private ResultHandle performValueConversion(Type parameter, MethodCreator set, R private String createFieldFillerForField(AnnotationInstance partType, FieldInfo field, String partName, BuildProducer generatedClasses, String dataClassName) { String fillerClassName = dataClassName + "$$" + field.name(); + Type fieldType = field.type(); try (ClassCreator c = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedClasses, true), fillerClassName, null, FieldFiller.class.getName())) { - createFieldFillerConstructor(partType, field.type(), partName, fillerClassName, c); + boolean treatAsCollection = treatMultipartDownloadFieldAsCollection(fieldType); + + createFieldFillerConstructor(partType, fieldType, partName, fillerClassName, c); MethodCreator set = c .getMethodCreator( MethodDescriptor.ofMethod(fillerClassName, "set", void.class, Object.class, Object.class)); + ResultHandle resultObj = set.getMethodParam(0); ResultHandle value = set.getMethodParam(1); - value = performValueConversion(field.type(), set, value); - set.writeInstanceField(field, set.getMethodParam(0), value); + value = performValueConversion(fieldType, set, value); + if (treatAsCollection) { + ResultHandle collection = set.readInstanceField(field, resultObj); + BytecodeCreator firstInvocation = set.ifNull(collection).trueBranch(); + firstInvocation.writeInstanceField(field, resultObj, + firstInvocation.newInstance(ARRAY_LIST_CONSTRUCTOR)); + set.invokeInterfaceMethod(COLLECTION_ADD, set.readInstanceField(field, resultObj), value); + } else { + set.writeInstanceField(field, resultObj, value); + } set.returnValue(null); } return fillerClassName; } + private boolean treatMultipartDownloadFieldAsCollection(Type type) { + return (type.kind() == PARAMETERIZED_TYPE) && (LIST.equals(type.name()) || COLLECTION.equals( + type.name())); + } + private void createFieldFillerConstructor(AnnotationInstance partType, Type type, String partName, String fillerClassName, ClassCreator c) { MethodCreator ctor = c.getMethodCreator(MethodDescriptor.ofConstructor(fillerClassName)); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java index c616e4d1025172..4e6ebfe9038825 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java @@ -12,6 +12,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.List; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -30,6 +31,7 @@ import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileDownload; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.api.extension.RegisterExtension; @@ -45,13 +47,17 @@ public class MultipartResponseTest { public static final String WOO_HOO_WOO_HOO_HOO = "Woo hoo, woo hoo hoo"; private static final long ONE_GIGA = 1024l * 1024l * 1024l * 1l; + private static final File TXT_FILE = new File("./src/test/resources/lorem.txt"); + private static final File HTML_FILE = new File("./src/test/resources/test.html"); + @TestHTTPResource URI baseUri; @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot( - (jar) -> jar.addClasses(TestJacksonBasicMessageBodyReader.class, TestJacksonBasicMessageBodyWriter.class)); + (jar) -> jar.addClasses(TestJacksonBasicMessageBodyReader.class, TestJacksonBasicMessageBodyWriter.class, + PathFileDownload.class)); @Test void shouldParseMultipartResponse() { @@ -116,6 +122,22 @@ void shouldParseMultipartResponseWithLargeFile() { assertThat(data.file.length()).isEqualTo(ONE_GIGA); } + @Test + void shouldParseMultipartResponseWithListOfFiles() { + Client client = createClient(); + FileList data = client.getFileList(); + assertThat(data.name).isEqualTo("test"); + assertThat(data.files).hasSize(2); + } + + @Test + void shouldParseMultipartResponseWithListOfFileDownloads() { + Client client = createClient(); + FileDownloadList data = client.getFileDownloadList(); + assertThat(data.name).isEqualTo("test"); + assertThat(data.files).hasSize(2); + } + @Test void shouldParseMultipartResponseWithNulls() { Client client = createClient(); @@ -195,6 +217,16 @@ public interface Client { @Path("/large") MultipartData getLargeFile(); + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/file-list") + FileList getFileList(); + + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/file-download-list") + FileDownloadList getFileDownloadList(); + @GET @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/empty") @@ -257,6 +289,26 @@ public MultipartData getLargeFile() throws IOException { return new MultipartData("foo", file, null, 1984, null); } + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/file-list") + public FileList fileList() { + var response = new FileList(); + response.name = "test"; + response.files = List.of(TXT_FILE, HTML_FILE); + return response; + } + + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/file-download-list") + public FileDownloadList fileDownloadList() { + var response = new FileDownloadList(); + response.name = "test"; + response.files = List.of(new PathFileDownload(TXT_FILE), new PathFileDownload(HTML_FILE)); + return response; + } + @GET @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/empty") @@ -394,4 +446,24 @@ public Panda(String weight, String height, String mood) { this.mood = mood; } } + + public static class FileList { + @RestForm + @PartType(MediaType.TEXT_PLAIN) + public String name; + + @RestForm + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public List files; + } + + public static class FileDownloadList { + @RestForm + @PartType(MediaType.TEXT_PLAIN) + public String name; + + @RestForm + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public List files; + } } diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/PathFileDownload.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/PathFileDownload.java new file mode 100644 index 00000000000000..e1b043607cd9d1 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/PathFileDownload.java @@ -0,0 +1,56 @@ +package io.quarkus.rest.client.reactive.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/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/lorem.txt b/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/lorem.txt new file mode 100644 index 00000000000000..ff415863722e0d --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/lorem.txt @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut +enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor +in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, + sunt in culpa qui officia deserunt mollit anim id est laborum. + diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/test.html b/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/test.html new file mode 100644 index 00000000000000..0d2c081db5dc91 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/resources/test.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java index 36d165192f98b4..a862eb66704d40 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java @@ -64,15 +64,23 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context, Object result = multipartData.newInstance(); builder.entity(result); List parts = context.getResponseMultipartParts(); - for (FieldFiller fieldFiller : multipartData.getFieldFillers()) { - InterfaceHttpData httpData = getPartForName(parts, fieldFiller.getPartName()); - if (httpData == null) { + for (InterfaceHttpData httpData : parts) { + FieldFiller fieldFiller = null; + // find the correct filler + for (FieldFiller ff : multipartData.getFieldFillers()) { + if (ff.getPartName().equals(httpData.getName())) { + fieldFiller = ff; + break; + } + } + if (fieldFiller == null) { continue; - } else if (httpData instanceof Attribute) { + } + if (httpData instanceof Attribute at) { // TODO: get rid of ByteArrayInputStream // TODO: maybe we could extract something closer to input stream from attribute ByteArrayInputStream in = new ByteArrayInputStream( - ((Attribute) httpData).getValue().getBytes(StandardCharsets.UTF_8)); + at.getValue().getBytes(StandardCharsets.UTF_8)); Object fieldValue = context.readEntity(in, fieldFiller.getFieldType(), MediaType.valueOf(fieldFiller.getMediaType()), @@ -83,8 +91,8 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context, if (fieldValue != null) { fieldFiller.set(result, fieldValue); } - } else if (httpData instanceof FileUpload) { - fieldFiller.set(result, new FileDownloadImpl((FileUpload) httpData)); + } else if (httpData instanceof FileUpload fu) { + fieldFiller.set(result, new FileDownloadImpl(fu)); } else { throw new IllegalArgumentException("Unsupported multipart message element type. " + "Expected FileAttribute or Attribute, got: " + httpData.getClass()); @@ -122,12 +130,4 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context, return builder.build(); } - private static InterfaceHttpData getPartForName(List parts, String partName) { - for (InterfaceHttpData part : parts) { - if (partName.equals(part.getName())) { - return part; - } - } - return null; - } }