diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java index 333226f0a9bf4..22c7a845d4119 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java @@ -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; @@ -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 { @@ -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") @@ -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 { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java index ef0f652e40b12..68418ada79c43 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/MultipartPopulatorGenerator.java @@ -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; @@ -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 + "'"); } @@ -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); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java index f598d3eaf4473..5af3b1b30a7d5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java @@ -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; @@ -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 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 uploads = getFileUploads(formName, context); if (!uploads.isEmpty()) {