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.
Click here to continue reading...