Skip to content

Commit

Permalink
Merge pull request #27498 from Sgitario/rr_multipart
Browse files Browse the repository at this point in the history
RESTEasy Reactive - Support text/binary conversion for multipart files
  • Loading branch information
geoand authored Aug 26, 2022
2 parents 93184f7 + 1600494 commit d69cd81
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 d69cd81

Please sign in to comment.