Multipart FileUpload – breaking changes between JAX-RS 1.1 and JAX-RS 2.0: now what?

For a project, we had to migrate from WebSphere 8.5 with Java EE 6 to Liberty with Java EE 7. With these kinds of migrations, you tend to run into some trouble, where things no longer work as you expect.

Now, as a part of Java EE 7, Liberty supports JAX-RS 2.0, whereas on WebSphere 8.5 with Java EE 6 only JAX-RS 1.1 is supported. This is where the trouble began since there are several behavior changes between JAX-RS 1.1 and JAX-RS 2.0.

The issue I ran into had to do with a Multipart FileUpload which no longer worked on Liberty with the new version of JAX-RS 2.0.

In this blog, I like to highlight this specific issue, and how I solved this.

Multipart FileUpload with Java EE 6 and JAX-RS 1.1

The application I work on has a REST service which is called from an Angular front-end and has some functionality with a Multipart FileUpload. With JAX-RS 1.1 this is pretty straightforward coding using the @FormParm annotation for each part, like:

@POST
@Consumes("multipart/form-data")
@Produces("multipart/form-data")
@Path("upload")
@RolesAllowed("maintainer")
public Response processUpload(
   @FormParam("identification") String identification,
   @FormParam("type") String type,
   @FormParam("user") String user,
   @FormParam("reason") String reason,
   @FormParam("file") File file) {

   // process the incoming parts

   return Response.ok(“upload processed”).build();
}

Multipart FileUpload with Java EE 7 and JAX-RS 2.0

In JAX-RS 2.0 there have been many API changes in handling a Multipart file. In JAX-RS 1.1 the @FormParam  could be used, in JAX-RS 2.0 only the @IMultipartBody  or @IAttachment  are available to handle a Multipart file.

Although the IBM knowledge center describes a resource method implementation for handling Multipart files with JAX-RS 2.0, it is no longer as straightforward and as clean of an implementation as it was with JAX-RS 1.1. With JAX-RS 2.0 we should now implement a resource method as follows (also see Configuring a resource to receive multipart/form-data parts from an HTML form submission in JAX-RS 2.0):

@POST
@Consumes("multipart/form-data")
@Produces("multipart/form-data")
@Path("upload")
@RolesAllowed("maintainer")
public Response processUpload (IMultipartBody multipartBody) throws IOException {
   List<IAttachment> attachments = multipartBody.getAllAttachments();
   String formElementValue = null;
   InputStream stream = null;
   for (Iterator<IAttachment> it = attachments.iterator(); it.hasNext(); ) {
      IAttachment attachment = it.next();
      if (attachment == null) {
         continue;
      }
      DataHandler dataHandler = attachment.getDataHandler();
      stream = dataHandler.getInputStream();
      MultivaluedMap<String, String> map = attachment.getHeaders();
      String fileName = null;
      String formElementName = null;
      String[] contentDisposition = map.getFirst("Content-Disposition").split(";");
      for (String tempName : contentDisposition) {
         String[] names = tempName.split("=");
         formElementName = names[1].trim().replaceAll("\"", "");
         if ((tempName.trim().startsWith("filename"))) {
            fileName = formElementName;
         }
      }
      if (fileName == null) {
         StringBuffer sb = new StringBuffer();
         BufferedReader br = new BufferedReader(new InputStreamReader(stream));
         String line = null;
         try {
            while ((line = br.readLine()) != null) {
               sb.append(line);
            }
         } catch (IOException e) {
            e.printStackTrace();
         } finally {
            if (br != null) {
               try {
                  br.close();
               } catch (IOException e) {
                  e.printStackTrace();
               }
            }
         }
         formElementValue = sb.toString();
         System.out.println(formElementName + ":" + formElementValue);
      } else {
         File tempFile = new File(fileName);
         // handle the file as you want...
      }
   }
   if (stream != null) {
      stream.close();
   }
   return Response.ok(“upload processed”).build();
}

Drawbacks of the new implementation with JAX-RS 2.0

Why am I not too happy with the IBM knowledge center’s solution, and what are the drawbacks of this new implementation?

  • First of all, this code looks quite nasty and is hard to maintain, let alone unit test.
  • Secondly, this implementation is vendor-specific (IBM/WebSphere).
  • Lastly, this implementation only works for JAX-RS 2.0, whereas the first solution only works for JAX-RS 1.1.

Especially the last point is a bit of a concern. Since we gradually migrate from WebSphere 8.5, to WebSphere 9.0, to Liberty, we need to support multiple environments for a certain period. Therefore we prefer a single solution for the Multipart FileUpload which works for all three.

In my search, I came across a blog from Jason Lee: File Uploads with JAX-RS 2. In his blog, he describes an alternative solution for implementing Multipart FileUpload. The advantage of his solution is that the implementation is not vendor-specific.

Surprisingly, it works for WebSphere 8.5 and WebSphere 9.0, as well as for Liberty.

This is because the solution uses the Servlet 3 specification, which provides an implementation-independent way of dealing with multipart requests. Namely the javax.servlet.http.Part , which is supported in WebSphere 8.5, WebSphere 9.0 and Liberty.

Let’s try it out

For the generic solution, I took the code from Jason’s blog and refactored it a bit. The main logic is embedded in a MultipartRequestMap (for more details see File Uploads with JAX-RS 2):

public class MultipartRequestMap extends HashMap<String, List<Object>> {

   private static final Logger LOGGER = LoggerFactory.getLogger(MultipartRequestMap.class);
   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) {
      try {
         this.tempLocation = tempLocation;
         this.encoding = request.getCharacterEncoding();
         setEncoding(request);
         processParts(request);
      } catch (IOException | ServletException exception) {
         LOGGER.error("Exception in constructing MultipartRequestMap", exception);
      }
   }

   private void setEncoding(HttpServletRequest request) {
      if (this.encoding == null) {
         try {
            request.setCharacterEncoding(DEFAULT_ENCODING);
            this.encoding = DEFAULT_ENCODING;
         } catch (UnsupportedEncodingException exception) {
            LOGGER.error("Exception in setting character encoding", exception);
         }
      }
   }

   private void processParts(HttpServletRequest request) 
      throws IOException, ServletException {
      for (Part part : request.getParts()) {
         String fileName = getFileName(part.getSubmittedFileName());
         if (fileName == null) {
            putMulti(part.getName(), getValue(part));
         } else {
            processFilePart(part, fileName);
         }
      }
   }

   private String getFileName(String submittedFileName) {
      return submittedFileName == null 
         ? null
         : Paths.get(submittedFileName).getFileName().toString();
   }

   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);
      if (list != null) {
         File file = (File) get(name).get(0);
         return file.isDirectory() ? null : file;
      }
      return null;
   }

   private void processFilePart(Part part, String fileName) throws IOException {
      File tempFile = new File(tempLocation, fileName);
      boolean fileCreated = tempFile.createNewFile();
      if (!fileCreated) {
         LOGGER.error("File already exists");
      }
      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 exception) {
         LOGGER.error("Exception in processing file part", exception);
      }
      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 = super.get(key);
      if (values == null) {
         values = new ArrayList<>();
         values.add(value);
         put(key, values);
      } else {
         values.add(value);
      }
   }

   @Override
   public boolean equals(Object o) {
      if (this == o) {
         return true;
      }
      if (o == null || getClass() != o.getClass()) {
         return false;
      }
      if (!super.equals(o)) {
         return false;
      }
      MultipartRequestMap that = (MultipartRequestMap) o;
      return encoding.equals(that.encoding) &&
            tempLocation.equals(that.tempLocation);
   }

   @Override
   public int hashCode() {
      return Objects.hash(super.hashCode(), encoding, tempLocation);
   }
}

Now the implementation of the resource method becomes fairly simple and clean again:
@POST
@Consumes("multipart/form-data")
@Produces("multipart/form-data")
@Path("upload")
@RolesAllowed("maintainer")
public Response processUpload (@Context HttpServletRequest request) {
   MultipartRequestMap map = new MultipartRequestMap(request);
   UploadParameters uploadParameters = UploadParameters.newBuilder()
         .withIdentification(map.getStringParameter("identification"))
         .withType(map.getStringParameter("type"))
         .withUser(map.getStringParameter("user"))
         .withReason(map.getStringParameter("reason"))
         .withFile(map.getFileParameter("file"))
         .build();

   // process the incoming parts and file

   return Response.ok(“upload processed”).build();
}

The method gets the HttpServletRequest  injected via the @Context  which we can then pass to the MultipartRequestMap . The MultipartRequestMap  constructor creates a map with all the parts from the request and through some convenience methods we then get the fields we need from the map for further processing.

All that’s left is to add the servlet and servlet-mapping details to your web.xml  Or use the correct annotations @ApplicationPath , @MultipartConfig , etc. if you want to do it without a deployment descriptor:

<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>
         <max-file-size>5000000</max-file-size>
         <max-request-size>20000000</max-request-size>
      </multipart-config>
   </servlet>
   <servlet-mapping>
      <servlet-name>javax.ws.rs.core.Application</servlet-name>
      <url-pattern>/rest/*</url-pattern>
   </servlet-mapping>
</web-app>

Isn’t there a more straightforward way?

Sure there is. As Jason also mentions in his blog, you can pass ‘real’ models as a method argument in the resource method, where all the magic is handled automatically. But you need the proper libraries for that, like Jersey, which are not always available by default.

The IBM JRE also often tends to have its own versions of a library, so including another external version often leads to conflicts, changes in classloading policies, etc. Which you want to avoid.

Besides, not every company allows you to include just any 3rd party library, which is the case for this project as well.

Conclusion

H/T to Jason Lee, his solution works in all of our three environments (WebSphere 8.5, WebSphere 9.0 and Liberty). It solves the breaking changes between JAX-RS 1.1 and JAX-RS 2.0 with regards to handling Multipart FileUpload. Without the abovementioned drawbacks.

Resources

JAX-RS 2.0 behavior changes
Multipart FileUpload
Configuring a resource to receive multipart/form-data parts from an HTML form submission in JAX-RS 2.0
Jason Lee: File Uploads with JAX-RS 2

 

Leave a Reply

Your email address will not be published.