Blobstore Tutorial: A GWT application for storing and serving images using the GAE Blobstore

The Google App Engine Blobstore Service is a valuable service that allows you to upload and store files such as pictures, spreadsheets, and other documents in the Cloud. For example, a picture gallery web application could allow users to upload and share their favorite pictures.

The Blobstore is certainly valuable, but it is equally complicated and difficult to use. In this tutorial, I will attempt to unlock the potential of the Blobstore by creating a GWT application that allows users to upload an image and meta-data describing the image and serve the image and meta-data back to the user.

This example uses Objectify to store the image meta-data as an entity in the datastore.  Configuring your project to use Objectify is easy, just make sure to do the following:

  • Inherit Objectify in your <project>.gwt.xml file
  • Add the Objectify JAR to your war/WEB-INF/lib directory
  • Add the Objectify JAR as a reference library to your Eclipse project
For more information on using Objectify see the "Use Objectify to store data in the Google App Engine Datastore" tutorial.

I drew up the following diagram to give an overview of a typical project that uses the Blobstore to store and serve blobs.



There are a lot of moving parts in this example, but a typical flow would be something like this.

A user in the Web Client (1) wishes to upload a picture, so an RPC call is made through the Interfaces (3) to the Blob Service (4) to start a Blobstore session and get a Blobstore upload URL.  This upload URL is returned to the client and applied to the FormPanel (2) and will be called when the FormPanel is submitted.

Submitting the FormPanel (which includes a FileUpload widget) makes an HTTP POST call to the Upload Service (6), which uploads the Blob to the Blobstore (7).  In the doPost method of the Upload Service, an Entity (8) is instantiated and put in the Datastore (5) so that some meta-data about the picture can be stored.

The Upload Service returns a unique identifier for the picture's meta-data to the Web Client and an RPC call is made through the Interfaces and the Blob Service to retrieve the Entity object from the Datastore.  Wrapped in the Entity is an ImageURL which will be placed in an Image widget and displayed to the user.  The ImageURL also points to the Blob Service and calls the doGet method to serve the Image.

Web Client Class

In the Web Client, we have a FormPanel, which contains a couple of text boxes, a FileUpload widget, a submit button, and a FlexTable.

The FormPanel and FileUpload widgets are required for using the Blobstore and they provide you the ability to make an HTTP POST call and browse for a file.

The submit button will Submit the form and the FlexTable will be used to serve the image and it's meta-data back to the user.


package com.example.myproject.client;

import com.example.myproject.client.entities.Picture;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FileUpload;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteEvent;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;

public class BlobstoreExample implements EntryPoint {

  // You must use a FormPanel to create a blobstore upload form
  final FormPanel uploadForm = new FormPanel();

  // Use an RPC call to the Blob Service to get the blobstore upload url
  BlobServiceAsync blobService = GWT.create(BlobService.class);

  VerticalPanel mainVerticalPanel = new VerticalPanel();
  HorizontalPanel hp1 = new HorizontalPanel();
  HorizontalPanel hp2 = new HorizontalPanel();
  HTML titleLabel = new HTML("Title");
  HTML descriptionLabel = new HTML("Description");
  TextBox titleTextBox = new TextBox();
  TextBox descriptionTextBox = new TextBox();
  FileUpload upload = new FileUpload();
  Button submitButton = new Button("Submit");

  FlexTable resultsTable = new FlexTable();

  @Override
  public void onModuleLoad() {

    hp1.add(titleLabel);
    hp1.add(titleTextBox);
    hp2.add(descriptionLabel);
    hp2.add(descriptionTextBox);

    mainVerticalPanel.add(hp1);
    mainVerticalPanel.add(hp2);
    mainVerticalPanel.add(upload);

    mainVerticalPanel.add(submitButton);
    mainVerticalPanel.add(resultsTable);

    hp1.setSpacing(5);
    hp2.setSpacing(5);
    mainVerticalPanel.setSpacing(5);

    uploadForm.setWidget(mainVerticalPanel);

    // The upload form, when submitted, will trigger an HTTP call to the
    // servlet.  The following parameters must be set
    uploadForm.setEncoding(FormPanel.ENCODING_MULTIPART);
    uploadForm.setMethod(FormPanel.METHOD_POST);

    // Set Names for the text boxes so that they can be retrieved from the
    // HTTP call as parameters
    titleTextBox.setName("titleTextBox");
    descriptionTextBox.setName("descriptionTextBox");
    upload.setName("upload");
    
    RootPanel.get("container").add(uploadForm);

    submitButton.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {

        blobService
            .getBlobStoreUploadUrl(new AsyncCallback<String>() {

              @Override
              public void onSuccess(String result) {
                // Set the form action to the newly created
                // blobstore upload URL
                uploadForm.setAction(result.toString());

                // Submit the form to complete the upload
                uploadForm.submit();
                uploadForm.reset();
              }

              @Override
              public void onFailure(Throwable caught) {
                caught.printStackTrace();
              }
            });

      }
    });

    uploadForm
        .addSubmitCompleteHandler(new FormPanel.SubmitCompleteHandler() {
          @Override
          public void onSubmitComplete(SubmitCompleteEvent event) {
            
            //The submit complete Event Results will contain the unique
            //identifier for the picture's meta-data.  Trim it to remove
            //trailing spaces and line breaks
            getPicture(event.getResults().trim());

          }

        });

  }

  public void getPicture(String id) {

    //Make another call to the Blob Service to retrieve the meta-data
    blobService.getPicture(id, new AsyncCallback<Picture>() {

      @Override
      public void onSuccess(Picture result) {
        
        Image image = new Image();
        image.setUrl(result.getImageUrl());
        
        //Use Getters from the Picture object to load the FlexTable
        resultsTable.setWidget(0, 0, image);
        resultsTable.setText(1, 0, result.getTitle());
        resultsTable.setText(2, 0, result.getDescription());
        
      }

      @Override
      public void onFailure(Throwable caught) {
        caught.printStackTrace();
      }
    });

  }
}


Synchronous and Async Interfaces

If you've been working with GWT for any period of time, then you probably know that you have to use a Synchronous interface and an Asynchronous interface to make an RPC call to the server side.

This is the Sync interface:


package com.example.myproject.client;

import com.example.myproject.client.entities.Picture;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("blobservice")
public interface BlobService extends RemoteService {

  String getBlobStoreUploadUrl();

  Picture getPicture(String id);

}


This is the Async interface:

package com.example.myproject.client;

import com.example.myproject.client.entities.Picture;
import com.google.gwt.user.client.rpc.AsyncCallback;

public interface BlobServiceAsync {

  void getBlobStoreUploadUrl(AsyncCallback<String> callback);

  void getPicture(String id, AsyncCallback<Picture> callback);

}


Blob Service

The Blob Service is a GWT server side class that extends RemoteServiceServlet and it contains three methods.  The getBlobStoreUploadUrl method starts a Blobstore session and returns an Upload URL string that is added to the client side FormPanel.  The getPicture method is used later and returns an entity object from the Datastore that wraps the meta-data for the blob.  The meta-data is used in the client to serve the image itself and the title and description of the image.  The doGet method is called by the Image widget and serves the image from the blob store.

You may wonder why the Upload Service is not included in the Blob Service.  The reason is that a FormPanel cannot be used to submit to a class that extends RemoteServiceServlet because the doPost method cannot be overridden.  FormPanels submit to a class that extends HttpServlet so that the doPost method can be overridden with your code.

package com.example.myproject.server;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.myproject.client.BlobService;
import com.example.myproject.client.entities.Picture;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;

@SuppressWarnings("serial")
public class BlobServiceImpl extends RemoteServiceServlet implements
    BlobService {

  //Start a GAE BlobstoreService session and Objectify session
  BlobstoreService blobstoreService = BlobstoreServiceFactory
      .getBlobstoreService();
  Objectify ofy = ObjectifyService.begin();
  
  //Register the Objectify Service for the Picture entity
  static {
    ObjectifyService.register(Picture.class);
  }

  //Generate a Blobstore Upload URL from the GAE BlobstoreService
  @Override
  public String getBlobStoreUploadUrl() {

    //Map the UploadURL to the uploadservice which will be called by
    //submitting the FormPanel
    return blobstoreService
        .createUploadUrl("/blobstoreexample/uploadservice");
  }

  //Retrieve the Blob's meta-data from the Datastore using Objectify
  @Override
  public Picture getPicture(String id) {
    
    long l = Long.parseLong(id);
    Picture picture = ofy.get(Picture.class, l);
    return picture;
  }
  
  //Override doGet to serve blobs.  This will be called automatically by the Image Widget
  //in the client
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

        BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
        blobstoreService.serve(blobKey, resp);

  }
}


Picture Entity

The Picture class is an entity class that wraps meta-data about the picture so that it can be stored in the Datastore.  The Picture object contains the Image's blob-key, the title of the image, and the description of the image.  The blob-key is used to serve the picture from the Blobstore.  Getters and Setters are used to conveniently set data in the object and get it back.

package com.example.myproject.client.entities;

import java.io.Serializable;

import javax.persistence.Id;

@SuppressWarnings("serial")
public class Picture implements Serializable {

  @Id
  public Long id;
  public String title;
  public String description;
  public String imageUrl;

  public void setTitle(String title) {
    this.title = title;
  }

  public String getTitle() {
    return title;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public String getDescription() {
    return description;
  }

  public String getImageUrl() {
    return imageUrl;
  }

  public void setImageUrl(String imageUrl) {
    this.imageUrl = imageUrl;

  }

  public void setId(Long id) {
    this.id = id;
  }

  public Long getId() {
    return id;
  }
}


Upload Service

The Upload Service is called when the FormPanel is submitted and will submit the Blob to the Blobstore.  At the same time, we are submitting some meta-data about the Blob to the Datastore, and responding with the meta-data unique ID.

package com.example.myproject.server;

import java.io.IOException;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.myproject.client.entities.Picture;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;

//The FormPanel must submit to a servlet that extends HttpServlet  
//RemoteServiceServlet cannot be used
@SuppressWarnings("serial")
public class UploadServiceImpl extends HttpServlet {

  //Start Blobstore and Objectify Sessions
  BlobstoreService blobstoreService = BlobstoreServiceFactory
      .getBlobstoreService();
  Objectify ofy = ObjectifyService.begin();

  static {
    ObjectifyService.register(Picture.class);
  }

  //Override the doPost method to store the Blob's meta-data
  public void doPost(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {

    Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req);
    BlobKey blobKey = blobs.get("upload");

    //Get the paramters from the request to populate the Picture object
    Picture picture = new Picture();
    picture.setDescription(req.getParameter("descriptionTextBox"));
    picture.setTitle(req.getParameter("titleTextBox"));
    //Map the ImageURL to the blobservice servlet, which will serve the image
    picture.setImageUrl("/blobstoreexample/blobservice?blob-key=" + blobKey.getKeyString());

    ofy.put(picture);

    //Redirect recursively to this servlet (calls doGet)
    res.sendRedirect("/blobstoreexample/uploadservice?id=" + picture.id);
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    //Send the meta-data id back to the client in the HttpServletResponse response
    String id = req.getParameter("id");
    resp.setHeader("Content-Type", "text/html");
    resp.getWriter().println(id);

  }

}


Web.xml

Web.xml is a critical piece of the puzzle for all GAE servlets and I have found that many of the errors I make are in web.xml.  Make sure that fully qualified class name and URL pattern are correct and remember the following:
  • For GWT servlets, the URL must map to the @RemoteServiceRelativePath annotation in your Synchronous interface
  • When you get a Blobstore Upload URL, map it to the URL in your uploadServlet.
  • When you serve a Blob, map the URL to your blobServlet.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <!-- Servlets -->

  <servlet>
    <servlet-name>blobServlet</servlet-name>
    <servlet-class>com.example.myproject.server.BlobServiceImpl</servlet-class>
  </servlet>

  <servlet>
    <servlet-name>uploadServlet</servlet-name>
    <servlet-class>com.example.myproject.server.UploadServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>blobServlet</servlet-name>
    <url-pattern>/blobstoreexample/blobservice</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>uploadServlet</servlet-name>
    <url-pattern>/blobstoreexample/uploadservice</url-pattern>
  </servlet-mapping>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>BlobstoreExample.html</welcome-file>
  </welcome-file-list>

</web-app>


Conclusion

This is what the application looks like in action:
 

Hopefully this tutorial will shed some light on using the GAE Blobstore.  When you are testing on your local development server, you can check the local datastore at http://localhost:8888/_ah/admin/datastore.
Please leave a comment if you have any questions or suggestions on improving this tutorial.

39 comments:

  1. This is a very useful code set...fentastic

    ReplyDelete
  2. Thank you so much for this tutorial. I have been working on this for days and was unable to decipher the information I found. This is wonderfully clear, thorough and complete. Thank you so much!

    ReplyDelete
  3. I can't add the mainpanel to the roor container, I get a null point exception. Strange...

    ReplyDelete
  4. If you want to post your stacktrace from the null pointer exception, I'll take a look.

    ReplyDelete
  5. Simply amazing. I really appreciate the code and concise explanations.

    ReplyDelete
  6. Hi fishbone,
    i like your tutorial, but for this tuto, i've a little problem :

    warning console :

    [WARN] No file found for: /BlobstoreExample/blobservice

    I've a problem with my web.xml ?

    Thanks for your tutorial ! =)

    ReplyDelete
  7. @Loic, make sure the RemoteServiceRelativePath annotation in your synchronous interface matches the URL pattern you configured in your web.xml file.

    ReplyDelete
  8. Thanks this post helped me a lot. I think that is important to say that you need to have a billing plan to be able to use the blobstore in the app engine production enviroment.

    ReplyDelete
  9. @fishbone Big Thanks ! It's OK =)

    ReplyDelete
  10. Thanks!!! Saved a lot of time :-)!

    ReplyDelete
  11. Hi,

    I am having a problem with the getPicture. I added a Window.alert to uploadForm.addSubmitCompleteHandler and got: "<-- OPTIONAL: include this if you want history support..." It's HTML.

    I think the problem is my UploadServiceImpl does not set Picture Id and pass it back correctly. where should Picture id be set?

    Thanks!

    ReplyDelete
  12. I've triple checked my web.xml and remoteservicerelativepath...i still get this error

    [WARN] No file found for: /blobstoreexample/blobservice
    com.google.gwt.user.client.rpc.StatusCodeException: 404

    ReplyDelete
  13. at com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.onResponseReceived(RequestCallbackAdapter.java:209)
    at com.google.gwt.http.client.Request.fireOnResponseReceived(Request.java:287)
    at com.google.gwt.http.client.RequestBuilder$1.onReadyStateChange(RequestBuilder.java:395)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    Please help me fix this!

    ReplyDelete
  14. the error is too long...but this is how it goes..pls help me fix it..thank you!

    ReplyDelete
  15. guess it somehow corrected itself...now workin properly!

    ReplyDelete
  16. Excellent tutorial. Thank you very much.

    ReplyDelete
  17. hello sir,
    Image file is uploaded successfully but not displayed.
    And also i can't understand the line
    return blobstoreService .createUploadUrl("/blobstoreexample/uploadservice");

    because there is no uploadservice interface.
    Please reply.

    ReplyDelete
  18. This comment has been removed by the author.

    ReplyDelete
  19. This comment has been removed by the author.

    ReplyDelete
  20. This comment has been removed by the author.

    ReplyDelete
  21. Unbelievably well written and helpful. Thank you very much.

    ReplyDelete
  22. Very well written thank you for your help...Can you do/direct me to a tutorial to use video in the same manner. Thanks alot

    ReplyDelete
  23. Also I can not post the picture every time I hit submit I get this error:
    WARNING: Error for /_ah/upload/aglub19hcHBfaWRyGwsSFV9fQmxvYlVwbG9hZFNlc3Npb25fXxgFDA
    java.lang.NoClassDefFoundError: Could not initialize class com.crewes.test.server.UploadServiceImpl
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at java.lang.Class.newInstance0(Class.java:355)
    at java.lang.Class.newInstance(Class.java:308)
    at org.mortbay.jetty.servlet.Holder.newInstance(Holder.java:153)
    at org.mortbay.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:428)
    at

    ReplyDelete
  24. I encountered two problems:
    1. "Attempting to register Picture.class twice"
    Removing the code below from BlobServiceImpl seems to have fixed that.

    static {
    ObjectifyService.register(Picture.class);
    }

    2. Umbrella exception after FormPanel.post() prevented the SubmitCompleteEvent to fire.
    Looks like this is because the URL retrieved from createUploadUrl used my computer name instead of 127.0.0.1 and then Chrome block this as a cross domain post. Changing to the code below fixed it.

    blobstoreService.createUploadUrl("/potchproperties/uploadservice").replace("-mycomputername-", "127.0.0.1");

    ReplyDelete
  25. @Mike thanks so much for your help I had no idea where to go to figure that one out. Now everything worked out great...one more question for the application to work once its uploaded to Google App Engine do you change it back to the given code or do you do the .replace("-mycomputername-", ...) With a different address?

    ReplyDelete
  26. @lilcrewes You're welcome. The .replace() will do nothing if you leave it on the production code as your computer name will not be part of the GAE upload URL. Better to take it off anyway as it's an unnecessary check and just wastes processing power and time.

    ReplyDelete
  27. @Mike thanks that worked out great :)

    ReplyDelete
  28. Awesome....Very Helpful :-)

    ReplyDelete
  29. Hi,
    running on local machine, the image + metadata gets uploaded, but I get an UmbrellaException:

    ...
    com.google.gwt.event.shared.UmbrellaException: One or more exceptions caught, see full set in UmbrellaException#getCauses
    ...
    Caused by: java.lang.NullPointerException: null
    at com.example.myproject.client.BlobstoreExample$2.onSubmitComplete(BlobstoreExample.java:111)
    at com.google.gwt.user.client.ui.FormPanel$SubmitCompleteEvent.dispatch(FormPanel.java:115)
    at com.google.gwt.user.client.ui.FormPanel$SubmitCompleteEvent.dispatch(FormPanel.java:1)
    ...

    line 111 is this code:
    getPicture(event.getResults().trim());

    Setting a breakpoint, looks like the resultHtml field of event is null
    Tried Mike's solution, but it does not work...
    Pls help...

    ReplyDelete
  30. I had the same problem as ron1. Did this problem ever get solved, and if so can someone direct me to the solution?

    ReplyDelete
  31. Yes double check to ensure that you spelled everything correctly this also gave me the same error

    ReplyDelete
  32. thank you!
    i have some errors: getPicture(event.getResults().trim());
    event.getResults().trim() null; i don not understand . can you help me?
    thank

    ReplyDelete
  33. can we use this for video file upload?

    ReplyDelete
  34. Note: when testing locally you may have trouble with the blob service returning a URL which contains your machine name and not 127.0.0.1. This will then result in the results returned in the onSubmit handler being null. To correct simply test for your machine name and replace with 127.0.0.1.

    Also I am not sure why you cant return the blob-key from the doPost?

    ReplyDelete
    Replies
    1. Bingo!! I've tried so may things, and FINALLY checking the URL of the blob service was it! Is there a better way to solve this, other than just replacing your hostname with 127.0.0.1?

      Delete
  35. is it possible to upload a file using multipart to GAE in blobstore

    ReplyDelete
  36. uploadForm.setAction(result.toString());

    result = result.replace("hostname", "127.0.0.1");
    uploadForm.setAction(result.toString());

    これでローカルでも問題なく動くようになりました。
    本当に助かりました。誠にありがとうございます。

    ReplyDelete
  37. I know that this post is pretty old, but i still have trouble with that warnung

    [WARN] No file found for: /blobstoreexample/blobservice
    com.google.gwt.user.client.rpc.StatusCodeException: 404

    i've experimented with the web.xml and the solutions offered here(changing the hostname with ip), but it gets me nowhere. would be nice if someone can help me out

    ReplyDelete
  38. Did everything "Mike Breytenbach" suggested, changed the url-path in "UploadServiceImpl" and the web.xml and it finally works. not sure why, though

    ReplyDelete