Skip to content

Commit

Permalink
Add support for downloading list of files in REST Client
Browse files Browse the repository at this point in the history
Closes: quarkusio#41978
(cherry picked from commit e2452d1)
  • Loading branch information
geoand authored and gsmet committed Jul 22, 2024
1 parent f5a7b97 commit 46bb201
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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
Expand All @@ -586,23 +593,40 @@ private ResultHandle performValueConversion(Type parameter, MethodCreator set, R
private String createFieldFillerForField(AnnotationInstance partType, FieldInfo field, String partName,
BuildProducer<GeneratedClassBuildItem> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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<File> files;
}

public static class FileDownloadList {
@RestForm
@PartType(MediaType.TEXT_PLAIN)
public String name;

@RestForm
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public List<FileDownload> files;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<!-- head definitions go here -->
</head>
<body>
<!-- the content goes here -->
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,23 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context,
Object result = multipartData.newInstance();
builder.entity(result);
List<InterfaceHttpData> 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()),
Expand All @@ -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());
Expand Down Expand Up @@ -122,12 +130,4 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context,
return builder.build();
}

private static InterfaceHttpData getPartForName(List<InterfaceHttpData> parts, String partName) {
for (InterfaceHttpData part : parts) {
if (partName.equals(part.getName())) {
return part;
}
}
return null;
}
}

0 comments on commit 46bb201

Please sign in to comment.