diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index cea258ce2771c..d33dd016be7ef 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -80,6 +80,7 @@ import org.jboss.jandex.PrimitiveType; import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientMultipartForm; import org.jboss.resteasy.reactive.client.handlers.ClientObservabilityHandler; import org.jboss.resteasy.reactive.client.impl.AbstractRxInvoker; import org.jboss.resteasy.reactive.client.impl.AsyncInvokerImpl; @@ -1833,8 +1834,8 @@ private void addInputStream(BytecodeCreator methodCreator, AssignableResultHandl ResultHandle formParamResult = methodCreator.load(formParamName); ResultHandle partFilenameResult = partFilename == null ? formParamResult : methodCreator.load(partFilename); methodCreator.assign(multipartForm, - methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "entity", - QuarkusMultipartForm.class, String.class, String.class, Object.class, String.class, Class.class), + methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientMultipartForm.class, "entity", + ClientMultipartForm.class, String.class, String.class, Object.class, String.class, Class.class), multipartForm, formParamResult, partFilenameResult, fieldValue, methodCreator.load(partType), // FIXME: doesn't support generics @@ -1844,8 +1845,8 @@ private void addInputStream(BytecodeCreator methodCreator, AssignableResultHandl private void addPojo(BytecodeCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, String partType, ResultHandle fieldValue, String type) { methodCreator.assign(multipartForm, - methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "entity", - QuarkusMultipartForm.class, String.class, Object.class, String.class, Class.class), + methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientMultipartForm.class, "entity", + ClientMultipartForm.class, String.class, Object.class, String.class, Class.class), multipartForm, methodCreator.load(formParamName), fieldValue, methodCreator.load(partType), // FIXME: doesn't support generics methodCreator.loadClassFromTCCL(type))); @@ -1870,8 +1871,8 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); // filename = name methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "binaryFileUpload", - QuarkusMultipartForm.class, String.class, String.class, String.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "binaryFileUpload", + ClientMultipartForm.class, String.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fileName, pathString, methodCreator.load(partType))); @@ -1880,8 +1881,8 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi // MultipartForm#textFileUpload(String name, String filename, String pathname, String mediaType);; // filename = name methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "textFileUpload", - QuarkusMultipartForm.class, String.class, String.class, String.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "textFileUpload", + ClientMultipartForm.class, String.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fileName, pathString, methodCreator.load(partType))); @@ -1925,8 +1926,8 @@ private void addString(BytecodeCreator methodCreator, AssignableResultHandle mul methodCreator.assign(multipartForm, // MultipartForm#stringFileUpload(String name, String filename, String content, String mediaType); methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "stringFileUpload", - QuarkusMultipartForm.class, String.class, String.class, String.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "stringFileUpload", + ClientMultipartForm.class, String.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), @@ -1936,7 +1937,7 @@ private void addString(BytecodeCreator methodCreator, AssignableResultHandle mul } else { methodCreator.assign(multipartForm, methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "attribute", QuarkusMultipartForm.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "attribute", ClientMultipartForm.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fieldValue, partFilenameHandle(methodCreator, partFilename))); @@ -1960,8 +1961,8 @@ private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandl // MultipartForm#binaryFileUpload(String name, String filename, Multi content, String mediaType); // filename = name methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "multiAsBinaryFileUpload", - QuarkusMultipartForm.class, String.class, String.class, Multi.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "multiAsBinaryFileUpload", + ClientMultipartForm.class, String.class, String.class, Multi.class, String.class), multipartForm, methodCreator.load(formParamName), methodCreator.load(filename), multi, methodCreator.load(partType))); @@ -1970,8 +1971,8 @@ private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandl // MultipartForm#multiAsTextFileUpload(String name, String filename, Multi content, String mediaType) // filename = name methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "multiAsTextFileUpload", - QuarkusMultipartForm.class, String.class, String.class, Multi.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "multiAsTextFileUpload", + ClientMultipartForm.class, String.class, String.class, Multi.class, String.class), multipartForm, methodCreator.load(formParamName), methodCreator.load(filename), multi, methodCreator.load(partType))); @@ -1990,8 +1991,8 @@ private void addBuffer(BytecodeCreator methodCreator, AssignableResultHandle mul methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, io.vertx.mutiny.core.buffer.Buffer content, String mediaType); methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "binaryFileUpload", - QuarkusMultipartForm.class, String.class, String.class, Buffer.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "binaryFileUpload", + ClientMultipartForm.class, String.class, String.class, Buffer.class, String.class), multipartForm, methodCreator.load(formParamName), filenameHandle, buffer, methodCreator.load(partType))); @@ -1999,8 +2000,8 @@ private void addBuffer(BytecodeCreator methodCreator, AssignableResultHandle mul methodCreator.assign(multipartForm, // MultipartForm#textFileUpload(String name, String filename, io.vertx.mutiny.core.buffer.Buffer content, String mediaType) methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "textFileUpload", - QuarkusMultipartForm.class, String.class, String.class, Buffer.class, + MethodDescriptor.ofMethod(ClientMultipartForm.class, "textFileUpload", + ClientMultipartForm.class, String.class, String.class, Buffer.class, String.class), multipartForm, methodCreator.load(formParamName), filenameHandle, buffer, methodCreator.load(partType))); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java index 78181fde6734e..6a20d4d959f3f 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java @@ -50,6 +50,7 @@ import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientMultipartForm; import org.jboss.resteasy.reactive.client.impl.WebTargetImpl; import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; @@ -137,8 +138,8 @@ class MicroProfileRestClientEnricher implements JaxrsClientReactiveEnricher { private static final MethodDescriptor MULTIVALUED_MAP_ADD_ALL_METHOD = MethodDescriptor.ofMethod(MultivaluedMap.class, "addAll", void.class, Object.class, List.class); private static final MethodDescriptor QUARKUS_MULTIPART_FORM_ATTRIBUTE_METHOD = MethodDescriptor.ofMethod( - QuarkusMultipartForm.class, - "attribute", QuarkusMultipartForm.class, String.class, String.class, String.class); + ClientMultipartForm.class, + "attribute", ClientMultipartForm.class, String.class, String.class, String.class); private static final Type STRING_TYPE = Type.create(DotName.STRING_NAME, Type.Kind.CLASS); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartProgrammaticTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartProgrammaticTest.java new file mode 100644 index 0000000000000..c69a80f88172a --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartProgrammaticTest.java @@ -0,0 +1,107 @@ +package io.quarkus.rest.client.reactive.multipart; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.FileInputStream; +import java.net.URI; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientMultipartForm; +import org.jboss.resteasy.reactive.multipart.FileUpload; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.smallrye.mutiny.Multi; + +public class MultipartProgrammaticTest { + + private static final Logger log = Logger.getLogger(MultipartProgrammaticTest.class); + + private static final int BYTES_SENT = 5_000_000; // 5 megs + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Resource.class, FormData.class, Client.class)); + + @TestHTTPResource + URI baseUri; + + @Test + void shouldUploadBiggishFile() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + AtomicLong i = new AtomicLong(); + Multi content = Multi.createBy().repeating().supplier( + () -> (byte) ((i.getAndIncrement() + 1) % 123)).atMost(BYTES_SENT); + String result = client.postMultipart(ClientMultipartForm.create() + .multiAsBinaryFileUpload("fileFormName", "fileName", content, MediaType.APPLICATION_OCTET_STREAM) + .stringFileUpload("otherFormName", "whatever", "test", MediaType.TEXT_PLAIN)); + assertThat(result).isEqualTo("fileFormName/fileName-test"); + } + + @Path("/multipart") + public interface Client { + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipart(ClientMultipartForm form); + } + + @Path("/multipart") + public static class Resource { + @Path("/") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String upload(FormData form) { + return verifyFile(form.file, BYTES_SENT, position -> (byte) (((1 + position) % 123))) + "-" + form.other; + } + + private String verifyFile(FileUpload upload, int expectedSize, Function expectedByte) { + var uploadedFile = upload.uploadedFile(); + int size; + + try (FileInputStream inputStream = new FileInputStream(uploadedFile.toFile())) { + int position = 0; + int b; + while ((b = inputStream.read()) != -1) { + long expected = expectedByte.apply(position); + position++; + if (expected != b) { + throw new RuntimeException( + "WRONG_BYTE_READ at pos " + (position - 1) + " expected: " + expected + " got: " + b); + } + } + size = position; + } catch (RuntimeException e) { + return e.getMessage(); + } catch (Exception e) { + log.error("Unexpected error in the test resource", e); + return "UNEXPECTED ERROR"; + } + + if (size != expectedSize) { + return "READ_WRONG_AMOUNT_OF_BYTES " + size; + } + return upload.name() + "/" + upload.fileName(); + } + } + + public static class FormData { + @FormParam("fileFormName") + public FileUpload file; + + @FormParam("otherFormName") + public String other; + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java new file mode 100644 index 0000000000000..7b7bc74d9c220 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java @@ -0,0 +1,89 @@ +package org.jboss.resteasy.reactive.client.api; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormDataPart; + +import io.smallrye.mutiny.Multi; +import io.vertx.core.buffer.Buffer; + +/** + * This class allows programmatic creation of multipart requests + */ +public abstract class ClientMultipartForm { + + protected Charset charset = StandardCharsets.UTF_8; + protected final List parts = new ArrayList<>(); + protected final List pojos = new ArrayList<>(); + + public static ClientMultipartForm create() { + return new QuarkusMultipartForm(); + } + + public ClientMultipartForm setCharset(String charset) { + return setCharset(charset != null ? Charset.forName(charset) : null); + } + + public ClientMultipartForm setCharset(Charset charset) { + this.charset = charset; + return this; + } + + public Charset getCharset() { + return charset; + } + + public ClientMultipartForm attribute(String name, String value, String filename) { + parts.add(new QuarkusMultipartFormDataPart(name, value, filename)); + return this; + } + + public ClientMultipartForm entity(String name, Object entity, String mediaType, Class type) { + return entity(name, null, entity, mediaType, type); + } + + public ClientMultipartForm entity(String name, String filename, Object entity, String mediaType, Class type) { + pojos.add(new QuarkusMultipartForm.PojoFieldData(name, filename, entity, mediaType, type, parts.size())); + parts.add(null); // make place for ^ + return this; + } + + public ClientMultipartForm textFileUpload(String name, String filename, String pathname, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, pathname, mediaType, true)); + return this; + } + + public ClientMultipartForm textFileUpload(String name, String filename, Buffer content, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, true)); + return this; + } + + public ClientMultipartForm stringFileUpload(String name, String filename, String content, String mediaType) { + return textFileUpload(name, filename, Buffer.buffer(content), mediaType); + } + + public ClientMultipartForm binaryFileUpload(String name, String filename, String pathname, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, pathname, mediaType, false)); + return this; + } + + public ClientMultipartForm binaryFileUpload(String name, String filename, Buffer content, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, false)); + return this; + } + + public ClientMultipartForm multiAsBinaryFileUpload(String name, String filename, Multi content, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, false)); + return this; + } + + public ClientMultipartForm multiAsTextFileUpload(String name, String filename, Multi content, String mediaType) { + parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, true)); + return this; + } + +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java index 36ce207fcaba2..11277d59b7aad 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java @@ -2,9 +2,6 @@ import java.io.IOException; import java.lang.reflect.Type; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -14,91 +11,18 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.MessageBodyWriter; +import org.jboss.resteasy.reactive.client.api.ClientMultipartForm; import org.jboss.resteasy.reactive.client.impl.ClientSerialisers; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; -import io.smallrye.mutiny.Multi; import io.vertx.core.buffer.Buffer; /** * based on {@link io.vertx.ext.web.multipart.MultipartForm} and {@link io.vertx.ext.web.multipart.impl.MultipartFormImpl} */ -public class QuarkusMultipartForm implements Iterable { - - private Charset charset = StandardCharsets.UTF_8; - private final List parts = new ArrayList<>(); - private final List pojos = new ArrayList<>(); - - public QuarkusMultipartForm setCharset(String charset) { - return setCharset(charset != null ? Charset.forName(charset) : null); - } - - public QuarkusMultipartForm setCharset(Charset charset) { - this.charset = charset; - return this; - } - - public Charset getCharset() { - return charset; - } - - public QuarkusMultipartForm attribute(String name, String value, String filename) { - parts.add(new QuarkusMultipartFormDataPart(name, value, filename)); - return this; - } - - public QuarkusMultipartForm entity(String name, Object entity, String mediaType, Class type) { - return entity(name, null, entity, mediaType, type); - } - - public QuarkusMultipartForm entity(String name, String filename, Object entity, String mediaType, Class type) { - pojos.add(new PojoFieldData(name, filename, entity, mediaType, type, parts.size())); - parts.add(null); // make place for ^ - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm textFileUpload(String name, String filename, String pathname, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, pathname, mediaType, true)); - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm textFileUpload(String name, String filename, Buffer content, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, true)); - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm stringFileUpload(String name, String filename, String content, String mediaType) { - return textFileUpload(name, filename, Buffer.buffer(content), mediaType); - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm binaryFileUpload(String name, String filename, String pathname, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, pathname, mediaType, false)); - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm binaryFileUpload(String name, String filename, Buffer content, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, false)); - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm multiAsBinaryFileUpload(String name, String filename, Multi content, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, false)); - return this; - } - - @SuppressWarnings("unused") - public QuarkusMultipartForm multiAsTextFileUpload(String name, String filename, Multi content, String mediaType) { - parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, true)); - return this; - } +public class QuarkusMultipartForm extends ClientMultipartForm implements Iterable { @Override public Iterator iterator() {