Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Multipart @FormParam on EntityPart #41204

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.regex.PatternSyntaxException;

import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.PathSegment;

Expand Down Expand Up @@ -102,8 +103,10 @@ public class ServerEndpointIndexer
private static final DotName PATH_DOT_NAME = DotName.createSimple(Path.class.getName());
private static final DotName FILEUPLOAD_DOT_NAME = DotName.createSimple(FileUpload.class.getName());

private static final DotName ENTITY_PART_NAME = DotName.createSimple(EntityPart.class.getName());

private static final Set<DotName> SUPPORTED_MULTIPART_FILE_TYPES = Set.of(FILE_DOT_NAME, PATH_DOT_NAME,
FILEUPLOAD_DOT_NAME);
FILEUPLOAD_DOT_NAME, ENTITY_PART_NAME);
protected final EndpointInvokerFactory endpointInvokerFactory;
protected final List<MethodScanner> methodScanners;
protected final FieldInjectionIndexerExtension fieldInjectionHandler;
Expand Down Expand Up @@ -355,6 +358,7 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas
if (SUPPORTED_MULTIPART_FILE_TYPES.contains(DotName.createSimple(declaredType))) {
fileFormNames.add(name);
}

return new ServerMethodParameter(name,
elementType, declaredType, declaredTypes.getDeclaredUnresolvedType(),
type, single, signature,
Expand Down Expand Up @@ -572,7 +576,8 @@ private ParameterConverterSupplier extractConverter(String elementType, IndexVie
} else if (elementType.equals(FileUpload.class.getName())
|| elementType.equals(Path.class.getName())
|| elementType.equals(File.class.getName())
|| elementType.equals(InputStream.class.getName())) {
|| elementType.equals(InputStream.class.getName())
|| elementType.equals(EntityPart.class.getName())) {
// this is handled by MultipartFormParamExtractor
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;

import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotSupportedException;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.MessageBodyReader;

Expand All @@ -30,10 +31,7 @@
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.ServerSerialisers;
import org.jboss.resteasy.reactive.server.handlers.RequestDeserializeHandler;
import org.jboss.resteasy.reactive.server.multipart.FileItem;
import org.jboss.resteasy.reactive.server.multipart.FormValue;
import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
import org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException;
import org.jboss.resteasy.reactive.server.multipart.*;
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader;

/**
Expand Down Expand Up @@ -352,6 +350,30 @@ public static List<DefaultFileUpload> getFileUploads(String formName, ResteasyRe
return result;
}

public static EntityPart getEntityPart(String formName, ResteasyReactiveRequestContext context) {
List<EntityPart> parts = getEntityParts(formName, context);
if (!parts.isEmpty()) {
return parts.get(0);
}
return null;
}

public static List<EntityPart> getEntityParts(String formName, ResteasyReactiveRequestContext context) {
List<EntityPart> result = new ArrayList<>();
FormData formData = context.getFormData();
if (formData != null) {
Collection<FormValue> partsForName = formData.get(formName);
if (partsForName != null) {
for (FormValue partValue : partsForName) {
// FIXME: use a real implementation of EntityPart.
EntityPart part = new EntityPartImpl(partValue, formName);
result.add(part);
}
}
}
return result;
}

public static List<File> getJavaIOFileUploads(String formName, ResteasyReactiveRequestContext context) {
List<File> result = new ArrayList<>();
List<DefaultFileUpload> uploads = getFileUploads(formName, context);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jboss.resteasy.reactive.server.core.parameters;

import static org.jboss.resteasy.reactive.server.core.multipart.MultipartSupport.*;

import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.multipart.FileUpload;
Expand All @@ -24,7 +26,8 @@ public enum Type {
PartType,
String,
ByteArray,
InputStream;
InputStream,
EntityPart;
}

public MultipartFormParamExtractor(String name, boolean single, Type type, Class<Object> typeClass,
Expand Down Expand Up @@ -88,6 +91,12 @@ public Object extractParameter(ResteasyReactiveRequestContext context) {
} else {
return MultipartSupport.getJavaPathFileUploads(name, context);
}
case EntityPart:
if (single) {
return MultipartSupport.getEntityPart(name, context);
} else {
return MultipartSupport.getEntityParts(name, context);
}
default:
throw new RuntimeException("Unknown multipart parameter type: " + type + " for parameter " + name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.function.Supplier;

import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
Expand Down Expand Up @@ -665,6 +666,8 @@ public ParameterExtractor parameterExtractor(Map<String, Integer> pathParameterI
multiPartType = MultipartFormParamExtractor.Type.InputStream;
} else if (param.type.equals(byte[].class.getName())) {
multiPartType = MultipartFormParamExtractor.Type.ByteArray;
} else if (param.type.equals(EntityPart.class.getName())) {
multiPartType = MultipartFormParamExtractor.Type.EntityPart;
} else if (param.mimeType != null && !param.mimeType.equals(MediaType.TEXT_PLAIN)) {
multiPartType = MultipartFormParamExtractor.Type.PartType;
// TODO: special primitive handling?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.jboss.resteasy.reactive.server.multipart;

import java.io.*;
import java.nio.charset.Charset;
import java.util.Optional;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.*;

import org.jboss.resteasy.reactive.common.util.UnmodifiableMultivaluedMap;

public class EntityPartImpl implements EntityPart {
private final String name;
private final MultivaluedMap<String, String> headers;
private final MediaType mediaType;
private final String fileName;
private final InputStream content;

public EntityPartImpl(FormValue value, String controlName) {
this.name = controlName;
this.headers = new UnmodifiableMultivaluedMap(value.getHeaders());
this.mediaType = MediaType.valueOf(value.getHeaders().getFirst("Content-Type"));
this.fileName = value.getFileName();

try {
if (value.isFileItem()) {
this.content = new FileInputStream(value.getFileItem().getFile().toFile());
} else {
this.content = new ByteArrayInputStream(value.getValue().getBytes(Charset.defaultCharset()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what encoding the byte array should have, but I suspect not the default charset. First, because we prefer utf-8 everywhere, and second because it's possible that anybody reading the InputStream will use the part's encoding as set in headers or media type, which would be different to the default charset.

Also, perhaps the InputStream should be build lazily in getContent? Otherwise if a user closes it, doing two calls to getContent will fail.

}
} catch (IOException e) {
throw new MultipartPartReadingException(e);
}
}

@Override
public String getName() {
return this.name;
}

@Override
public Optional<String> getFileName() {
return Optional.ofNullable(this.fileName);
}

@Override
public InputStream getContent() {
return this.content;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably lazily build the InputStream here based on the value.

}

@Override
public <T> T getContent(Class<T> type)
throws IllegalArgumentException, IllegalStateException, IOException, WebApplicationException {
ollelogdahl marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

@Override
public <T> T getContent(GenericType<T> type)
throws IllegalArgumentException, IllegalStateException, IOException, WebApplicationException {
return null;
}

@Override
public MultivaluedMap<String, String> getHeaders() {
return this.headers;
}

@Override
public MediaType getMediaType() {
return this.mediaType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.*;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
Expand All @@ -48,6 +45,7 @@
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Multi;
import io.vertx.core.buffer.Buffer;
import org.jboss.resteasy.reactive.multipart.FileUpload;

@Path("")
public class MultipartResource {
Expand All @@ -63,6 +61,35 @@ public class MultipartResource {
@RestClient
MultipartChunksClient chunkClient;

@POST
@Path("/client/single-entity-part")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Blocking
public String singlePartDummy(@FormParam("part") EntityPart part) throws IOException {
String content = new String(part.getContent().readAllBytes());

if(!part.getFileName().orElse("").equals("file1.txt")) return "ERROR";
if(!"Hello, World!".equals(content)) return "ERROR";
return "OK";
}

@POST
@Path("/client/multiple-entity-parts")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Blocking
public String multiplePartsDummy(@FormParam("part") List<EntityPart> parts) throws IOException {
if(parts.size() != 2) return "ERROR";
if(!parts.get(0).getFileName().orElse("").equals("file1.txt")) return "ERROR";
if(!parts.get(1).getFileName().orElse("").equals("file2.txt")) return "ERROR";

String content1 = new String(parts.get(0).getContent().readAllBytes());
String content2 = new String(parts.get(1).getContent().readAllBytes());
if(!"Hello, World!".equals(content1)) return "ERROR";
if(!"Hello, World!".equals(content2)) return "ERROR";

return "OK";
}

@GET
@Path("/client/octet-stream")
@Produces(MediaType.TEXT_PLAIN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ public class MultipartResourceTest {
private static final String EXPECTED_CONTENT_DISPOSITION_PART = "Content-Disposition: form-data; name=\"%s\"";
private static final String EXPECTED_CONTENT_TYPE_PART = "Content-Type: %s";

@Test
public void shouldHandleSingleEntityPart() {
// @formatter:off
given()
.header("Content-Type", "multipart/form-data")
.multiPart("part", "file1.txt", "Hello, World!".getBytes())
.when().post("/client/single-entity-part")
.then()
.statusCode(200)
.body(equalTo("OK"));
// @formatter:on
}

@Test
public void shouldHandleMultipleEntityParts() {
// @formatter:off
given()
.header("Content-Type", "multipart/form-data")
.multiPart("part", "file1.txt", "Hello, World!".getBytes())
.multiPart("part", "file2.txt", "Hello, World!".getBytes())
.when().post("/client/multiple-entity-parts")
.then()
.statusCode(200)
.body(equalTo("OK"));
// @formatter:on
}

@Test
public void shouldSendByteArrayAsBinaryFile() {
// @formatter:off
Expand Down
Loading