diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java index a111f6ee1763e5..a6dbd75cdb3a44 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartResource.java @@ -1,16 +1,21 @@ package io.quarkus.resteasy.reactive.jackson.deployment.test; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.validation.Valid; import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.smallrye.common.annotation.Blocking; @@ -35,4 +40,38 @@ public Map greeting(@Valid @MultipartForm FormData formData) { result.put("persons2", formData.persons2); return result; } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Blocking + @Path("/param/json") + public Map greeting( + @RestForm @PartType(MediaType.APPLICATION_JSON) Map map, + + @FormParam("names") @PartType(MediaType.TEXT_PLAIN) List names, + + @RestForm @PartType(MediaType.TEXT_PLAIN) int[] numbers, + + @RestForm @PartType(MediaType.TEXT_PLAIN) List numbers2, + + @RestForm @PartType(MediaType.APPLICATION_JSON) @Valid Person person, + + @RestForm @PartType(MediaType.APPLICATION_JSON) Person[] persons, + + @RestForm @PartType(MediaType.APPLICATION_JSON) List persons2, + + @RestForm("htmlFile") FileUpload htmlPart) { + Map result = new HashMap<>(map); + result.put("person", person); + result.put("htmlFileSize", htmlPart.size()); + result.put("htmlFilePath", htmlPart.uploadedFile().toAbsolutePath().toString()); + result.put("htmlFileContentType", htmlPart.contentType()); + result.put("names", names); + result.put("numbers", numbers); + result.put("numbers2", numbers2); + result.put("persons", persons); + result.put("persons2", persons2); + return result; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java index 169063a78debaf..3daab7970555c4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MultipartTest.java @@ -84,6 +84,56 @@ public void testValid() throws IOException { .body("persons2[1].last", equalTo("Last2")); } + @Test + public void testValidParam() throws IOException { + RestAssured.given() + .multiPart("map", + "{\n" + + " \"foo\": \"bar\",\n" + + " \"sub\": {\n" + + " \"foo2\": \"bar2\"\n" + + " }\n" + + "}") + .multiPart("person", "{\"first\": \"Bob\", \"last\": \"Builder\"}", "application/json") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("names", "name1") + .multiPart("names", "name2") + .multiPart("numbers", 1) + .multiPart("numbers", 2) + .multiPart("numbers2", 1) + .multiPart("numbers2", 2) + .multiPart("persons", "{\"first\": \"First1\", \"last\": \"Last1\"}", "application/json") + .multiPart("persons", "{\"first\": \"First2\", \"last\": \"Last2\"}", "application/json") + .multiPart("persons2", "{\"first\": \"First1\", \"last\": \"Last1\"}", "application/json") + .multiPart("persons2", "{\"first\": \"First2\", \"last\": \"Last2\"}", "application/json") + .accept("application/json") + .when() + .post("/multipart/param/json") + .then() + .statusCode(200) + .body("foo", equalTo("bar")) + .body("sub.foo2", equalTo("bar2")) + .body("person.first", equalTo("Bob")) + .body("person.last", equalTo("Builder")) + .body("htmlFileSize", equalTo(Files.readAllBytes(HTML_FILE.toPath()).length)) + .body("htmlFilePath", not(equalTo(HTML_FILE.toPath().toAbsolutePath().toString()))) + .body("htmlFileContentType", equalTo("text/html")) + .body("names[0]", equalTo("name1")) + .body("names[1]", equalTo("name2")) + .body("numbers[0]", equalTo(1)) + .body("numbers[1]", equalTo(2)) + .body("numbers2[0]", equalTo(1)) + .body("numbers2[1]", equalTo(2)) + .body("persons[0].first", equalTo("First1")) + .body("persons[0].last", equalTo("Last1")) + .body("persons[1].first", equalTo("First2")) + .body("persons[1].last", equalTo("Last2")) + .body("persons2[0].first", equalTo("First1")) + .body("persons2[0].last", equalTo("Last1")) + .body("persons2[1].first", equalTo("First2")) + .body("persons2[1].last", equalTo("Last2")); + } + @Test public void testInvalid() { RestAssured.given() @@ -106,4 +156,27 @@ public void testInvalid() { .then() .statusCode(400); } + + @Test + public void testInvalidParam() { + RestAssured.given() + .multiPart("map", + "{\n" + + " \"foo\": \"bar\",\n" + + " \"sub\": {\n" + + " \"foo2\": \"bar2\"\n" + + " }\n" + + "}") + .multiPart("person", "{\"first\": \"Bob\"}", "application/json") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("names", "name1") + .multiPart("names", "name2") + .multiPart("numbers", 1) + .multiPart("numbers", 2) + .accept("application/json") + .when() + .post("/multipart/param/json") + .then() + .statusCode(400); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java index 9fe9a051645de5..048c7b2450193d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java @@ -68,6 +68,20 @@ public void testInput() { assertThat(response).isEqualTo("John-Divino Pastor"); } + @Test + public void testInputParam() { + String response = RestAssured + .given() + .multiPart("name", "John") + .multiPart("school", SCHOOL, MediaType.APPLICATION_XML) + .post("/multipart/param/input") + .then() + .statusCode(200) + .extract().asString(); + + assertThat(response).isEqualTo("John-Divino Pastor"); + } + private void assertContains(String response, String name, String contentType, Object value) { String[] lines = response.split("--"); assertThat(lines).anyMatch(line -> line.contains(String.format(EXPECTED_CONTENT_DISPOSITION_PART, name)) @@ -97,6 +111,13 @@ public String input(@MultipartForm MultipartInput input) { return input.name + "-" + input.school.name; } + @POST + @Path("/param/input") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String input(@RestForm String name, + @RestForm @PartType(MediaType.APPLICATION_XML) School school) { + return name + "-" + school.name; + } } private static class MultipartOutputResponse { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java index 88617b88dbfb27..fa8bdd53e5778e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java @@ -82,6 +82,27 @@ public void testSimple() { Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); } + @Test + public void testSimpleParam() { + RestAssured.given() + .multiPart("name", "Alice") + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart/param/simple/2") + .then() + .statusCode(200) + .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); + } + @Test public void testBlocking() throws IOException { RestAssured.given() @@ -144,6 +165,26 @@ public void testSameName() { Assertions.assertEquals(4, uploadDir.toFile().listFiles().length); } + @Test + public void testSameNameParam() { + RestAssured.given() + .multiPart("active", "false") + .multiPart("status", "EATING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("htmlFile", HTML_FILE2, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart/param/same-name") + .then() + .statusCode(200) + .body(equalTo("EATING - 2 - 1 - 1")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(4, uploadDir.toFile().listFiles().length); + } + private String filePath(File file) { return file.toPath().toAbsolutePath().toString(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java index 48cdec080f1ca1..c9529c094e823f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputWithAllUploadsTest.java @@ -76,4 +76,25 @@ public void testSimple() throws IOException { // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); } + + @Test + public void testSimpleParam() throws IOException { + RestAssured.given() + .multiPart("name", "Alice") + .multiPart("active", "true") + .multiPart("num", "25") + .multiPart("status", "WORKING") + .multiPart("htmlFile", HTML_FILE, "text/html") + .multiPart("xmlFile", XML_FILE, "text/xml") + .multiPart("txtFile", TXT_FILE, "text/plain") + .accept("text/plain") + .when() + .post("/multipart-all/param/simple/2") + .then() + .statusCode(200) + .body(equalTo("Alice - true - 50 - WORKING - 3 - text/plain")); + + // ensure that the 3 uploaded files where created on disk + Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java index 58edf0ceae3a72..eb1d194af8ea35 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java @@ -1,7 +1,9 @@ package io.quarkus.resteasy.reactive.server.test.multipart; +import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; @@ -12,7 +14,10 @@ import javax.ws.rs.core.Response; import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.runtime.BlockingOperationControl; import io.smallrye.common.annotation.Blocking; @@ -36,6 +41,30 @@ public String simple(@MultipartForm FormData formData, Integer times) { + formData.txtFile.exists(); } + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/param/simple/{times}") + @NonBlocking + public String simple( + // don't set a part type, use the default + @RestForm String name, + @RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm("htmlFile") FileUpload htmlPart, + @RestForm("xmlFile") java.nio.file.Path xmlPart, + @RestForm File txtFile, + @RestForm @PartType(MediaType.TEXT_PLAIN) boolean active, + @RestForm @PartType(MediaType.TEXT_PLAIN) int num, + Integer times) { + if (BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should not have dispatched"); + } + return name + " - " + active + " - " + times * num + " - " + status + + " - " + + htmlPart.contentType() + " - " + Files.exists(xmlPart) + " - " + + txtFile.exists(); + } + @POST @Blocking @Produces(MediaType.TEXT_PLAIN) @@ -67,6 +96,21 @@ public String sameName(FormDataSameFileName formData) { + formData.xmlFiles.size(); } + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/param/same-name") + public String sameName(@RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm("htmlFile") List htmlFiles, + @RestForm("txtFile") List txtFiles, + @RestForm("xmlFile") List xmlFiles) { + if (!BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should have dispatched"); + } + return status + " - " + htmlFiles.size() + " - " + txtFiles.size() + " - " + + xmlFiles.size(); + } + @POST @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.MULTIPART_FORM_DATA) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java index baec08f7f9c764..18c359b991df9e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResourceWithAllUploads.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.server.test.multipart; +import java.util.List; + import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -7,6 +9,8 @@ import javax.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.runtime.BlockingOperationControl; @@ -29,4 +33,25 @@ public String simple(@MultipartForm FormDataWithAllUploads formData, Integer tim + " - " + formData.getUploads().size() + " - " + txtFile.contentType(); } + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + @NonBlocking + @Path("/param/simple/{times}") + public String simple( + @RestForm + // don't set a part type, use the default + String name, + @RestForm @PartType(MediaType.TEXT_PLAIN) Status status, + @RestForm(FileUpload.ALL) List uploads, + @RestForm @PartType(MediaType.TEXT_PLAIN) boolean active, + @RestForm @PartType(MediaType.TEXT_PLAIN) int num, + Integer times) { + if (BlockingOperationControl.isBlockingAllowed()) { + throw new RuntimeException("should not have dispatched"); + } + FileUpload txtFile = uploads.stream().filter(f -> f.name().equals("txtFile")).findFirst().get(); + return name + " - " + active + " - " + times * num + " - " + status + + " - " + uploads.size() + " - " + txtFile.contentType(); + } } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index f3b2e27bfc1721..7040941b6f5ed5 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -140,7 +140,9 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas return new MethodParameter(name, elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), signature, type, single, - defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded); + defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, + // FIXME: mime type from any @PartType annotation + null); } @Override diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java index 8b1cc932a9ed74..e627ce3bb50d3b 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/PartType.java @@ -6,9 +6,10 @@ import java.lang.annotation.Target; /** - * Used on fields of {@link MultipartForm} POJOs to designate the media type the corresponding body part maps to. + * Used on fields of {@link MultipartForm} POJOs or form parameters to designate the media type the corresponding body part maps + * to. */ -@Target({ ElementType.FIELD }) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface PartType { String value(); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java index 2d17f503afc709..2c038b63ecd8ef 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java @@ -23,6 +23,7 @@ public class MethodParameter { private String defaultValue; private boolean optional; private boolean isObtainedAsCollection; + public String mimeType; public MethodParameter() { } @@ -30,7 +31,7 @@ public MethodParameter() { public MethodParameter(String name, String type, String declaredType, String declaredUnresolvedType, String signature, ParameterType parameterType, boolean single, - String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded) { + String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded, String mimeType) { this.name = name; this.type = type; this.declaredType = declaredType; @@ -42,6 +43,7 @@ public MethodParameter(String name, String type, String declaredType, String dec this.isObtainedAsCollection = isObtainedAsCollection; this.optional = optional; this.encoded = encoded; + this.mimeType = mimeType; } public String getName() { diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java index de58afdeb503d9..f9955f940c3707 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java @@ -6,6 +6,7 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; @@ -207,6 +208,28 @@ public static Type getEffectiveReturnType(Type returnType) { throw new UnsupportedOperationException("Endpoint return type not supported yet: " + returnType); } + public static Type getMultipartElementType(Type paramType) { + if (paramType instanceof Class) { + if (((Class) paramType).isArray()) { + return ((Class) paramType).getComponentType(); + } + return paramType; + } + if (paramType instanceof ParameterizedType) { + ParameterizedType type = (ParameterizedType) paramType; + Type firstTypeArgument = type.getActualTypeArguments()[0]; + if (type.getRawType() == List.class) { + return firstTypeArgument; + } + return paramType; + } + if (paramType instanceof GenericArrayType) { + GenericArrayType type = (GenericArrayType) paramType; + return type.getGenericComponentType(); + } + throw new UnsupportedOperationException("Endpoint return type not supported yet: " + paramType); + } + public static Class getRawType(Type type) { if (type instanceof Class) return (Class) type; diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 419555c144dbad..7576168d51aa61 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java @@ -4,6 +4,12 @@ public interface FileUpload extends FilePart { + /** + * Use this constant as form parameter name in order to get all file uploads from a multipart form, regardless of their + * names + */ + public final static String ALL = "*"; + default Path uploadedFile() { return filePath(); } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index b64b2b9ddab575..af3a280daf63c7 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -22,6 +22,8 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.SORTED_SET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ZONED_DATE_TIME; +import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -35,6 +37,7 @@ import java.util.regex.PatternSyntaxException; import javax.enterprise.inject.spi.DeploymentException; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.PathSegment; @@ -56,6 +59,7 @@ import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import org.jboss.resteasy.reactive.server.core.parameters.converters.ArrayConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.CharParamConverter; @@ -305,23 +309,37 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf return currentInjectableBean; } + private String getPartMime(Map annotations) { + AnnotationInstance partType = annotations.get(ResteasyReactiveDotNames.PART_TYPE_NAME); + String mimeType = null; + if (partType != null && partType.value() != null) { + mimeType = partType.value().asString(); + // remove what ends up being the default + if (MediaType.TEXT_PLAIN.equals(mimeType)) { + mimeType = null; + } + } + return mimeType; + } + protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, ServerIndexedParameter parameterResult, String name, String defaultValue, ParameterType type, String elementType, boolean single, String signature) { ParameterConverterSupplier converter = parameterResult.getConverter(); DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); + String mimeType = getPartMime(parameterResult.getAnns()); return new ServerMethodParameter(name, elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), type, single, signature, converter, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - parameterResult.getCustomParameterExtractor()); + parameterResult.getCustomParameterExtractor(), mimeType); } protected void handleOtherParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { try { builder.setConverter(extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters)); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns())); } catch (Throwable throwable) { throw new RuntimeException("Could not create converter for " + elementType + " for " + builder.getErrorLocation() + " of type " + builder.getType(), throwable); @@ -331,7 +349,7 @@ protected void handleOtherParam(Map existingConverters, String e protected void handleSortedSetParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new SortedSetConverter.SortedSetSupplier(converter)); } @@ -344,7 +362,7 @@ protected void handleOptionalParam(Map existingConverters, if (genericElementType != null) { ParameterConverterSupplier genericTypeConverter = extractConverter(genericElementType, index, existingConverters, - errorLocation, hasRuntimeConverters); + errorLocation, hasRuntimeConverters, builder.getAnns()); if (LIST.toString().equals(elementType)) { converter = new ListConverter.ListSupplier(genericTypeConverter); builder.setSingle(false); @@ -362,7 +380,8 @@ protected void handleOptionalParam(Map existingConverters, if (converter == null) { // If no generic type provided or element type is not supported, then we try to use a custom runtime converter: - converter = extractConverter(elementType, index, existingConverters, errorLocation, hasRuntimeConverters); + converter = extractConverter(elementType, index, existingConverters, errorLocation, hasRuntimeConverters, + builder.getAnns()); } builder.setConverter(new OptionalConverter.OptionalSupplier(converter)); @@ -371,21 +390,21 @@ protected void handleOptionalParam(Map existingConverters, protected void handleSetParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new SetConverter.SetSupplier(converter)); } protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new ListConverter.ListSupplier(converter)); } protected void handleArrayParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns()); builder.setConverter(new ArrayConverter.ArraySupplier(converter, elementType)); } @@ -485,7 +504,11 @@ private String contextualizeErrorMessage(String errorMessage, MethodInfo current } private ParameterConverterSupplier extractConverter(String elementType, IndexView indexView, - Map existingConverters, String errorLocation, boolean hasRuntimeConverters) { + Map existingConverters, String errorLocation, boolean hasRuntimeConverters, + Map annotations) { + // no converter if we have a RestForm mime type: this goes via message body readers in MultipartFormParamExtractor + if (getPartMime(annotations) != null) + return null; if (elementType.equals(String.class.getName())) { if (hasRuntimeConverters) return new RuntimeResolvedConverter.Supplier().setDelegate(new NoopParameterConverter.Supplier()); @@ -509,6 +532,11 @@ private ParameterConverterSupplier extractConverter(String elementType, IndexVie return new CharParamConverter.Supplier(); } else if (elementType.equals(Character.class.getName())) { return new CharacterParamConverter.Supplier(); + } else if (elementType.equals(FileUpload.class.getName()) + || elementType.equals(Path.class.getName()) + || elementType.equals(File.class.getName())) { + // this is handled by MultipartFormParamExtractor + return null; } return converterSupplierIndexerExtension.extractConverterImpl(elementType, indexView, existingConverters, errorLocation, hasRuntimeConverters); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 67118258789def..c43793bfece592 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -4,12 +4,14 @@ import static org.jboss.resteasy.reactive.common.util.types.Types.getEffectiveReturnType; import static org.jboss.resteasy.reactive.common.util.types.Types.getRawType; +import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -39,6 +41,8 @@ import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import org.jboss.resteasy.reactive.common.util.ServerMediaType; import org.jboss.resteasy.reactive.common.util.types.TypeSignatureParser; +import org.jboss.resteasy.reactive.common.util.types.Types; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.DeploymentInfo; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; import org.jboss.resteasy.reactive.server.core.parameters.AsyncResponseExtractor; @@ -50,6 +54,7 @@ import org.jboss.resteasy.reactive.server.core.parameters.InjectParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.LocatableResourcePathParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.MatrixParamExtractor; +import org.jboss.resteasy.reactive.server.core.parameters.MultipartFormParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.NullParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import org.jboss.resteasy.reactive.server.core.parameters.PathParamExtractor; @@ -333,10 +338,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.RESOLVE_METHOD_PARAMETERS); for (int i = 0; i < parameters.length; i++) { ServerMethodParameter param = (ServerMethodParameter) parameters[i]; - boolean single = param.isSingle(); - ParameterExtractor extractor = parameterExtractor(pathParameterIndexes, locatableResource, param.parameterType, - param.type, param.name, - single, param.encoded, param.customParameterExtractor); + ParameterExtractor extractor = parameterExtractor(pathParameterIndexes, locatableResource, param); ParameterConverter converter = null; ParamConverterProviders paramConverterProviders = info.getParamConverterProviders(); boolean userProviderConvertersExist = !paramConverterProviders.getParamConverterProviders().isEmpty(); @@ -618,49 +620,71 @@ private ServerRestHandler alternateInvoker(ServerResourceMethod method, Endpoint } public ParameterExtractor parameterExtractor(Map pathParameterIndexes, boolean locatableResource, - ParameterType type, String javaType, - String name, - boolean single, boolean encoded, ParameterExtractor customExtractor) { + ServerMethodParameter param) { ParameterExtractor extractor; - switch (type) { + switch (param.parameterType) { case HEADER: - return new HeaderParamExtractor(name, single); + return new HeaderParamExtractor(param.name, param.isSingle()); case COOKIE: - return new CookieParamExtractor(name, javaType); + return new CookieParamExtractor(param.name, param.type); case FORM: - return new FormParamExtractor(name, single, encoded); + MultipartFormParamExtractor.Type multiPartType = null; + Class typeClass = null; + Type genericType = null; + if (param.mimeType != null && !param.mimeType.equals(MediaType.TEXT_PLAIN)) { + multiPartType = MultipartFormParamExtractor.Type.PartType; + // TODO: special primitive handling? + // FIXME: by using the element type, we're also getting converters for parameter collection types such as List/Array/Set + // but also others we may not want? + typeClass = loadClass(param.type); + genericType = TypeSignatureParser.parse(param.signature); + // strip the element type for the message body readers + genericType = Types.getMultipartElementType(genericType); + } else if (param.type.equals(FileUpload.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.FileUpload; + } else if (param.type.equals(File.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.File; + } else if (param.type.equals(Path.class.getName())) { + multiPartType = MultipartFormParamExtractor.Type.Path; + } + if (multiPartType != null) { + return new MultipartFormParamExtractor(param.name, param.isSingle(), multiPartType, typeClass, genericType, + param.mimeType); + } + // regular form + return new FormParamExtractor(param.name, param.isSingle(), param.encoded); case PATH: - Integer index = pathParameterIndexes.get(name); + Integer index = pathParameterIndexes.get(param.name); if (index == null) { if (locatableResource) { - extractor = new LocatableResourcePathParamExtractor(name); + extractor = new LocatableResourcePathParamExtractor(param.name); } else { extractor = new NullParamExtractor(); } } else { - extractor = new PathParamExtractor(index, encoded, single); + extractor = new PathParamExtractor(index, param.encoded, param.isSingle()); } return extractor; case CONTEXT: - return new ContextParamExtractor(javaType); + return new ContextParamExtractor(param.type); case ASYNC_RESPONSE: return new AsyncResponseExtractor(); case QUERY: - extractor = new QueryParamExtractor(name, single, encoded); + extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded); return extractor; case BODY: return new BodyParamExtractor(); case MATRIX: - extractor = new MatrixParamExtractor(name, single, encoded); + extractor = new MatrixParamExtractor(param.name, param.isSingle(), param.encoded); return extractor; case BEAN: - return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(loadClass(javaType))); + return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(loadClass(param.type))); case MULTI_PART_FORM: - return new InjectParamExtractor((BeanFactory) new ReflectionBeanFactoryCreator().apply(javaType)); + return new InjectParamExtractor((BeanFactory) new ReflectionBeanFactoryCreator().apply(param.type)); case CUSTOM: - return customExtractor; + return param.customParameterExtractor; default: - throw new RuntimeException("Unknown param type: " + type); + throw new RuntimeException("Unknown param type: " + param.parameterType); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java index 82ffe380fe9e93..6f1fdb67b4e20b 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java @@ -20,10 +20,11 @@ public ServerMethodParameter(String name, String type, String declaredType, Stri String signature, ParameterConverterSupplier converter, String defaultValue, boolean obtainedAsCollection, boolean optional, boolean encoded, - ParameterExtractor customParameterExtractor) { + ParameterExtractor customParameterExtractor, + String mimeType) { super(name, type, declaredType, declaredUnresolvedType, signature, parameterType, single, defaultValue, obtainedAsCollection, optional, - encoded); + encoded, mimeType); this.converter = converter; this.customParameterExtractor = customParameterExtractor; }