Skip to content

Commit

Permalink
Support large files in multipart for resteasy reactive
Browse files Browse the repository at this point in the history
I've added tests for resteasy reactive and also resteasy reactive rest client. 

Fix #24472
  • Loading branch information
Sgitario committed Mar 23, 2022
1 parent 7429026 commit d7ae188
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/jdk-early-access-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +21,7 @@ public class MultipartOutputResource {
public static final List<String> 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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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++) {
Expand All @@ -209,15 +226,24 @@ 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));
}
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")
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -81,7 +80,7 @@ private void write(List<PartItem> 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);
}
Expand Down Expand Up @@ -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();
Expand All @@ -125,13 +124,12 @@ private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReact
MessageBodyWriter<Object>[] writers = (MessageBodyWriter<Object>[]) serializers
.findWriters(null, entityClass, mediaType, RuntimeType.SERVER)
.toArray(ServerSerialisers.NO_WRITER);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean wrote = false;
for (MessageBodyWriter<Object> 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;
}
Expand All @@ -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() {
Expand Down

0 comments on commit d7ae188

Please sign in to comment.