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
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.


This is a very useful code set...fentastic
ReplyDeleteThank 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!
ReplyDeleteI can't add the mainpanel to the roor container, I get a null point exception. Strange...
ReplyDeleteIf you want to post your stacktrace from the null pointer exception, I'll take a look.
ReplyDeleteSimply amazing. I really appreciate the code and concise explanations.
ReplyDeleteHi fishbone,
ReplyDeletei 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 ! =)
@Loic, make sure the RemoteServiceRelativePath annotation in your synchronous interface matches the URL pattern you configured in your web.xml file.
ReplyDeleteThanks 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@fishbone Big Thanks ! It's OK =)
ReplyDeleteThanks!!! Saved a lot of time :-)!
ReplyDeleteHi,
ReplyDeleteI 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!
I've triple checked my web.xml and remoteservicerelativepath...i still get this error
ReplyDelete[WARN] No file found for: /blobstoreexample/blobservice
com.google.gwt.user.client.rpc.StatusCodeException: 404
at com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.onResponseReceived(RequestCallbackAdapter.java:209)
ReplyDeleteat 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!
the error is too long...but this is how it goes..pls help me fix it..thank you!
ReplyDeleteguess it somehow corrected itself...now workin properly!
ReplyDeleteExcellent tutorial. Thank you very much.
ReplyDeletehello sir,
ReplyDeleteImage 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.
This comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteUnbelievably well written and helpful. Thank you very much.
ReplyDeleteVery well written thank you for your help...Can you do/direct me to a tutorial to use video in the same manner. Thanks alot
ReplyDeleteAlso I can not post the picture every time I hit submit I get this error:
ReplyDeleteWARNING: 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
I encountered two problems:
ReplyDelete1. "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");
@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@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@Mike thanks that worked out great :)
ReplyDeleteAwesome....Very Helpful :-)
ReplyDeleteHi,
ReplyDeleterunning 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...
I had the same problem as ron1. Did this problem ever get solved, and if so can someone direct me to the solution?
ReplyDeleteYes double check to ensure that you spelled everything correctly this also gave me the same error
ReplyDeletethank you!
ReplyDeletei have some errors: getPicture(event.getResults().trim());
event.getResults().trim() null; i don not understand . can you help me?
thank
can we use this for video file upload?
ReplyDeleteNote: 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.
ReplyDeleteAlso I am not sure why you cant return the blob-key from the doPost?
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?
Deleteis it possible to upload a file using multipart to GAE in blobstore
ReplyDeleteuploadForm.setAction(result.toString());
ReplyDelete↓
result = result.replace("hostname", "127.0.0.1");
uploadForm.setAction(result.toString());
これでローカルでも問題なく動くようになりました。
本当に助かりました。誠にありがとうございます。
I know that this post is pretty old, but i still have trouble with that warnung
ReplyDelete[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
Did everything "Mike Breytenbach" suggested, changed the url-path in "UploadServiceImpl" and the web.xml and it finally works. not sure why, though
ReplyDelete