diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java index 2c0971ff4401a..0bbf3f08a6be5 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java @@ -60,6 +60,7 @@ public RestResponse withFormDataOutput() { MultipartFormDataOutput form = new MultipartFormDataOutput(); form.addFormData("name", RESPONSE_NAME, MediaType.TEXT_PLAIN_TYPE); form.addFormData("part-with-filename", RESPONSE_FILENAME, MediaType.TEXT_PLAIN_TYPE, "file.txt"); + form.addFormData("part-with-filename", RESPONSE_FILENAME, MediaType.TEXT_PLAIN_TYPE, "file2.txt"); form.addFormData("custom-surname", RESPONSE_SURNAME, MediaType.TEXT_PLAIN_TYPE); form.addFormData("custom-status", RESPONSE_STATUS, MediaType.TEXT_PLAIN_TYPE) .getHeaders().putSingle("extra-header", "extra-value"); diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index a631ec0f25390..d1a1b3032726f 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -75,6 +75,7 @@ public void testWithFormData() { String body = extractable.asString(); assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); assertContainsValue(body, "part-with-filename", MediaType.TEXT_PLAIN, "filename=\"file.txt\""); + assertContainsValue(body, "part-with-filename", MediaType.TEXT_PLAIN, "filename=\"file2.txt\""); assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); 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 6896e96e67225..2c8750777dbba 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 @@ -85,21 +85,27 @@ private void write(MultipartFormDataOutput formDataOutput, String boundary, Outp throws IOException { Charset charset = requestContext.getDeployment().getRuntimeConfiguration().body().defaultCharset(); String boundaryLine = "--" + boundary; - Map parts = formDataOutput.getFormData(); - for (Map.Entry entry : parts.entrySet()) { + Map> parts = formDataOutput.getAllFormData(); + for (var entry : parts.entrySet()) { String partName = entry.getKey(); - PartItem part = entry.getValue(); - Object partValue = part.getEntity(); - if (partValue != null) { - if (isListOf(part, File.class) || isListOf(part, FileDownload.class)) { - List list = (List) partValue; - for (int i = 0; i < list.size(); i++) { - writePart(partName, list.get(i), part, boundaryLine, charset, outputStream, requestContext); + List partItems = entry.getValue(); + if (partItems.isEmpty()) { + continue; + } + for (PartItem part : partItems) { + Object partValue = part.getEntity(); + if (partValue != null) { + if (isListOf(part, File.class) || isListOf(part, FileDownload.class)) { + List list = (List) partValue; + for (int i = 0; i < list.size(); i++) { + writePart(partName, list.get(i), part, boundaryLine, charset, outputStream, requestContext); + } + } else { + writePart(partName, partValue, part, boundaryLine, charset, outputStream, requestContext); } - } else { - writePart(partName, partValue, part, boundaryLine, charset, outputStream, requestContext); } } + } // write boundary: -- ... -- diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java index 3b8212907a3ca..baccd98c55cc5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java @@ -1,7 +1,9 @@ package org.jboss.resteasy.reactive.server.multipart; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import jakarta.ws.rs.core.MediaType; @@ -10,9 +12,26 @@ * Used when a Resource method needs to return a multipart output */ public final class MultipartFormDataOutput { - private final Map parts = new LinkedHashMap<>(); + private final Map> parts = new LinkedHashMap<>(); + /** + * @deprecated use {@link #getAllFormData()} instead + */ + @Deprecated(forRemoval = true) public Map getFormData() { + Map result = new LinkedHashMap<>(); + for (var entry : parts.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + // use the last item inserted as this is the old behavior + int lastIndex = entry.getValue().size() - 1; + result.put(entry.getKey(), entry.getValue().get(lastIndex)); + } + return Collections.unmodifiableMap(result); + } + + public Map> getAllFormData() { return Collections.unmodifiableMap(parts); } @@ -31,7 +50,8 @@ public PartItem addFormData(String key, Object entity, MediaType mediaType, Stri } private PartItem addFormData(String key, PartItem part) { - parts.put(key, part); + List items = parts.computeIfAbsent(key, k -> new ArrayList<>()); + items.add(part); return part; } }