Skip to content

Commit

Permalink
RESTEasy Reactive - Support text/binary conversion for multipart files
Browse files Browse the repository at this point in the history
Assuming we receive the following request:
```
Content-Disposition: form-data; name="textPart"; filename="my_file.txt"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
```

We can load the content of the file using the `FileUpload` class:

```java
public class FormData {
    @FormParam("myFile")
    public FileUpload myFile;
}
```

However, according to what the specs state:
```
The presence of the filename parameter does not force an
implementation to write the entity to a separate file. It is
perfectly acceptable for implementations to leave the entity as part
of the normal mail stream unless the user requests otherwise. As a
consequence, the parameter may be used on any MIME entity, even
`inline' ones. These will not normally be written to files, but the
parameter could be used to provide a filename if the receiving user
should choose to write the part to a file.
```

Then, we should not enforce the use of `FileUpload` and hence also support reading the whole content of the file into strings, and other binary formats like byte[] and InputStream.

```java
public class FormData {
    @FormParam("myFile")
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    public String myFile;
}

public class FormData {
    @FormParam("myFile")
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    public byte[] binaryPart;
}

public class FormData {
    @FormParam("myFile")
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    public InputStream streamPart;
}
```

Fix quarkusio#27083
  • Loading branch information
Sgitario committed Aug 26, 2022
1 parent 885e25e commit 1600494
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.stream.Collectors;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.Consumes;
Expand Down Expand Up @@ -57,6 +63,45 @@ void shouldUseFileNameFromAnnotation() throws IOException {
assertThat(client.postMultipartWithPartFilename(form)).isEqualTo(ClientForm2.FILE_NAME);
}

@Test
void shouldCopyFileContentToString() throws IOException {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);

File file = File.createTempFile("MultipartTest", ".txt");
Files.writeString(file.toPath(), "content!");
file.deleteOnExit();

ClientForm form = new ClientForm();
form.file = file;
assertThat(client.postMultipartWithFileContent(form)).isEqualTo("content!");
}

@Test
void shouldCopyFileContentToBytes() throws IOException {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);

File file = File.createTempFile("MultipartTest", ".txt");
Files.writeString(file.toPath(), "content!");
file.deleteOnExit();

ClientForm form = new ClientForm();
form.file = file;
assertThat(client.postMultipartWithFileContentAsBytes(form)).isEqualTo("content!");
}

@Test
void shouldCopyFileContentToInputStream() throws IOException {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);

File file = File.createTempFile("MultipartTest", ".txt");
Files.writeString(file.toPath(), "content!");
file.deleteOnExit();

ClientForm form = new ClientForm();
form.file = file;
assertThat(client.postMultipartWithFileContentAsInputStream(form)).isEqualTo("content!");
}

@Path("/multipart")
@ApplicationScoped
public static class Resource {
Expand All @@ -65,12 +110,52 @@ public static class Resource {
public String upload(@MultipartForm FormData form) {
return form.myFile.fileName();
}

@POST
@Path("/file-content")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadWithFileContent(@MultipartForm FormDataWithFileContent form) {
return form.fileContent;
}

@POST
@Path("/file-content-as-bytes")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadWithFileContentAsBytes(@MultipartForm FormDataWithBytes form) {
return new String(form.fileContentAsBytes);
}

@POST
@Path("/file-content-as-inputstream")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadWithFileContentAsInputStream(@MultipartForm FormDataWithInputStream form) {
return new BufferedReader(new InputStreamReader(form.fileContentAsInputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
}

public static class FormData {
@FormParam("myFile")
public FileUpload myFile;
}

public static class FormDataWithFileContent {
@FormParam("myFile")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public String fileContent;
}

public static class FormDataWithBytes {
@FormParam("myFile")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public byte[] fileContentAsBytes;
}

public static class FormDataWithInputStream {
@FormParam("myFile")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public InputStream fileContentAsInputStream;
}

@Path("/multipart")
Expand All @@ -82,6 +167,21 @@ public interface Client {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
String postMultipartWithPartFilename(@MultipartForm ClientForm2 clientForm);

@POST
@Path("/file-content")
@Consumes(MediaType.MULTIPART_FORM_DATA)
String postMultipartWithFileContent(@MultipartForm ClientForm clientForm);

@POST
@Path("/file-content-as-bytes")
@Consumes(MediaType.MULTIPART_FORM_DATA)
String postMultipartWithFileContentAsBytes(@MultipartForm ClientForm clientForm);

@POST
@Path("/file-content-as-inputstream")
@Consumes(MediaType.MULTIPART_FORM_DATA)
String postMultipartWithFileContentAsInputStream(@MultipartForm ClientForm clientForm);
}

public static class ClientForm {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.ArrayList;
Expand Down Expand Up @@ -241,11 +242,10 @@ public static String generate(ClassInfo multipartClassInfo, ClassOutput classOut
"Setter '" + setterName + "' of class '" + multipartClassInfo + "' must be public");
}

if (fieldDotName.equals(DotNames.INPUT_STREAM_NAME)
|| fieldDotName.equals(DotNames.INPUT_STREAM_READER_NAME)) {
if (fieldDotName.equals(DotNames.INPUT_STREAM_READER_NAME)) {
// don't support InputStream as it's too easy to get into trouble
throw new IllegalArgumentException(
"InputStream and InputStreamReader are not supported as a field type of a Multipart POJO class. Offending field is '"
"InputStreamReader are not supported as a field type of a Multipart POJO class. Offending field is '"
+ field.name() + "' of class '" + multipartClassName + "'");
}

Expand Down Expand Up @@ -332,6 +332,33 @@ public static String generate(ClassInfo multipartClassInfo, ClassOutput classOut
rrCtxHandle);
populate.assign(resultVariableHandle, allFileUploadsHandle);
}
} else if (partType.equals(MediaType.APPLICATION_OCTET_STREAM)) {
if (fieldType.kind() == Type.Kind.ARRAY
&& fieldType.asArrayType().component().name().equals(DotNames.BYTE_NAME)) {
populate.assign(resultVariableHandle,
populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class,
"getSingleFileUploadAsArrayBytes", byte[].class, String.class,
ResteasyReactiveRequestContext.class),
formAttrNameHandle, rrCtxHandle));
} else if (fieldDotName.equals(DotNames.INPUT_STREAM_NAME)) {
populate.assign(resultVariableHandle,
populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class,
"getSingleFileUploadAsInputStream", InputStream.class, String.class,
ResteasyReactiveRequestContext.class),
formAttrNameHandle, rrCtxHandle));
} else if (fieldDotName.equals(DotNames.STRING_NAME)) {
populate.assign(resultVariableHandle,
populate.invokeStaticMethod(MethodDescriptor.ofMethod(MultipartSupport.class,
"getSingleFileUploadAsString", String.class, String.class,
ResteasyReactiveRequestContext.class),
formAttrNameHandle, rrCtxHandle));
} else {
throw new IllegalArgumentException(
"Unsupported type to read multipart file contents. Offending field is '"
+ field.name() + "' of class '"
+ field.declaringClass().name()
+ "'. If you need to read the contents of the uploaded file, use 'Path' or 'File' as the field type and use File IO APIs to read the bytes, while making sure you annotate the endpoint with '@Blocking'");
}
} else {
// this is a common enough mistake, so let's provide a good error message
failIfFileTypeUsedAsGenericType(field, fieldType, fieldDotName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.MediaType;
Expand Down Expand Up @@ -102,6 +106,55 @@ public static Object convertFormAttribute(String value, Class type, Type generic
throw new NotSupportedException("Media type '" + mediaType + "' in multipart request is not supported");
}

public static String getSingleFileUploadAsString(String formName, ResteasyReactiveRequestContext context) {
DefaultFileUpload upload = getSingleFileUpload(formName, context);
if (upload != null) {
try {
return Files.readString(upload.filePath(), Charset.defaultCharset());
} catch (IOException e) {
throw new MultipartPartReadingException(e);
}
}

return null;
}

public static byte[] getSingleFileUploadAsArrayBytes(String formName, ResteasyReactiveRequestContext context) {
DefaultFileUpload upload = getSingleFileUpload(formName, context);
if (upload != null) {
try {
return Files.readAllBytes(upload.filePath());
} catch (IOException e) {
throw new MultipartPartReadingException(e);
}
}

return null;
}

public static InputStream getSingleFileUploadAsInputStream(String formName, ResteasyReactiveRequestContext context) {
DefaultFileUpload upload = getSingleFileUpload(formName, context);
if (upload != null) {
try {
return new FileInputStream(upload.filePath().toFile());
} catch (IOException e) {
throw new MultipartPartReadingException(e);
}
}

return null;
}

public static DefaultFileUpload getSingleFileUpload(String formName, ResteasyReactiveRequestContext context) {
List<DefaultFileUpload> uploads = getFileUploads(formName, context);
if (uploads.size() > 1) {
throw new BadRequestException("Found more than one files for attribute '" + formName + "'. Expected only one file");
} else if (uploads.size() == 1) {
return uploads.get(0);
}
return null;
}

public static DefaultFileUpload getFileUpload(String formName, ResteasyReactiveRequestContext context) {
List<DefaultFileUpload> uploads = getFileUploads(formName, context);
if (!uploads.isEmpty()) {
Expand Down

0 comments on commit 1600494

Please sign in to comment.