From 106b744f930cdafb9f6e0cb545501bff9a2d9669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szynkiewicz?= Date: Mon, 13 Sep 2021 19:03:11 +0200 Subject: [PATCH] REST Client Reactive: support POJO as non-file fields in multipart messages fixes #19892 --- .../JaxrsClientReactiveProcessor.java | 60 +++--- .../reactive/runtime/MultipartFormUtils.java | 17 -- .../handlers/ClientSendRequestHandler.java | 18 +- .../client/impl/RestClientRequestContext.java | 10 +- .../impl/multipart/QuarkusMultipartForm.java | 134 ++++++++++++ .../QuarkusMultipartFormDataPart.java | 147 +++++++++++++ .../multipart/QuarkusMultipartFormUpload.java | 202 ++++++++++++++++++ .../common/processor/EndpointIndexer.java | 2 +- .../rest-client-reactive-multipart/pom.xml | 8 +- .../client/multipart/MultipartClient.java | 40 ++++ .../client/multipart/MultipartResource.java | 50 ++++- .../multipart/MultipartResourceTest.java | 12 ++ 12 files changed, 639 insertions(+), 61 deletions(-) delete mode 100644 extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/MultipartFormUtils.java create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormDataPart.java create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java 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 6ec043a1f8bd3c..713889bb861c15 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 @@ -61,6 +61,7 @@ import org.jboss.resteasy.reactive.client.impl.MultiInvoker; import org.jboss.resteasy.reactive.client.impl.UniInvoker; import org.jboss.resteasy.reactive.client.impl.WebTargetImpl; +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.processor.beanparam.BeanParamItem; import org.jboss.resteasy.reactive.client.processor.beanparam.ClientBeanParamInfo; import org.jboss.resteasy.reactive.client.processor.beanparam.CookieParamItem; @@ -121,7 +122,6 @@ import io.quarkus.gizmo.TryBlock; import io.quarkus.jaxrs.client.reactive.runtime.ClientResponseBuilderFactory; import io.quarkus.jaxrs.client.reactive.runtime.JaxrsClientReactiveRecorder; -import io.quarkus.jaxrs.client.reactive.runtime.MultipartFormUtils; import io.quarkus.jaxrs.client.reactive.runtime.ToObjectArray; import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.QuarkusFactoryCreator; @@ -137,7 +137,6 @@ import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.multipart.MultipartForm; public class JaxrsClientReactiveProcessor { @@ -963,11 +962,9 @@ A more full example of generated client (with sub-resource) can is at the bottom */ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHandle methodParam, Type formClassType, IndexView index) { - AssignableResultHandle multipartForm = methodCreator.createVariable(MultipartForm.class); + AssignableResultHandle multipartForm = methodCreator.createVariable(QuarkusMultipartForm.class); methodCreator.assign(multipartForm, - methodCreator - .invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartFormUtils.class, "create", MultipartForm.class))); + methodCreator.newInstance(MethodDescriptor.ofConstructor(QuarkusMultipartForm.class))); ClassInfo formClass = index.getClassByName(formClassType.name()); @@ -1016,10 +1013,8 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand } else if (is(BUFFER, fieldClass, index)) { // and buffer addBuffer(methodCreator, multipartForm, formParamName, partType, fieldValue, field); - } else { - throw new IllegalArgumentException("Unsupported multipart form field on: " + formClassType.name() - + "." + fieldType.name() + - ". Supported types are: java.lang.String, java.io.File, java.nio.Path and io.vertx.core.Buffer"); + } else { // assume POJO: + addPojo(methodCreator, multipartForm, formParamName, partType, fieldValue, field); } break; case ARRAY: @@ -1030,8 +1025,8 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand throw new IllegalArgumentException("Array of unsupported type: " + componentType.name() + " on " + formClassType.name() + "." + field.name()); } - ResultHandle buffer = methodCreator.invokeStaticMethod( - MethodDescriptor.ofMethod(MultipartFormUtils.class, "buffer", Buffer.class, byte[].class), + ResultHandle buffer = methodCreator.invokeStaticInterfaceMethod( + MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class), fieldValue); addBuffer(methodCreator, multipartForm, formParamName, partType, buffer, field); break; @@ -1053,9 +1048,18 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand return multipartForm; } + private void addPojo(MethodCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, + String partType, ResultHandle fieldValue, FieldInfo field) { + methodCreator.assign(multipartForm, + methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "entity", + QuarkusMultipartForm.class, String.class, Object.class, String.class, Class.class), + multipartForm, methodCreator.load(field.name()), fieldValue, methodCreator.load(partType), + methodCreator.loadClass(field.type().name().toString()))); + } + /** - * add file upload, see {@link MultipartForm#binaryFileUpload(String, String, String, String)} and - * {@link MultipartForm#textFileUpload(String, String, String, String)} + * add file upload, see {@link QuarkusMultipartForm#binaryFileUpload(String, String, String, String)} and + * {@link QuarkusMultipartForm#textFileUpload(String, String, String, String)} */ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, String partType, ResultHandle filePath) { @@ -1066,9 +1070,9 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); // filename = name - methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(MultipartForm.class, "binaryFileUpload", - MultipartForm.class, String.class, String.class, String.class, + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "binaryFileUpload", + QuarkusMultipartForm.class, String.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fileName, pathString, methodCreator.load(partType))); @@ -1076,9 +1080,9 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa methodCreator.assign(multipartForm, // MultipartForm#textFileUpload(String name, String filename, String pathname, String mediaType);; // filename = name - methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(MultipartForm.class, "textFileUpload", - MultipartForm.class, String.class, String.class, String.class, + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "textFileUpload", + QuarkusMultipartForm.class, String.class, String.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fileName, pathString, methodCreator.load(partType))); @@ -1115,8 +1119,8 @@ private ResultHandle primitiveToString(MethodCreator methodCreator, ResultHandle private void addString(MethodCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, ResultHandle fieldValue) { methodCreator.assign(multipartForm, - methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(MultipartForm.class, "attribute", MultipartForm.class, + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "attribute", QuarkusMultipartForm.class, String.class, String.class), multipartForm, methodCreator.load(formParamName), fieldValue)); } @@ -1132,9 +1136,9 @@ private void addBuffer(MethodCreator methodCreator, AssignableResultHandle multi methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); // filename = name - methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(MultipartForm.class, "binaryFileUpload", - MultipartForm.class, String.class, String.class, Buffer.class, + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "binaryFileUpload", + QuarkusMultipartForm.class, String.class, String.class, Buffer.class, String.class), multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), buffer, methodCreator.load(partType))); @@ -1142,9 +1146,9 @@ private void addBuffer(MethodCreator methodCreator, AssignableResultHandle multi methodCreator.assign(multipartForm, // MultipartForm#textFileUpload(String name, String filename, io.vertx.mutiny.core.buffer.Buffer content, String mediaType) // filename = name - methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(MultipartForm.class, "textFileUpload", - MultipartForm.class, String.class, String.class, Buffer.class, + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(QuarkusMultipartForm.class, "textFileUpload", + QuarkusMultipartForm.class, String.class, String.class, Buffer.class, String.class), multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), buffer, methodCreator.load(partType))); diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/MultipartFormUtils.java b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/MultipartFormUtils.java deleted file mode 100644 index 3cc6157e871abc..00000000000000 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/MultipartFormUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.quarkus.jaxrs.client.reactive.runtime; - -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.multipart.MultipartForm; - -public class MultipartFormUtils { - public static MultipartForm create() { - return MultipartForm.create(); - } - - public static Buffer buffer(byte[] bytes) { - return Buffer.buffer(bytes); - } - - private MultipartFormUtils() { - } -} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index 62bed99697d57e..62057faa31cfc0 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -13,8 +13,6 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.http.RequestOptions; import io.vertx.core.streams.Pipe; -import io.vertx.ext.web.client.impl.MultipartFormUpload; -import io.vertx.ext.web.multipart.MultipartForm; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; @@ -28,6 +26,8 @@ import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.AsyncInvokerImpl; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormUpload; import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.common.core.Serialisers; @@ -62,7 +62,7 @@ public void handle(HttpClientRequest httpClientRequest) { Future sent; if (requestContext.isMultipart()) { Promise requestPromise = Promise.promise(); - MultipartFormUpload actualEntity; + QuarkusMultipartFormUpload actualEntity; try { actualEntity = ClientSendRequestHandler.this.setMultipartHeadersAndPrepareBody(httpClientRequest, requestContext); @@ -173,20 +173,24 @@ public Future createRequest(RestClientRequestContext state) { return httpClient.request(requestOptions); } - private MultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientRequest httpClientRequest, + private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientRequest httpClientRequest, RestClientRequestContext state) throws Exception { - if (!(state.getEntity().getEntity() instanceof MultipartForm)) { + if (!(state.getEntity().getEntity() instanceof QuarkusMultipartForm)) { throw new IllegalArgumentException( "Multipart form upload expects an entity of type MultipartForm, got: " + state.getEntity().getEntity()); } MultivaluedMap headerMap = state.getRequestHeaders().asMap(); - MultipartForm entity = (MultipartForm) state.getEntity().getEntity(); + QuarkusMultipartForm multipartForm = (QuarkusMultipartForm) state.getEntity().getEntity(); + multipartForm.preparePojos(state); + Object property = state.getConfiguration().getProperty(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE); HttpPostRequestEncoder.EncoderMode mode = HttpPostRequestEncoder.EncoderMode.RFC1738; if (property != null) { mode = (HttpPostRequestEncoder.EncoderMode) property; } - MultipartFormUpload multipartFormUpload = new MultipartFormUpload(Vertx.currentContext(), entity, true, mode); + QuarkusMultipartFormUpload multipartFormUpload = new QuarkusMultipartFormUpload(Vertx.currentContext(), multipartForm, + true, + mode); setEntityRelatedHeaders(headerMap, state.getEntity()); // multipart has its own headers: diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index cdfe4570fd3473..07b98c3ddf9e77 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -6,7 +6,6 @@ import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; -import io.vertx.ext.web.multipart.MultipartForm; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; @@ -32,6 +31,7 @@ import javax.ws.rs.ext.WriterInterceptor; import org.jboss.resteasy.reactive.ClientWebApplicationException; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext; import org.jboss.resteasy.reactive.common.core.Serialisers; @@ -156,10 +156,14 @@ public T readEntity(InputStream in, configuration); } - ReaderInterceptor[] getReaderInterceptors() { + public ReaderInterceptor[] getReaderInterceptors() { return configuration.getReaderInterceptors().toArray(Serialisers.NO_READER_INTERCEPTOR); } + public Map getProperties() { + return properties; + } + public void initialiseResponse(HttpClientResponse vertxResponse) { MultivaluedMap headers = new CaseInsensitiveMap<>(); MultiMap vertxHeaders = vertxResponse.headers(); @@ -413,7 +417,7 @@ public RestClientRequestContext setAbortedWith(Response abortedWith) { } public boolean isMultipart() { - return entity != null && entity.getEntity() instanceof MultipartForm; + return entity != null && entity.getEntity() instanceof QuarkusMultipartForm; } public Map getClientFilterProperties() { 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 new file mode 100644 index 00000000000000..394252966cd82b --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartForm.java @@ -0,0 +1,134 @@ +package org.jboss.resteasy.reactive.client.impl.multipart; + +import io.vertx.core.buffer.Buffer; +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; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +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; + +/** + * 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) { + parts.add(new QuarkusMultipartFormDataPart(name, value)); + return this; + } + + public QuarkusMultipartForm entity(String name, Object entity, String mediaType, Class type) { + pojos.add(new PojoFieldData(name, 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 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; + } + + @Override + public Iterator iterator() { + return parts.iterator(); + } + + public void preparePojos(RestClientRequestContext context) throws IOException { + Serialisers serialisers = context.getRestClient().getClientContext().getSerialisers(); + for (PojoFieldData pojo : pojos) { + MultivaluedMap headers = new MultivaluedTreeMap<>(); + + Object entityObject = pojo.entity; + Entity entity = Entity.entity(entityObject, pojo.mediaType); + Class entityClass; + Type entityType; + if (entityObject instanceof GenericEntity) { + GenericEntity genericEntity = (GenericEntity) entityObject; + entityClass = genericEntity.getRawType(); + entityType = pojo.type; + entityObject = genericEntity.getEntity(); + } else { + entityType = entityClass = pojo.type; + } + + List> writers = serialisers.findWriters(context.getConfiguration(), + entityClass, entity.getMediaType(), + RuntimeType.CLIENT); + Buffer value = null; + for (MessageBodyWriter w : writers) { + Buffer ret = ClientSerialisers.invokeClientWriter(entity, entityObject, entityClass, entityType, headers, w, + context.getConfiguration().getWriterInterceptors().toArray(Serialisers.NO_WRITER_INTERCEPTOR), + context.getProperties(), + serialisers, context.getConfiguration()); + if (ret != null) { + value = ret; + break; + } + } + parts.set(pojo.position, new QuarkusMultipartFormDataPart(pojo.name, value, pojo.mediaType, pojo.type)); + } + } + + public static class PojoFieldData { + private final String name; + private final Object entity; + private final String mediaType; + private final Class type; + private final int position; + + public PojoFieldData(String name, Object entity, String mediaType, Class type, int position) { + this.name = name; + this.entity = entity; + this.mediaType = mediaType; + this.type = type; + this.position = position; + } + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormDataPart.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormDataPart.java new file mode 100644 index 00000000000000..61f07868d3d071 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormDataPart.java @@ -0,0 +1,147 @@ +package org.jboss.resteasy.reactive.client.impl.multipart; + +import io.vertx.core.buffer.Buffer; + +/** + * based on {@link io.vertx.ext.web.multipart.impl.FormDataPartImpl} + */ +public class QuarkusMultipartFormDataPart { + + private final String name; + private final String value; + private final String filename; + private final String mediaType; + private final String pathname; + private final boolean text; + private final boolean isObject; + private final Class type; + private final Buffer content; + + public QuarkusMultipartFormDataPart(String name, Buffer content, String mediaType, Class type) { + this.name = name; + this.content = content; + this.mediaType = mediaType; + this.type = type; + + if (name == null) { + throw new NullPointerException("Multipart field name cannot be null"); + } + if (mediaType == null) { + throw new NullPointerException("Multipart field media type cannot be null"); + } + if (type == null) { + throw new NullPointerException("Multipart field media type cannot be null"); + } + this.isObject = true; + this.value = null; + this.filename = null; + this.pathname = null; + this.text = false; + } + + public QuarkusMultipartFormDataPart(String name, String value) { + if (name == null) { + throw new NullPointerException("Multipart field name cannot be null"); + } + if (value == null) { + throw new NullPointerException("Multipart field value cannot be null"); + } + this.name = name; + this.value = value; + this.filename = null; + this.pathname = null; + this.content = null; + this.mediaType = null; + this.text = false; + this.isObject = false; + this.type = null; + } + + public QuarkusMultipartFormDataPart(String name, String filename, String pathname, String mediaType, boolean text) { + if (name == null) { + throw new NullPointerException("Multipart field name cannot be null"); + } + if (filename == null) { + throw new NullPointerException("Multipart field name filename cannot be null when sending files"); + } + if (pathname == null) { + throw new NullPointerException("Multipart field name pathname cannot be null when sending files"); + } + if (mediaType == null) { + throw new NullPointerException("Multipart field media type cannot be null"); + } + this.name = name; + this.value = null; + this.filename = filename; + this.pathname = pathname; + this.content = null; + this.mediaType = mediaType; + this.text = text; + this.isObject = false; + this.type = null; + } + + public QuarkusMultipartFormDataPart(String name, String filename, Buffer content, String mediaType, boolean text) { + if (name == null) { + throw new NullPointerException("Multipart field name cannot be null"); + } + if (filename == null) { + throw new NullPointerException("Multipart field name filename cannot be null when sending files"); + } + if (content == null) { + throw new NullPointerException("Multipart field name content cannot be null when sending files"); + } + if (mediaType == null) { + throw new NullPointerException("Multipart field media type cannot be null"); + } + this.name = name; + this.value = null; + this.filename = filename; + this.pathname = null; + this.content = content; + this.mediaType = mediaType; + this.text = text; + this.isObject = false; + this.type = null; + } + + public String name() { + return name; + } + + public boolean isAttribute() { + return value != null; + } + + public boolean isObject() { + return isObject; + } + + public String value() { + return value; + } + + public String filename() { + return filename; + } + + public String pathname() { + return pathname; + } + + public Buffer content() { + return content; + } + + public String mediaType() { + return mediaType; + } + + public boolean isText() { + return text; + } + + public Class getType() { + return type; + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java new file mode 100644 index 00000000000000..70f13fc378d4bd --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java @@ -0,0 +1,202 @@ +package org.jboss.resteasy.reactive.client.impl.multipart; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpConstants; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.FileUpload; +import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; +import io.netty.handler.codec.http.multipart.MemoryFileUpload; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.impl.headers.HeadersAdaptor; +import io.vertx.core.streams.ReadStream; +import io.vertx.core.streams.impl.InboundBuffer; +import java.io.File; +import java.nio.charset.Charset; + +/** + * based on {@link io.vertx.ext.web.client.impl.MultipartFormUpload} + */ +public class QuarkusMultipartFormUpload implements ReadStream { + + private static final UnpooledByteBufAllocator ALLOC = new UnpooledByteBufAllocator(false); + + private DefaultFullHttpRequest request; + private HttpPostRequestEncoder encoder; + private Handler exceptionHandler; + private Handler dataHandler; + private Handler endHandler; + private boolean ended; + private final InboundBuffer pending; + private final Context context; + + public QuarkusMultipartFormUpload(Context context, + QuarkusMultipartForm parts, + boolean multipart, + HttpPostRequestEncoder.EncoderMode encoderMode) throws Exception { + this.context = context; + this.pending = new InboundBuffer<>(context) + .handler(this::handleChunk) + .drainHandler(v -> run()).pause(); + this.request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + io.netty.handler.codec.http.HttpMethod.POST, + "/"); + Charset charset = parts.getCharset() != null ? parts.getCharset() : HttpConstants.DEFAULT_CHARSET; + this.encoder = new HttpPostRequestEncoder( + new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE, charset) { + @Override + public FileUpload createFileUpload(HttpRequest request, String name, String filename, String contentType, + String contentTransferEncoding, Charset _charset, long size) { + if (_charset == null) { + _charset = charset; + } + return super.createFileUpload(request, name, filename, contentType, contentTransferEncoding, _charset, + size); + } + }, + request, + multipart, + charset, + encoderMode); + for (QuarkusMultipartFormDataPart formDataPart : parts) { + if (formDataPart.isAttribute()) { + encoder.addBodyAttribute(formDataPart.name(), formDataPart.value()); + } else if (formDataPart.isObject()) { + MemoryFileUpload data = new MemoryFileUpload(formDataPart.name(), "", formDataPart.mediaType(), + formDataPart.isText() ? null : "binary", null, formDataPart.content().length()); + data.setContent(formDataPart.content().getByteBuf()); + encoder.addBodyHttpData(data); + } else { + String pathname = formDataPart.pathname(); + if (pathname != null) { + encoder.addBodyFileUpload(formDataPart.name(), + formDataPart.filename(), new File(formDataPart.pathname()), + formDataPart.mediaType(), formDataPart.isText()); + } else { + String contentType = formDataPart.mediaType(); + if (formDataPart.mediaType() == null) { + if (formDataPart.isText()) { + contentType = "text/plain"; + } else { + contentType = "application/octet-stream"; + } + } + String transferEncoding = formDataPart.isText() ? null : "binary"; + MemoryFileUpload fileUpload = new MemoryFileUpload( + formDataPart.name(), + formDataPart.filename(), + contentType, transferEncoding, null, formDataPart.content().length()); + fileUpload.setContent(formDataPart.content().getByteBuf()); + encoder.addBodyHttpData(fileUpload); + } + } + } + encoder.finalizeRequest(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void handleChunk(Object item) { + Handler handler; + synchronized (QuarkusMultipartFormUpload.this) { + if (item instanceof Buffer) { + handler = dataHandler; + } else if (item instanceof Throwable) { + handler = exceptionHandler; + } else if (item == InboundBuffer.END_SENTINEL) { + handler = endHandler; + item = null; + } else { + return; + } + } + handler.handle(item); + } + + public void run() { + if (Vertx.currentContext() != context) { + throw new IllegalArgumentException(); + } + while (!ended) { + if (encoder.isChunked()) { + try { + HttpContent chunk = encoder.readChunk(ALLOC); + ByteBuf content = chunk.content(); + Buffer buff = Buffer.buffer(content); + boolean writable = pending.write(buff); + if (encoder.isEndOfInput()) { + ended = true; + request = null; + encoder = null; + pending.write(InboundBuffer.END_SENTINEL); + } else if (!writable) { + break; + } + } catch (Exception e) { + ended = true; + request = null; + encoder = null; + pending.write(e); + break; + } + } else { + ByteBuf content = request.content(); + Buffer buffer = Buffer.buffer(content); + request = null; + encoder = null; + pending.write(buffer); + ended = true; + pending.write(InboundBuffer.END_SENTINEL); + } + } + } + + public MultiMap headers() { + return new HeadersAdaptor(request.headers()); + } + + @Override + public synchronized QuarkusMultipartFormUpload exceptionHandler(Handler handler) { + exceptionHandler = handler; + return this; + } + + @Override + public synchronized QuarkusMultipartFormUpload handler(Handler handler) { + dataHandler = handler; + return this; + } + + @Override + public synchronized QuarkusMultipartFormUpload pause() { + pending.pause(); + return this; + } + + @Override + public ReadStream fetch(long amount) { + pending.fetch(amount); + return this; + } + + @Override + public synchronized QuarkusMultipartFormUpload resume() { + pending.resume(); + return this; + } + + @Override + public synchronized QuarkusMultipartFormUpload endHandler(Handler handler) { + endHandler = handler; + return this; + } + +} diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 5cd644546e3a0f..618ca7c146c5b5 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -483,7 +483,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf // TODO: does it make sense to default to MediaType.MULTIPART_FORM_DATA when no consumes is set? if (!validConsumes) { throw new RuntimeException( - "'@MultipartForm' can only be used on methods that annotated with '@Consumes(MediaType.MULTIPART_FORM_DATA)'. Offending method is '" + "'@MultipartForm' can only be used on methods annotated with '@Consumes(MediaType.MULTIPART_FORM_DATA)'. Offending method is '" + currentMethodInfo.declaringClass().name() + "#" + currentMethodInfo + "'"); } } diff --git a/integration-tests/rest-client-reactive-multipart/pom.xml b/integration-tests/rest-client-reactive-multipart/pom.xml index ee3a0fad2d55a2..0a4e6f34f4ba14 100644 --- a/integration-tests/rest-client-reactive-multipart/pom.xml +++ b/integration-tests/rest-client-reactive-multipart/pom.xml @@ -15,12 +15,12 @@ io.quarkus - quarkus-rest-client-reactive + quarkus-rest-client-reactive-jackson io.quarkus - quarkus-resteasy-reactive + quarkus-resteasy-reactive-jackson @@ -42,7 +42,7 @@ io.quarkus - quarkus-rest-client-reactive-deployment + quarkus-rest-client-reactive-jackson-deployment ${project.version} pom test @@ -55,7 +55,7 @@ io.quarkus - quarkus-resteasy-reactive-deployment + quarkus-resteasy-reactive-jackson-deployment ${project.version} pom test diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java index f7d81d1f2ca134..d805de10c1c179 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartClient.java @@ -67,6 +67,46 @@ public interface MultipartClient { @Path("/text") String sendPathAsTextFile(@MultipartForm WithPathAsTextFile data); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("/with-pojo") + String sendFileWithPojo(@MultipartForm FileWithPojo data); + + class FileWithPojo { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public byte[] file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + + @FormParam("pojo") + @PartType(MediaType.APPLICATION_JSON) + public Pojo pojo; + } + + class Pojo { + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + class WithByteArrayAsBinaryFile { @FormParam("file") diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java index e90f677193c61d..7ded6e47ffd8c2 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java @@ -13,6 +13,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import org.eclipse.microprofile.rest.client.inject.RestClient; @@ -20,6 +21,8 @@ import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; +import io.quarkus.it.rest.client.multipart.MultipartClient.FileWithPojo; +import io.quarkus.it.rest.client.multipart.MultipartClient.Pojo; import io.quarkus.it.rest.client.multipart.MultipartClient.WithBufferAsBinaryFile; import io.quarkus.it.rest.client.multipart.MultipartClient.WithBufferAsTextFile; import io.quarkus.it.rest.client.multipart.MultipartClient.WithByteArrayAsBinaryFile; @@ -42,6 +45,26 @@ public class MultipartResource { @RestClient MultipartClient client; + @GET + @Path("/client/byte-array-as-binary-file-with-pojo") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayWithPojo() { + FileWithPojo data = new FileWithPojo(); + data.file = HELLO_WORLD.getBytes(UTF_8); + data.fileName = GREETING_TXT; + data.pojo = new Pojo(); + data.pojo.setName("some-name"); + data.pojo.setValue("some-value"); + try { + return client.sendFileWithPojo(data); + } catch (WebApplicationException e) { + String responseAsString = e.getResponse().readEntity(String.class); + return String.format("Error: %s statusCode %s", responseAsString, e.getResponse().getStatus()); + } + } + @GET @Path("/client/byte-array-as-binary-file") @Consumes(MediaType.TEXT_PLAIN) @@ -180,7 +203,17 @@ public String consumeText(@MultipartForm MultipartBodyWithTextFile body) { return String.format("fileOk:%s,numberOk:%s", containsHelloWorld(body.file), NUMBER == Integer.parseInt(body.number)); } - private Object containsHelloWorld(File file) { + @POST + @Path("/echo/with-pojo") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String consumeBinaryWithPojo(@MultipartForm MultipartBodyWithBinaryFileAndPojo fileWithPojo) { + return String.format("fileOk:%s,nameOk:%s,pojoOk:%s", + containsHelloWorld(fileWithPojo.file), + GREETING_TXT.equals(fileWithPojo.fileName), + "some-name".equals(fileWithPojo.pojo.getName()) && "some-value".equals(fileWithPojo.pojo.getValue())); + } + + private boolean containsHelloWorld(File file) { try { String actual = new String(Files.readAllBytes(file.toPath())); return HELLO_WORLD.equals(actual); @@ -212,4 +245,19 @@ public static class MultipartBodyWithTextFile { public String number; } + public static class MultipartBodyWithBinaryFileAndPojo { + + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + + @FormParam("pojo") + @PartType(MediaType.APPLICATION_JSON) + public Pojo pojo; + } + } diff --git a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java index 654ea5a38b20f1..a4322823104147 100644 --- a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java +++ b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java @@ -105,4 +105,16 @@ public void shouldSendPathAsTextFile() { .body(equalTo("fileOk:true,numberOk:true")); // @formatter:on } + + @Test + public void shouldSendByteArrayAndPojo() { + // @formatter:off + given() + .header("Content-Type", "text/plain") + .when().get("/client/byte-array-as-binary-file-with-pojo") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true,pojoOk:true")); + // @formatter:on + } }