24

I have successfully developed a service, in which I read files uploaded in a multipart form in Jersey. Here's an extremely simplified version of what I've been doing:

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("file") InputStream uploadedInputStream,
        @FormDataParam("file") FormDataContentDisposition fileDetail) throws IOException {
    //handle the file
}

This works just fine but I've been given a new requirement. In addition to the file I'm uploading, I have to handle an arbitrary number of resources. Let's assume these are image files.

I figured I'd just provide the client with a form with one input for the file, one input for the first image and a button to allow adding more inputs to the form (using AJAX or simply plain JavaScript).

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

So the user can append the form with more inputs for images, like this:

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

I hoped it would be simple enough to read the fields with the same name as a collection. I've done it successfully with text inputs in MVC .NET and I thought it wouldn't be harder in Jersey. It turns out I was wrong.

Having found no tutorials on the subject, I started experimenting.

In order to see how to do it, I dumbed the problem down to simple text inputs.

<form action="blahblabhblah" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="submit" value="Upload It" />
   </fieldset>
</form>

Obviously, I needed to have some sort of collection as a parameter to my method. Here's what I tried, grouped by collection type.

Array

At first, I checked whether Jersey was smart enough to handle a simple array:

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("test") String[] inputs) {
    //handle the request
}

but the array wasn't injected as expected.

MultiValuedMap

Having failed miserably, I remembered that MultiValuedMap objects could be handled out of the box.

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(MultiValuedMap<String, String> formData) {
    //handle the request
}

but it doesn't work either. This time, I got an exception

SEVERE: A message body reader for Java class javax.ws.rs.core.MultivaluedMap, 
and Java type javax.ws.rs.core.MultivaluedMap<java.lang.String, java.lang.String>, 
and MIME media type multipart/form-data; 
boundary=----WebKitFormBoundaryxgxeXiWk62fcLALU was not found.

I was told that this exception could be gotten rid of by including the mimepull library so I added the following dependency to my pom:

    <dependency>
        <groupId>org.jvnet</groupId>
        <artifactId>mimepull</artifactId>
        <version>1.3</version>
    </dependency>

Unfortunately the problem persists. It's probably a matter of choosing the right body reader and using different parameters for the generic. I'm not sure how to do this. I want to consume both file and text inputs, as well as some others (mostly Long values and custom parameter classes).

FormDataMultipart

After some more research, I found the FormDataMultiPart class. I've successfully used it to extract the string values from my form

@POST
@Path("upload2")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart){
    List<FormDataBodyPart> fields = multiPart.getFields("test");
    System.out.println("Name\tValue");
    for(FormDataBodyPart field : fields){
        System.out.println(field.getName() + "\t" + field.getValue());
        //handle the values
    }
    //prepare the response
}

The problem is, this is a solution to the simplified version of my problem. While I know that every single parameter injected by Jersey is created by parsing a string at some point (no wonder, it's HTTP after all) and I have some experience writing my own parameter classes, I don't really how to convert these fields to InputStream or File instances for further processing.

Therefore, before diving into Jersey source code to see how these objects are created, I decided to ask here whether there is an easier way to read a set (of unknown size) of files. Do you know how to solve this conundrum?

toniedzwiedz
  • 17,895
  • 9
  • 86
  • 131

3 Answers3

32

I have found the solution by following the example with FormDataMultipart. It turns out I was very close to the answer.

The FormDataBodyPart class provides a method that allows its user to read the value as InputStream (or theoretically, any other class, for which a message body reader is present).

Here's the final solution:

Form

The form remains unchanged. I have a couple of fields with the same name, in which I can place files. It's possible to use both multiple form inputs (you want these when uploading many files from a directory) and numerous inputs that share a name (Flexible way to upload an unspecified number of files from different location). It's also possible to append the form with more inputs using JavaScript.

<form action="/files" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="file" name="test" multiple="multiple"/>
       <input type="file" name="test" />
       <input type="file" name="test" />
   </fieldset>
   <input type="submit" value="Upload It" />
</form>

Service - using FormDataMultipart

Here's a simplified method that reads a collection of files from a multipart form. All inputs with the same are assigned to a List and their values are converted to InputStream using the getValueAs method of FormDataBodyPart. Once you have these files as InputStream instances, it's easy to do almost anything with them.

@POST
@Path("files")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart) throws IOException{        
    List<FormDataBodyPart> fields = multiPart.getFields("test");        
    for(FormDataBodyPart field : fields){
        handleInputStream(field.getValueAs(InputStream.class));
    }
    //prepare the response
}

private void handleInputStream(InputStream is){
    //read the stream any way you want
}
toniedzwiedz
  • 17,895
  • 9
  • 86
  • 131
  • 1
    This last piece REALLY REALLY helped me implement a proxy service that takes the multiPart parameter and submits it to another server using Jersey's client: – Marcello DeSales Jan 23 '14 at 08:26
  • Thanks @Tom!!! Specially for the case of a Proxy, which forwards all the incoming calls to another server using the same Jersey client.... https://gist.github.com/marcellodesales/e0832b08c46729560049 – Marcello DeSales Jan 24 '14 at 17:39
  • 1
    I get `java.lang.OutOfMemoryError: Java heap space` with this code when uploading ~50mb file. Can you tell me how to read large file efficiently? – gkiko Jun 30 '15 at 09:23
  • @gkiko I haven't tried the code above with files this big. It's hard to say what the problem is without seeing how you're handling the input streams. I think you should ask a separate question here on SO with a short example of what exactly you're doing and outlining the problem. – toniedzwiedz Jun 30 '15 at 17:46
  • @toniedzwiedz thanks for your reply. here is my problem http://stackoverflow.com/q/31138727/660408 – gkiko Jun 30 '15 at 22:51
  • 1
    and how do you get FormDataContentDisposition objects? – Vadym Kovalenko May 18 '17 at 13:59
  • 1
    multiPart object happens to be null, while testing using Postman. – Kuldeep Yadav Jun 21 '20 at 16:18
9
@Path("/upload/multiples")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadImage(@FormDataParam("image") List<FormDataBodyPart> imageDatas){
   for( FormDataBodyPart imageData : imageDatas ){
       // Your actual code.
       imageData.getValueAs(InputStream.class);
    }
}
Amit Sharma
  • 281
  • 5
  • 8
3

If anyone is trying to do generic type=text input boxes with the same name attribute like I was, you will be able to switch them to type=hidden inputs and have that work as a @FormParam("inputName") List<String> nameList in your route.

Obviously, when switching to hidden inputs the only point is to still send the data to the server without creating a UI element for it, so you will need to switch to an alternate display UI (for example, I used a button element for easy click-to-remove functionality).

Blaskovicz
  • 6,122
  • 7
  • 41
  • 50