Skip to content

Commit eb5e42b

Browse files
authored
Merge pull request #47823 from geoand/#37765
Properly support `List<File>` (and friends) for REST Client multipart upload
2 parents c3666a0 + fd6c4f9 commit eb5e42b

File tree

2 files changed

+162
-28
lines changed

2 files changed

+162
-28
lines changed

extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
import io.quarkus.gizmo.CatchBlockCreator;
166166
import io.quarkus.gizmo.ClassCreator;
167167
import io.quarkus.gizmo.FieldDescriptor;
168+
import io.quarkus.gizmo.ForEachLoop;
168169
import io.quarkus.gizmo.MethodCreator;
169170
import io.quarkus.gizmo.MethodDescriptor;
170171
import io.quarkus.gizmo.ResultHandle;
@@ -2220,58 +2221,90 @@ private AssignableResultHandle createRestClientField(String name, ClassCreator c
22202221
return client;
22212222
}
22222223

2223-
private void handleMultipartField(String formParamName, String partType, String partFilename,
2224-
String type, String parameterSignature,
2224+
private void handleMultipartField(IndexView index, String formParamName, String mimeType, String partFilename,
2225+
Type type, String parameterSignature,
22252226
ResultHandle fieldValue, AssignableResultHandle multipartForm,
22262227
BytecodeCreator methodCreator,
22272228
ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations,
22282229
ResultHandle genericType, String errorLocation) {
22292230

22302231
BytecodeCreator ifValueNotNull = methodCreator.ifNotNull(fieldValue).trueBranch();
22312232

2233+
if (isCollection(type, index)) {
2234+
Type componentType = null;
2235+
if (type.kind() == PARAMETERIZED_TYPE) {
2236+
Type paramType = type.asParameterizedType().arguments().get(0);
2237+
if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) {
2238+
componentType = paramType;
2239+
}
2240+
}
2241+
if (componentType == null) {
2242+
componentType = Type.create(Object.class);
2243+
}
2244+
ForEachLoop loop = ifValueNotNull.forEach(fieldValue);
2245+
BytecodeCreator block = loop.block();
2246+
doHandleMultipartField(formParamName, mimeType, partFilename, componentType, null, loop.element(),
2247+
multipartForm,
2248+
methodCreator,
2249+
client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation, block);
2250+
} else {
2251+
doHandleMultipartField(formParamName, mimeType, partFilename, type, parameterSignature, fieldValue, multipartForm,
2252+
methodCreator,
2253+
client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation, ifValueNotNull);
2254+
}
2255+
}
2256+
2257+
private void doHandleMultipartField(String formParamName, String mimeType, String partFilename, Type type,
2258+
String parameterSignature, ResultHandle fieldValue, AssignableResultHandle multipartForm,
2259+
BytecodeCreator methodCreator, ResultHandle client, String restClientInterfaceClassName,
2260+
ResultHandle parameterAnnotations, ResultHandle genericType, String errorLocation,
2261+
BytecodeCreator bytecodeCreator) {
22322262
// we support string, and send it as an attribute unconverted
2233-
if (type.equals(String.class.getName())) {
2234-
addString(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue);
2235-
} else if (type.equals(File.class.getName())) {
2263+
String typeStr = type.name().toString();
2264+
if (typeStr.equals(String.class.getName())) {
2265+
addString(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, fieldValue);
2266+
} else if (typeStr.equals(File.class.getName())) {
22362267
// file is sent as file :)
2237-
ResultHandle filePath = ifValueNotNull.invokeVirtualMethod(
2268+
ResultHandle filePath = bytecodeCreator.invokeVirtualMethod(
22382269
MethodDescriptor.ofMethod(File.class, "toPath", Path.class), fieldValue);
2239-
addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, filePath);
2240-
} else if (type.equals(Path.class.getName())) {
2270+
addFile(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, filePath);
2271+
} else if (typeStr.equals(Path.class.getName())) {
22412272
// and so is path
2242-
addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue);
2243-
} else if (type.equals(FileUpload.class.getName())) {
2273+
addFile(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, fieldValue);
2274+
} else if (typeStr.equals(FileUpload.class.getName())) {
22442275
addFileUpload(fieldValue, multipartForm, methodCreator);
2245-
} else if (type.equals(InputStream.class.getName())) {
2276+
} else if (typeStr.equals(InputStream.class.getName())) {
22462277
// and so is path
2247-
addInputStream(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, type);
2248-
} else if (type.equals(Buffer.class.getName())) {
2278+
addInputStream(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, fieldValue, typeStr);
2279+
} else if (typeStr.equals(Buffer.class.getName())) {
22492280
// and buffer
2250-
addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, errorLocation);
2251-
} else if (type.startsWith("[")) {
2281+
addBuffer(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, fieldValue, errorLocation);
2282+
} else if (typeStr.startsWith("[")) {
22522283
// byte[] can be sent as file too
2253-
if (!type.equals("[B")) {
2254-
throw new IllegalArgumentException("Array of unsupported type: " + type
2284+
if (!typeStr.equals("[B")) {
2285+
throw new IllegalArgumentException("Array of unsupported type: " + typeStr
22552286
+ " on " + errorLocation);
22562287
}
2257-
ResultHandle buffer = ifValueNotNull.invokeStaticInterfaceMethod(
2288+
ResultHandle buffer = bytecodeCreator.invokeStaticInterfaceMethod(
22582289
MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class),
22592290
fieldValue);
2260-
addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, buffer, errorLocation);
2261-
} else if (parameterSignature.equals(MULTI_BYTE_SIGNATURE)) {
2262-
addMultiAsFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, errorLocation);
2263-
} else if (partType != null) {
2291+
addBuffer(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, buffer, errorLocation);
2292+
} else if (MULTI_BYTE_SIGNATURE.equals(parameterSignature)) {
2293+
addMultiAsFile(bytecodeCreator, multipartForm, formParamName, mimeType, partFilename, fieldValue,
2294+
errorLocation);
2295+
} else if (mimeType != null) {
22642296
if (partFilename != null) {
22652297
log.warnf("Using the @PartFilename annotation is unsupported on the type '%s'. Problematic field is: '%s'",
2266-
partType, formParamName);
2298+
mimeType, formParamName);
22672299
}
22682300
// assume POJO:
2269-
addPojo(ifValueNotNull, multipartForm, formParamName, partType, fieldValue, type);
2301+
addPojo(bytecodeCreator, multipartForm, formParamName, mimeType, fieldValue, typeStr);
22702302
} else {
22712303
// go via converter
2272-
ResultHandle convertedFormParam = convertParamToString(ifValueNotNull, client, fieldValue, type, genericType,
2304+
ResultHandle convertedFormParam = convertParamToString(bytecodeCreator, client, fieldValue, typeStr,
2305+
genericType,
22732306
parameterAnnotations);
2274-
BytecodeCreator parameterIsStringBranch = checkStringParam(ifValueNotNull, convertedFormParam,
2307+
BytecodeCreator parameterIsStringBranch = checkStringParam(bytecodeCreator, convertedFormParam,
22752308
restClientInterfaceClassName, errorLocation);
22762309
addString(parameterIsStringBranch, multipartForm, formParamName, null, partFilename, convertedFormParam);
22772310
}
@@ -3327,9 +3360,9 @@ private void addFormParam(MethodInfo jandexMethod, BytecodeCreator methodCreator
33273360
String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams,
33283361
ResultHandle genericType,
33293362
ResultHandle parameterAnnotations, boolean multipart,
3330-
String partType, String partFilename, String errorLocation) {
3363+
String mimeType, String partFilename, String errorLocation) {
33313364
if (multipart) {
3332-
handleMultipartField(paramName, partType, partFilename, parameterType.name().toString(), parameterSignature,
3365+
handleMultipartField(index, paramName, mimeType, partFilename, parameterType, parameterSignature,
33333366
formParamHandle,
33343367
formParams, methodCreator,
33353368
client, restClientInterfaceClassName, parameterAnnotations, genericType,

extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import java.nio.charset.StandardCharsets;
1414
import java.nio.file.Files;
1515
import java.util.ArrayList;
16+
import java.util.Collection;
17+
import java.util.List;
18+
import java.util.Map;
1619
import java.util.stream.Collectors;
1720

1821
import jakarta.enterprise.context.ApplicationScoped;
@@ -27,10 +30,15 @@
2730
import org.jboss.resteasy.reactive.PartFilename;
2831
import org.jboss.resteasy.reactive.PartType;
2932
import org.jboss.resteasy.reactive.RestForm;
33+
import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder;
3034
import org.jboss.resteasy.reactive.multipart.FileUpload;
35+
import org.jboss.resteasy.reactive.server.multipart.FormValue;
36+
import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
3137
import org.junit.jupiter.api.Test;
3238
import org.junit.jupiter.api.extension.RegisterExtension;
3339

40+
import com.fasterxml.jackson.databind.ObjectMapper;
41+
3442
import io.quarkus.test.QuarkusUnitTest;
3543
import io.quarkus.test.common.http.TestHTTPResource;
3644
import io.smallrye.mutiny.Multi;
@@ -39,6 +47,7 @@ public class MultipartFilenameTest {
3947

4048
public static final String FILE_NAME = "clientFile";
4149
public static final String FILE_CONTENT = "file content";
50+
public static final String FILE_CONTENT2 = "file content2";
4251
public static final String EXPECTED_OUTPUT = FILE_NAME + ":" + FILE_CONTENT;
4352

4453
@TestHTTPResource
@@ -192,6 +201,52 @@ void shouldCopyFileContentToString() throws IOException {
192201
assertThat(client.postMultipartWithFileContent(form)).isEqualTo(FILE_CONTENT);
193202
}
194203

204+
@Test
205+
void shouldWorkWithListOfFiles() throws IOException {
206+
Client client = RestClientBuilder.newBuilder()
207+
.baseUri(baseUri)
208+
// we use this encoder mode on the client in order to make it possible for the server to read items with the same name
209+
.property("io.quarkus.rest.client.multipart-post-encoder-mode",
210+
PausableHttpPostRequestEncoder.EncoderMode.HTML5)
211+
.build(Client.class);
212+
213+
File file = File.createTempFile("MultipartTest", ".txt");
214+
Files.writeString(file.toPath(), FILE_CONTENT);
215+
file.deleteOnExit();
216+
ClientListForm form = new ClientListForm();
217+
form.files = List.of(file);
218+
219+
String responseStr = client.postMultipartWithFileContentAsMultipartFormDataInput(form);
220+
221+
ObjectMapper mapper = new ObjectMapper();
222+
223+
Result result = mapper.readValue(responseStr, Result.class);
224+
assertThat(result).satisfies(r -> {
225+
assertThat(r.count).isEqualTo(1);
226+
assertThat(r.items).singleElement().satisfies(i -> {
227+
assertThat(i.name).isEqualTo("myFile");
228+
assertThat(i.size).isEqualTo(FILE_CONTENT.length());
229+
assertThat(i.isFileItem).isEqualTo(true);
230+
});
231+
});
232+
233+
File file2 = File.createTempFile("MultipartTest2", ".txt");
234+
Files.writeString(file2.toPath(), FILE_CONTENT2);
235+
file2.deleteOnExit();
236+
form = new ClientListForm();
237+
form.files = List.of(file, file2);
238+
239+
responseStr = client.postMultipartWithFileContentAsMultipartFormDataInput(form);
240+
241+
result = mapper.readValue(responseStr, Result.class);
242+
assertThat(result).satisfies(r -> {
243+
assertThat(r.count).isEqualTo(2);
244+
assertThat(r.items).hasSize(2).extracting(Item::name).containsOnly("myFile");
245+
assertThat(r.items).hasSize(2).extracting(Item::size).containsOnly((long) FILE_CONTENT.length(),
246+
(long) FILE_CONTENT2.length());
247+
});
248+
}
249+
195250
@Test
196251
void shouldCopyFileContentToBytes() throws IOException {
197252
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);
@@ -256,6 +311,25 @@ public String uploadWithFileContentAsInputStream(@MultipartForm FormDataWithInpu
256311
.lines()
257312
.collect(Collectors.joining(System.lineSeparator()));
258313
}
314+
315+
@POST
316+
@Path("/file-content-as-multipart-form-data-input")
317+
@Consumes(MediaType.MULTIPART_FORM_DATA)
318+
public String uploadMultipartFormDataInput(MultipartFormDataInput input) throws IOException {
319+
Map<String, Collection<FormValue>> map = input.getValues();
320+
List<Item> items = new ArrayList<>();
321+
for (var entry : map.entrySet()) {
322+
for (FormValue value : entry.getValue()) {
323+
items.add(new Item(
324+
entry.getKey(),
325+
value.isFileItem() ? value.getFileItem().getFileSize() : value.getValue().length(),
326+
value.getCharset(),
327+
value.isFileItem()));
328+
}
329+
330+
}
331+
return new ObjectMapper().writeValueAsString(new Result(items, items.size()));
332+
}
259333
}
260334

261335
public static class FormData {
@@ -363,6 +437,11 @@ String postMultipartWithPartFilenameUsingMultiByte(
363437
@Path("/file-content-as-inputstream")
364438
@Consumes(MediaType.MULTIPART_FORM_DATA)
365439
String postMultipartWithFileContentAsInputStream(@MultipartForm ClientForm clientForm);
440+
441+
@POST
442+
@Path("/file-content-as-multipart-form-data-input")
443+
@Consumes(MediaType.MULTIPART_FORM_DATA)
444+
String postMultipartWithFileContentAsMultipartFormDataInput(@MultipartForm ClientListForm clientForm);
366445
}
367446

368447
public static class ClientForm {
@@ -371,6 +450,12 @@ public static class ClientForm {
371450
public File file;
372451
}
373452

453+
public static class ClientListForm {
454+
@FormParam("myFile")
455+
@PartType(APPLICATION_OCTET_STREAM)
456+
public List<File> files;
457+
}
458+
374459
public static class ClientFormUsingFileUpload {
375460
@RestForm
376461
public FileUpload file;
@@ -417,4 +502,20 @@ public static class ClientFormUsingMultiByte {
417502
@PartFilename(FILE_NAME)
418503
public Multi<Byte> file;
419504
}
505+
506+
public record Item(String name, long size, String charset, boolean isFileItem) {
507+
}
508+
509+
public static class Result {
510+
public List<Item> items;
511+
public int count;
512+
513+
public Result() {
514+
}
515+
516+
public Result(List<Item> items, int count) {
517+
this.items = items;
518+
this.count = count;
519+
}
520+
}
420521
}

0 commit comments

Comments
 (0)