Sunday, December 23, 2012

Uploading Binary Files the Fun Way

Here’s the problem: suppose you have a domain object (say, an item) which can be created restfully by posting json data in a request body. Spring can automagically convert request bodies in json to Java objects on the server, which is incredibly convenient. It’s also an incredible pain in the butt if you have a new requirement to upload the contents of a binary file as part of that json object (say, an item image to be stored on the server and displayed later).

Fortunately, there’s a fun way to include binary file content in your json object!

Here are the Java objects we’ll be dealing with. Note that the Image class has a byte[]: this will be coming from an image file from the browser, stored in the DB, and streamed back as needed upon request.

@Entity
public class Item {

  @OneToOne
  private Image image = null;

  ...
}

@Entity
public class Image {

  @Lob
  private byte[] bytes = null;

  @Basic
  private String contentType = null;

  ...
}


Next, on the browser we can make use of the file API to load a file, then encode its contents using Base64 encoding, and finally assign the encoded contents to our javascript object before posting it.

function put() {

  // get a file object, we only need one
  var file = $('input[type="file"]').get(0).files[0];

  // obtain the object you want to post as a json object
  // in the body of a POST
  var item = ...

  if(file) {

  var reader = new FileReader();

  // after the file has loaded on the client,
  // convert to base64 and assign to the item before POSTing
reader.onload = function loaded(evt) {
       item.image={};
       item.image.bytes = arrayBufferToBase64(evt.target.result);
       item.image.contentType = file.type;

       // post json object as the request body
       // with the technique of your choice
       // I like using $.ajax(...)
       create(item);
  };

  reader.readAsArrayBuffer(file);
  }
  else {
  create(item);
  }
}

// pass in an HTML5 ArrayBuffer, returns a base64 encoded string
function arrayBufferToBase64( arrayBuffer ) {
  var bytes = new Uint8Array( arrayBuffer );
  var len = bytes.byteLength;
  var binary = '';
  for (var i = 0; i < len; i++) {
  binary += String.fromCharCode( bytes[ i ] );
  }
  return window.btoa( binary );
}


Now on the server, we have a Spring Controller which will handle the request. As part of the json unmarshalling that converts the the request body to an Item, the base64-encoded value in image.item.bytes will be converted to a standard Java byte[], and stored as a LOB in the database.

@RequestMapping(value = BASE_URL + "/item", method = RequestMethod.POST)
public @ResponseBody long createItem(@RequestBody Item incoming)
{
  // authorization and input scrubbing removed for brevity

  service.createItem(incomingItem); // sets the itemId
  long newItemId = incoming.getId();
  return newItemId;
}


To retrieve the image, another Spring Controller method can provide the image by streaming the bytes directly.

@RequestMapping(value=BASE_URL + "/item/{id}/image", method=RequestMethod.GET)
public void getItemImage(@PathVariable Long id, HttpServletResponse response) throws IOException
{
  Item item = service.loadItem(id);
  response.setContentType(item.getImage().getContentType());
  response.getOutputStream().write(item.getImage().getBytes());

  // don't close the output stream from the response
}


Now if you need to retrieve the image, you can simply reference the item’s id in the appropriate URL in an image tag: <img src="... /item/image/1/image" />

Beautiful!

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete

  2. For those who may read this post in 2018 and try to do it, there is a readAsDataURL method on FileReader object which gives base64 encoded value of the image directly. You can use it instead of arrayBufferToBase64.

    ReplyDelete
  3. This Blogspot blog has since been migrated to Wordpress, the corresponding article is here: https://thoughtfulsoftware.wordpress.com/2012/12/24/uploading-binary-files-the-fun-way/

    but yes it is an older article... thanks for the update!

    ReplyDelete