diff --git a/.github/workflows/jdk-early-access-build.yml b/.github/workflows/jdk-early-access-build.yml index cb90d1e6df324..f93acb3a606b9 100644 --- a/.github/workflows/jdk-early-access-build.yml +++ b/.github/workflows/jdk-early-access-build.yml @@ -24,7 +24,7 @@ env: # Workaround testsuite locale issue LANG: en_US.UTF-8 MAVEN_OPTS: -Xmx2g -XX:MaxMetaspaceSize=1g - JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-containers -Dstart-containers -Dformat.skip" + JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-containers -Dstart-containers -Dtest-resteasy-reactive-large-files -Dformat.skip" DB_USER: hibernate_orm_test DB_PASSWORD: hibernate_orm_test DB_NAME: hibernate_orm_test 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 cb34fe5a88fd4..8fd628899a878 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 @@ -1,6 +1,8 @@ package io.quarkus.resteasy.reactive.server.test.multipart; import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; import java.util.List; import javax.ws.rs.GET; @@ -19,6 +21,7 @@ public class MultipartOutputResource { public static final List RESPONSE_VALUES = List.of("one", "two"); public static final boolean RESPONSE_ACTIVE = true; + private static final long ONE_GIGA = 1024l * 1024l * 1024l * 1l; private final File TXT_FILE = new File("./src/test/resources/lorem.txt"); @GET @@ -57,13 +60,28 @@ public String usingString() { @GET @Path("/with-file") @Produces(MediaType.MULTIPART_FORM_DATA) - public MultipartOutputFileResponse complex() { + public MultipartOutputFileResponse file() { MultipartOutputFileResponse response = new MultipartOutputFileResponse(); response.name = RESPONSE_NAME; response.file = TXT_FILE; return response; } + @GET + @Path("/with-large-file") + @Produces(MediaType.MULTIPART_FORM_DATA) + public MultipartOutputFileResponse largeFile() throws IOException { + File largeFile = File.createTempFile("rr-large-file", ".tmp"); + largeFile.deleteOnExit(); + RandomAccessFile f = new RandomAccessFile(largeFile, "rw"); + f.setLength(ONE_GIGA); + + MultipartOutputFileResponse response = new MultipartOutputFileResponse(); + response.name = RESPONSE_NAME; + response.file = largeFile; + return response; + } + @GET @Path("/with-null-fields") @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/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index b653ec406ff82..80c1f131644a8 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 @@ -8,6 +8,7 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.resteasy.reactive.server.test.multipart.other.OtherPackageFormDataBase; @@ -84,6 +85,16 @@ public void testWithFiles() { assertContainsFile(response, "file", MediaType.APPLICATION_OCTET_STREAM, "lorem.txt"); } + @EnabledIfSystemProperty(named = "test-resteasy-reactive-large-files", matches = "true") + @Test + public void testWithLargeFiles() { + RestAssured.given() + .get("/multipart/output/with-large-file") + .then() + .contentType(ContentType.MULTIPART) + .statusCode(200); + } + @Test public void testWithNullFields() { RestAssured diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java index 7cab2bc05b4fd..ad94920c4bbf3 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartResponseTest.java @@ -8,6 +8,7 @@ import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; +import java.io.RandomAccessFile; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -29,6 +30,7 @@ import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; @@ -38,6 +40,8 @@ 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; + @TestHTTPResource URI baseUri; @@ -98,6 +102,15 @@ void shouldParseMultipartResponseWithSmallFile() { assertThat(data.numberz).isNull(); } + @EnabledIfSystemProperty(named = "test-resteasy-reactive-large-files", matches = "true") + @Test + void shouldParseMultipartResponseWithLargeFile() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + MultipartData data = client.getLargeFile(); + assertThat(data.file).exists(); + assertThat(data.file.length()).isEqualTo(ONE_GIGA); + } + @Test void shouldParseMultipartResponseWithNulls() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); @@ -164,6 +177,11 @@ public interface Client { @Path("/small") MultipartData getSmallFile(); + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/large") + MultipartData getLargeFile(); + @GET @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/empty") @@ -193,8 +211,7 @@ public static class Resource { @GET @Produces(MediaType.MULTIPART_FORM_DATA) public MultipartData getFile() throws IOException { - File file = File.createTempFile("toDownload", ".txt"); - file.deleteOnExit(); + File file = createTempFileToDownload(); // let's write Woo hoo, woo hoo hoo 10k times try (FileOutputStream out = new FileOutputStream(file)) { for (int i = 0; i < 10000; i++) { @@ -209,8 +226,7 @@ public MultipartData getFile() throws IOException { @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/small") public MultipartData getSmallFile() throws IOException { - File file = File.createTempFile("toDownload", ".txt"); - file.deleteOnExit(); + File file = createTempFileToDownload(); // let's write Woo hoo, woo hoo hoo 1 time try (FileOutputStream out = new FileOutputStream(file)) { out.write(WOO_HOO_WOO_HOO_HOO.getBytes(StandardCharsets.UTF_8)); @@ -218,6 +234,16 @@ public MultipartData getSmallFile() throws IOException { return new MultipartData("foo", file, null, 1984, null); } + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("/large") + public MultipartData getLargeFile() throws IOException { + File file = createTempFileToDownload(); + RandomAccessFile f = new RandomAccessFile(file, "rw"); + f.setLength(ONE_GIGA); + return new MultipartData("foo", file, null, 1984, null); + } + @GET @Produces(MediaType.MULTIPART_FORM_DATA) @Path("/empty") @@ -231,6 +257,12 @@ public MultipartData getEmptyData() { public MultipartData throwError() { throw new RuntimeException("forced error"); } + + private static File createTempFileToDownload() throws IOException { + File file = File.createTempFile("toDownload", ".txt"); + file.deleteOnExit(); + return file; + } } public static class MultipartData { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java index e150124e45404..ed6ee182dd3bb 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java @@ -1,7 +1,6 @@ package org.jboss.resteasy.reactive.client.impl; import io.vertx.core.buffer.Buffer; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -104,10 +103,10 @@ public static Buffer invokeClientWriter(Entity entity, Object entityObject, C if (writer.isWriteable(entityClass, entityType, entity.getAnnotations(), entity.getMediaType())) { if (writerInterceptors == null) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + VertxBufferOutputStream out = new VertxBufferOutputStream(); writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), - entity.getMediaType(), headerMap, baos); - return Buffer.buffer(baos.toByteArray()); + entity.getMediaType(), headerMap, out); + return out.getBuffer(); } else { return runClientWriterInterceptors(entityObject, entityClass, entityType, entity.getAnnotations(), entity.getMediaType(), headerMap, writer, writerInterceptors, properties, serialisers, diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/VertxBufferOutputStream.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/VertxBufferOutputStream.java new file mode 100644 index 0000000000000..eab88c9d70181 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/VertxBufferOutputStream.java @@ -0,0 +1,32 @@ +package org.jboss.resteasy.reactive.client.impl; + +import io.vertx.core.buffer.Buffer; +import java.io.IOException; +import java.io.OutputStream; + +public class VertxBufferOutputStream extends OutputStream { + private Buffer buffer; + + public VertxBufferOutputStream() { + this.buffer = Buffer.buffer(); + } + + @Override + public void write(int b) throws IOException { + buffer.appendByte((byte) (b & 0xFF)); + } + + @Override + public void write(byte[] b) throws IOException { + buffer.appendBytes(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + buffer.appendBytes(b, off, len); + } + + public Buffer getBuffer() { + return this.buffer.copy(); + } +} 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 b836994dc4d75..bbc96b83c38b5 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 @@ -1,6 +1,5 @@ package org.jboss.resteasy.reactive.server.core.multipart; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -81,7 +80,7 @@ private void write(List parts, String boundary, OutputStream outputStr writeLine(outputStream, charset); // write content - write(outputStream, serialiseEntity(part.getValue(), part.getType(), requestContext)); + writeEntity(outputStream, part.getValue(), part.getType(), requestContext); // extra line writeLine(outputStream, charset); } @@ -116,7 +115,7 @@ private void writeLine(OutputStream os, Charset defaultCharset) throws IOExcepti os.write(LINE_SEPARATOR.getBytes(defaultCharset)); } - private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReactiveRequestContext context) + private void writeEntity(OutputStream os, Object entity, MediaType mediaType, ResteasyReactiveRequestContext context) throws IOException { ServerSerialisers serializers = context.getDeployment().getSerialisers(); Class entityClass = entity.getClass(); @@ -125,13 +124,12 @@ private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReact MessageBodyWriter[] writers = (MessageBodyWriter[]) serializers .findWriters(null, entityClass, mediaType, RuntimeType.SERVER) .toArray(ServerSerialisers.NO_WRITER); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); boolean wrote = false; for (MessageBodyWriter writer : writers) { if (writer.isWriteable(entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType)) { // FIXME: spec doesn't really say what headers we should use here writer.writeTo(entity, entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType, - Serialisers.EMPTY_MULTI_MAP, baos); + Serialisers.EMPTY_MULTI_MAP, os); wrote = true; break; } @@ -140,7 +138,6 @@ private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReact if (!wrote) { throw new IllegalStateException("Could not find MessageBodyWriter for " + entityClass + " as " + mediaType); } - return baos.toByteArray(); } private String generateBoundary() {