File Uploads with JAX-RS 2 - No Fluff Just Stuff

File Uploads with JAX-RS 2

Posted by: Jason Lee on May 1, 2014

If you search for how to upload a file to a JAX-RS 2 endpoint, most suggestions will point you to implementation-specific approaches. While that works, it defeats one of the purposes of a spec: portability. There are some posts out there that will point you in the right direction, though. What I’ll do here, then, is present a clear, portable solution to the problem.

pass::[more]

In this example, we’re going to upload arbitrary, binary data. Let’s think of this in HTML terms: we have a form on a page that has a number of text input fields, and at least one file field. In this example, we’ll use two fields: name, and attachment. A Java model (which will become more important later), might look like this:

public class Example {
          private String name;
          private byte[] attachment;
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public byte[] getAttachment() {
              return attachment;
          }
      
          public void setAttachment(byte[] attachment) {
              this.attachment = attachment;
          }
      }

One way of getting the data passed to a JAX-RS resource would be to use @FormParam. Normally, this will work fine, but since we’re wanting a file to be part of the payload, the request must be of type multipart/form-data, which @FormParam doesn’t seem to like. Fortunately, the Servlet 3 spec provides an implementation-independent way of dealing with multipart requests: javax.servlet.http.Part, which we’ll use here. First, the resource method itself:

    @POST
          @Consumes(MediaType.MULTIPART_FORM_DATA)
          public Response formPost(@Context HttpServletRequest request) {
              MultipartRequestMap map = new MultipartRequestMap(request);
              Example example = new Example();
              example.setName(map.getStringParameter("name"));
              example.setAttachment(readFile(map.getFileParameter("attachment")));
      
              return Response.ok(buildMessage(example.getName(), example.getAttachment().length)).build();
          }

Before looking at where things actually get done, just a quick note here. We are asking the JAX-RS runtime to inject the HttpServletRequest, which we pass to MultipartRequestMap (see below). We then pull the fields we want from our Map, build a model object that we don’t do much with, then return a simple String response to show that we did something. Pretty simple.

And now, the details:

public class MultipartRequestMap extends HashMap<String, List<Object>> {
      
          private static final String DEFAULT_ENCODING = "UTF-8";
          private String encoding;
          private String tempLocation;
      
          public MultipartRequestMap(HttpServletRequest request) {
              this(request, System.getProperty("java.io.tmpdir"));
          }
      
          public MultipartRequestMap(HttpServletRequest request, String tempLocation) {
              super();
              try {
                  this.tempLocation = tempLocation;
      
                  this.encoding = request.getCharacterEncoding();
                  if (this.encoding == null) {
                      try {
                          request.setCharacterEncoding(this.encoding = DEFAULT_ENCODING);
                      } catch (UnsupportedEncodingException ex) {
                          Logger.getLogger(MultipartRequestMap.class.getName()).log(Level.SEVERE, null, ex);
                      }
                  }
      
                  for (Part part : request.getParts()) {
                      String fileName = part.getSubmittedFileName();
                      if (fileName == null) {
                          putMulti(part.getName(), getValue(part));
                      } else {
                          processFilePart(part, fileName);
                      }
                  }
              } catch (IOException | ServletException ex) {
                  Logger.getLogger(MultipartRequestMap.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
      
          public String getStringParameter(String name) {
              List<Object> list = get(name);
              return (list != null) ? (String) get(name).get(0) : null;
          }
      
          public File getFileParameter(String name) {
              List<Object> list = get(name);
              return (list != null) ? (File) get(name).get(0) : null;
          }
      
          private void processFilePart(Part part, String fileName) throws IOException {
              File tempFile = new File(tempLocation, fileName);
              tempFile.createNewFile();
              tempFile.deleteOnExit();
      
              try (BufferedInputStream input = new BufferedInputStream(part.getInputStream(), 8192);
                      BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(tempFile), 8192);) {
      
                  byte[] buffer = new byte[8192];
                  for (int length = 0; ((length = input.read(buffer)) > 0);) {
                      output.write(buffer, 0, length);
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
              part.delete();
              putMulti(part.getName(), tempFile);
          }
      
          private String getValue(Part part) throws IOException {
              BufferedReader reader
                      = new BufferedReader(new InputStreamReader(part.getInputStream(), encoding));
              StringBuilder value = new StringBuilder();
              char[] buffer = new char[8192];
              for (int length; (length = reader.read(buffer)) > 0;) {
                  value.append(buffer, 0, length);
              }
              return value.toString();
          }
      
          private <T> void putMulti(final String key, final T value) {
              List<Object> values = (List<Object>) super.get(key);
      
              if (values == null) {
                  values = new ArrayList<>();
                  values.add(value);
                  put(key, values);
              } else {
                  values.add(value);
              }
          }
      }

This class is based on one by BalusC, though I’ve simplified it some (e.g., removing any EL concerns), so his very well may be more robust. This works well enough, though, for demonstration purposes.

The most interesting part (no pun intended :) is in this loop: for (Part part : request.getParts()) {. In a nutshell, we’re looping though each Part returned by the server. If the Part has a file name, we assume (!!!) it’s a binary part, so we handle it accordingly. Otherwise, we’ll store the value as a simple String. Note that a key might be given more than once in a request, so we store the values for each key in a List. This Map implementation, though, provides convenience methods to get the first value in the List, which is what we’re interested in. If you’re curious about how the binary data is read off the request, look at processFilePart.

If you deploy the application now, you’ll get an error at runtime because you need to configure multipart support. It’s a bit obnoxious that there aren’t sensible defaults, but that’s the way it is. In this example, we don’t have any other configuration requirements, we’ll just use the JAX-RS standard application:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
               version="3.1">
          <servlet>
              <servlet-name>javax.ws.rs.core.Application</servlet-name>
              <multipart-config>
                  <location>/tmp</location>
                  <max-file-size>35000000</max-file-size>
                  <max-request-size>218018841</max-request-size>
                  <file-size-threshold>0</file-size-threshold>
              </multipart-config>
          </servlet>
          <servlet-mapping>
              <servlet-name>javax.ws.rs.core.Application</servlet-name>
              <url-pattern>/*</url-pattern>
          </servlet-mapping>
      </web-app>

The area of interest is the <multipart-config> element. Feel free to tweak the values as you see fit. It might be possible to use annotations (e.g., @ApplicationPath, @MultipartConfig, etc) to register all of this without the deployment descriptor, but I haven’t figured out the correct incantation yet, so I use web.xml. :)

We’re now ready to deploy and test, which we’ll do using curl:

$ curl -X POST -H 'Accept: application/json' \
          -F 'name=Form Upload Example' \
          -F 'attachment=@src/main/resources/java.jpg' \
          http://localhost:8080/upload-1.0-SNAPSHOT/upload
      You uploaded an Example named 'Form Upload Example' with an attachment that is 9425 bytes long.

And there it is! POSTing a binary file to a JAX-RS resource. As I mentioned earlier, there is another, perhaps better way. If you’re using "real" models, there’s no extra magic required:

    @POST
          @Consumes(MediaType.APPLICATION_JSON)
          public Response jsonPost(Example example) {
              return Response.ok(buildMessage(example.getName(), example.getAttachment().length)).build();
          }

which can be called with:

curl -X POST -H 'Content-type: application/json' \
          -H 'Accept: application/json' \
          -d '{"attachment":"binary data here","name":"JSON Example"}' \
          http://localhost:8080/upload-1.0-SNAPSHOT/upload

For this method, JAX-RS (possibly Jersey. I haven’t tested that.) unmarshalls the JSON for us, building the Example instance, and calling the resource method. It’s much easier and cleaner, so if you can go that route, I’d certainly recommend it, but that’s not always possible. Now, though, you should be equipped to do it either way.

Jason Lee

About Jason Lee

Jason Lee is a Senior Java Developer for Sun Microsystems working on the GlassFish Administration Console, and is a member of the JSF 2.0 (JSR 314) Expert Group. Jason has extensive experience working with web-based technologies such as JavaServer Faces and Ajax, as well as enterprise technologies based on the GlassFish platform. He is currently the main developer of Mojarra Scales, working to create a set of high quality JSF components wrapping libraries such as the Yahoo! User Interface Library, as well as bring Facelets compatibility to JSFTemplating.

Jason has been writing software professionally since 1997 in a wide variety of languages and environments, including Java, PHP, C/C++, and Delphi on both Linux/Unix and Windows. You can read more about what Jason's working on at his blog at http://blogs.steeplesoft.com

Apart from work, he is currently serving as the president of the Oklahoma City Java Users Group, where he is an active member and presenter. More importantly, Jason is married to a beautiful woman and has two sons who, thankfully, look like their mother.

Why Attend the NFJS Tour?

  • » Cutting-Edge Technologies
  • » Agile Practices
  • » Peer Exchange

Current Topics:

  • Languages on the JVM: Scala, Groovy, Clojure
  • Enterprise Java
  • Core Java, Java 8
  • Agility
  • Testing: Geb, Spock, Easyb
  • REST
  • NoSQL: MongoDB, Cassandra
  • Hadoop
  • Spring 4
  • Cloud
  • Automation Tools: Gradle, Git, Jenkins, Sonar
  • HTML5, CSS3, AngularJS, jQuery, Usability
  • Mobile Apps - iPhone and Android
  • More...
Learn More »