jerseyjax-rsmedia-type

Consuming MULTIPART_FORM_DATA and application/x-www-form-urlencoded Media Types in a method of jersey servlet


I have a method in jersey servlet which consumes both MULTIPART_FORM_DATA and application/x-www-form-urlencoded Media Types, In my request I am sending some parameters along with a file in the file input stream.

Here is my method

@POST
@Path("/upload")
@Consumes({MediaType.MULTIPART_FORM_DATA,MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.TEXT_PLAIN)
public String uploadFile(MultivaluedMap<String,String> requestParamsPost,@FormDataParam("file") InputStream fis,
@FormDataParam("file") FormDataContentDisposition fdcd){

//some code goes here

}

But my problem is when I start my server after making the mapping of the servlet in web.xml, I get some severe exceptions

SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 0
SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 1
SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.package.ImportService.uploadFile(java.lang.String,java.lang.String,java.lang.String) at parameter at index 2

Is it somehow possible to consume two Media Types in one method at single endpoint? Sending a file Parameter is necessary in every request?


Solution

  • The reason for the error is the MultivaluedMap parameter. Jersey doesn't know what to do with it. You can only have one entity type per method. In your method you are trying to accept two different body types in the request. You can't do that. I don't even know how you plan on sending that from the client.

    The application/x-www-form-urlencoded data needs to be part of the multipart body. So you can do

    @Consumes({MediaType.MULTIPART_FORM_DATA})
    public String uploadFile(@FormDataParam("form-data") MultivaluedMap<String,String> form,
                             @FormDataParam("file") InputStream fis,
                             @FormDataParam("file") FormDataContentDisposition fdcd){
    

    That would work. The only thing is, you need to make sure the client set the Content-Type of the form-data part to application/x-www-form-urlencoded. If they don't, then the default Content-Type for that part will be text/plain and Jersey will not be able to parse it to a MultivaluedMap.

    What you can do instead is just use FormDataBodyPart as a method parameter, then explicitly set the media type. Then you can extract it to a MultivaluedMap. This way the client doesn't need to be expected to set the Content-Type for that part. Some clients don't even allow for setting individual part types.

    Here's an example using Jersey Test Framework

    import java.util.logging.Logger;
    import javax.ws.rs.Consumes;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    import javax.ws.rs.client.Entity;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.MultivaluedMap;
    import javax.ws.rs.core.Response;
    
    import org.glassfish.jersey.client.ClientConfig;
    import org.glassfish.jersey.filter.LoggingFilter;
    import org.glassfish.jersey.media.multipart.FormDataBodyPart;
    import org.glassfish.jersey.media.multipart.FormDataMultiPart;
    import org.glassfish.jersey.media.multipart.FormDataParam;
    import org.glassfish.jersey.media.multipart.MultiPartFeature;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.glassfish.jersey.test.JerseyTest;
    
    import org.junit.Test;
    import static junit.framework.Assert.assertEquals;
    
    public class MultipartTest extends JerseyTest {
    
        @Path("test")
        public static class MultiPartResource {
    
            @POST
            @Consumes(MediaType.MULTIPART_FORM_DATA)
            public Response post(@FormDataParam("form-data") FormDataBodyPart bodyPart, 
                                 @FormDataParam("data") String data) {
                bodyPart.setMediaType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
                MultivaluedMap<String, String> formData = bodyPart.getEntityAs(MultivaluedMap.class);
                StringBuilder sb = new StringBuilder();
                sb.append(data).append(";").append(formData.getFirst("key"));
    
                return Response.ok(sb.toString()).build();
            }
        }
    
        @Override
        public ResourceConfig configure() {
            return new ResourceConfig(MultiPartResource.class)
                    .register(MultiPartFeature.class)
                    .register(new LoggingFilter(Logger.getAnonymousLogger(), true));
        }
    
        @Override
        public void configureClient(ClientConfig config) {
            config.register(MultiPartFeature.class);
        }
    
        @Test
        public void doit() {
            FormDataMultiPart multiPart = new FormDataMultiPart();
            multiPart.field("data", "hello");
            multiPart.field("form-data", "key=world");
            final Response response = target("test")
                    .request().post(Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA));
            assertEquals("hello;world", response.readEntity(String.class));
        }
    }
    

    If you look at the logging, you will see the request as

    --Boundary_1_323823279_1458137333706
    Content-Type: text/plain
    Content-Disposition: form-data; name="data"
    
    hello
    --Boundary_1_323823279_1458137333706
    Content-Type: text/plain
    Content-Disposition: form-data; name="form-data"
    
    key=world
    --Boundary_1_323823279_1458137333706--
    

    You can see the Content-Type for the form-data body part is text/plain, which is the default, but in the server side, we explicitly set it before Jersey parses it

    public Response post(@FormDataParam("form-data") FormDataBodyPart bodyPart, 
                         @FormDataParam("data") String data) {
        bodyPart.setMediaType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
        MultivaluedMap<String, String> formData = bodyPart.getEntityAs(MultivaluedMap.class);