Skip to content

Commit

Permalink
Merge pull request quarkusio#38031 from geoand/rest-client-multipart-api
Browse files Browse the repository at this point in the history
Expose an API for programmatically creating multipart requests in reactive REST Client
  • Loading branch information
geoand authored Jan 4, 2024
2 parents 09c7d8a + 0e99c87 commit 620aa9c
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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)));
Expand All @@ -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)));
Expand All @@ -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)));
Expand Down Expand Up @@ -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),
Expand All @@ -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)));
Expand All @@ -1960,8 +1961,8 @@ private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandl
// MultipartForm#binaryFileUpload(String name, String filename, Multi<Byte> 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)));
Expand All @@ -1970,8 +1971,8 @@ private void addMultiAsFile(BytecodeCreator methodCreator, AssignableResultHandl
// MultipartForm#multiAsTextFileUpload(String name, String filename, Multi<Byte> 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)));
Expand All @@ -1990,17 +1991,17 @@ 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)));
} else {
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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Byte> 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<Integer, Byte> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<QuarkusMultipartFormDataPart> parts = new ArrayList<>();
protected final List<QuarkusMultipartForm.PojoFieldData> 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<Byte> content, String mediaType) {
parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, false));
return this;
}

public ClientMultipartForm multiAsTextFileUpload(String name, String filename, Multi<Byte> content, String mediaType) {
parts.add(new QuarkusMultipartFormDataPart(name, filename, content, mediaType, true));
return this;
}

}
Loading

0 comments on commit 620aa9c

Please sign in to comment.