Skip to content

Commit

Permalink
Merge pull request #4091 from jamezp/RESTEASY-3463
Browse files Browse the repository at this point in the history
[RESTEASY-3463] Create an API for getting the EntityPart's from the m…
  • Loading branch information
jamezp committed Mar 21, 2024
2 parents 1c650cc + 53a343f commit b95cfe0
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 140 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.plugins.providers.multipart;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;

import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.EntityPart;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.ext.Providers;

import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.plugins.server.Cleanables;
import org.jboss.resteasy.spi.EntityOutputStream;
import org.jboss.resteasy.spi.multipart.MultipartContent;
import org.jboss.resteasy.spi.util.Types;

/**
* Checks the method found to see if it as a {@link FormParam} or {@link org.jboss.resteasy.annotations.jaxrs.FormParam}
* annotation on an {@link EntityPart} or {@link List List<EntityPart>} parameter. If so, the multipart parts are
* parsed and a {@link MultipartContent} is created and placed on the context to be used in other readers and parameter
* injector.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
* @since 6.2.8.Final
*/
@Provider
public class EntityPartFilter implements ContainerRequestFilter {

@Context
private Providers providers;

@Context
private ResourceInfo resourceInfo;

@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
final MediaType mediaType = requestContext.getMediaType();
// Ensure the media type is multipart/form-data and a MultipartContent type is not already in our context
if (MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mediaType)
&& !ResteasyContext.hasContextData(MultipartContent.class)) {
final String boundary = mediaType.getParameters().get("boundary");
// If we don't have a boundary, we will just skip the processing
if (boundary != null) {
// Get the methods and check that we need to get the entity parts
final Method method = resourceInfo.getResourceMethod();
final Parameter[] parameters = method.getParameters();
if (hasEntityPartParameter(parameters)) {
final MultipartFormDataInputImpl input = new MultipartFormDataInputImpl(requestContext.getMediaType(),
providers);
// Copy the input stream as it's being parsed. This will allow us to reset the entity stream for
// further reads.
final CopyInputStream copyInputStream = new CopyInputStream(requestContext.getEntityStream());
input.parse(copyInputStream);
// Set the entity stream to the copied content for cases where another read might be required
requestContext.setEntityStream(copyInputStream.entity.toInputStream());
final List<EntityPart> parts = List.copyOf(input.toEntityParts());
final MultipartContent multipartParts = () -> parts;
ResteasyContext.pushContext(MultipartContent.class, multipartParts);
final Cleanables cleanables = ResteasyContext.getContextData(Cleanables.class);
if (cleanables != null) {
cleanables.addCleanable(() -> ResteasyContext.popContextData(MultipartContent.class));
}
}
}
}
}

private static boolean hasEntityPartParameter(final Parameter[] parameters) {
for (Parameter parameter : parameters) {
if (parameter.isAnnotationPresent(FormParam.class)
|| parameter.isAnnotationPresent(org.jboss.resteasy.annotations.jaxrs.FormParam.class)) {
if (parameter.getType().isAssignableFrom(EntityPart.class)) {
return true;
} else if (parameter.getType().isAssignableFrom(List.class)
&& Types.isGenericTypeInstanceOf(EntityPart.class, parameter.getParameterizedType())) {
return true;
}
}
}
return false;
}

private static class CopyInputStream extends InputStream {
private final InputStream delegate;
private final EntityOutputStream entity;

private CopyInputStream(final InputStream delegate) {
this.delegate = delegate;
entity = new EntityOutputStream();
}

@Override
public int read(final byte[] b) throws IOException {
final int read = delegate.read(b);
write(b, 0, read);
return read;
}

@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
final int read = delegate.read(b, off, len);
write(b, off, read);
return read;
}

@Override
public byte[] readAllBytes() throws IOException {
final byte[] read = delegate.readAllBytes();
if (read.length > 0) {
entity.write(read);
}
return read;
}

@Override
public byte[] readNBytes(final int len) throws IOException {
final byte[] read = delegate.readNBytes(len);
if (read.length > 0) {
entity.write(read);
}
return read;
}

@Override
public int readNBytes(final byte[] b, final int off, final int len) throws IOException {
final int read = delegate.readNBytes(b, off, len);
write(b, off, read);
return read;
}

@Override
public long skip(final long n) throws IOException {
return delegate.skip(n);
}

@Override
public void skipNBytes(final long n) throws IOException {
delegate.skipNBytes(n);
}

@Override
public int available() throws IOException {
return delegate.available();
}

@Override
public void close() throws IOException {
try {
delegate.close();
} finally {
entity.close();
}
}

@Override
public void mark(final int readlimit) {
delegate.mark(readlimit);
}

@Override
public void reset() throws IOException {
delegate.reset();
}

@Override
public boolean markSupported() {
return delegate.markSupported();
}

@Override
public long transferTo(final OutputStream out) throws IOException {
return delegate.transferTo(out);
}

@Override
public int read() throws IOException {
final int read = delegate.read();
if (read != -1) {
entity.write(read);
}
return read;
}

private void write(final byte[] b, final int off, final int read) throws IOException {
if (read > 0) {
final int writeLen = (read - off);
if (writeLen > 0) {
entity.write(b, off, writeLen);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.plugins.providers.multipart.i18n.Messages;
import org.jboss.resteasy.spi.multipart.MultipartContent;
import org.jboss.resteasy.spi.util.Types;

/**
Expand Down Expand Up @@ -68,6 +69,13 @@ public List<EntityPart> readFrom(final Class<List<EntityPart>> type, final Type
final String boundary = mediaType.getParameters().get("boundary");
if (boundary == null)
throw new IOException(Messages.MESSAGES.unableToGetBoundary());

// Check if we've already parsed the entity parts
final MultipartContent multipartContent = ResteasyContext.getContextData(MultipartContent.class);
if (multipartContent != null) {
return multipartContent.entityParts();
}

// On the returned EntityPart an injected (@Context Providers) doesn't work as it can't be found when
// constructing this type. Therefore, the lookup here is required.
final Providers providers = ResteasyContext.getRequiredContextData(Providers.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ org.jboss.resteasy.plugins.providers.multipart.MultipartFormAnnotationWriter
org.jboss.resteasy.plugins.providers.multipart.MimeMultipartProvider
org.jboss.resteasy.plugins.providers.multipart.XopWithMultipartRelatedReader
org.jboss.resteasy.plugins.providers.multipart.XopWithMultipartRelatedWriter
org.jboss.resteasy.plugins.providers.multipart.EntityPartFilter
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
Expand Down Expand Up @@ -409,49 +408,6 @@ public void multiAllFilesInjection() throws Exception {
}
}

/**
* Tests sending {@code multipart/form-data} content as a {@link EntityPart List<EntityPart>}. One part is sent
* and injected as {@link FormParam @FormParam} method parameters. The same part should be injected in different
* formats; {@link String}, {@link EntityPart} and {@link InputStream}. The content should be the same for each
* injected parameter.
* <p>
* The result from the REST endpoint is {@code multipart/form-data} content with a new name and the content for the
* injected field.
* </p>
*
* @throws Exception if an error occurs in the test
*/
@Test
public void injection() throws Exception {
try (Client client = ClientBuilder.newClient()) {
final List<EntityPart> multipart = List.of(
EntityPart.withName("content")
.content("test content")
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.build());
try (
Response response = client.target(INSTANCE.configuration().baseUriBuilder().path("test/injected"))
.request(MediaType.MULTIPART_FORM_DATA_TYPE)
.post(Entity.entity(new GenericEntity<>(multipart) {
}, MediaType.MULTIPART_FORM_DATA))) {
Assertions.assertEquals(Response.Status.OK, response.getStatusInfo());
final List<EntityPart> entityParts = response.readEntity(new GenericType<>() {
});
if (entityParts.size() != 3) {
final String msg = "Expected 3 entries got " +
entityParts.size() +
'.' +
System.lineSeparator() +
getMessage(entityParts);
Assertions.fail(msg);
}
checkEntity(entityParts, "received-entity-part", "test content");
checkEntity(entityParts, "received-string", "test content");
checkEntity(entityParts, "received-input-stream", "test content");
}
}
}

/**
* Tests sending {@code multipart/form-data} content as a {@link EntityPart List<EntityPart>}. Three parts are sent
* and processed by the resource returning all the headers from the request.
Expand Down Expand Up @@ -693,7 +649,6 @@ private static EntityPart find(final List<EntityPart> parts, final String name)
}

@ApplicationPath("/")
@MultipartConfig
public static class TestApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Expand Down Expand Up @@ -747,29 +702,6 @@ public Response multipleInjectable(@FormParam("string-part") final String string
}, MediaType.MULTIPART_FORM_DATA).build();
}

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/injected")
public List<EntityPart> injected(@FormParam("content") final String string,
@FormParam("content") final EntityPart entityPart,
@FormParam("content") final InputStream in) throws IOException {
return List.of(
EntityPart.withName("received-entity-part")
.content(entityPart.getContent(String.class))
.mediaType(entityPart.getMediaType())
.fileName(entityPart.getFileName().orElse(null))
.build(),
EntityPart.withName("received-input-stream")
.content(MultipartEntityPartProviderTest.toString(in).getBytes(StandardCharsets.UTF_8))
.mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE)
.build(),
EntityPart.withName("received-string")
.content(string)
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.build());
}

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.MULTIPART_FORM_DATA)
Expand Down

0 comments on commit b95cfe0

Please sign in to comment.