I’ve been working on an image sharing application using GWT and App Engine to familiarize myself with the newer aspects of GWT. The project and code are here:

http://ikai-photoshare.appspot.com

http://github.com/ikai/gwt-gae-image-gallery

(Please excuse spaghetti code in client side GWT code, much of it was me feeling my way around GWT. I’ve come to appreciate GWT quite a bit in spite of the fact that I’m pretty familiar with client side development; I’ll write about this in a future post).

The 1.3.6 release of the App Engine SDK shipped with a high performance image serving API. What this means is that a developer can take a blob key pointing to image data stored in the blobstore and call getServingUrl() to create a special URL for serving the image. What are the benefits to using this API?

You don’t have to write your own handler for uploaded images

You don’t have to consume storage quota for saving resized or cropped images, as you can perform transforms on the image simply by appending URL parameters. You only need to store the final URL that is generated by getServingUrl().

You aren’t charged for datastore CPU for fetching the image (you will still be billed for bandwidth)

Images are, in general, served from edge server locations which can be geographically located closer to the user

There are a few drawbacks, however, to using the API:

There aren’t any great schemes for access control of the images, and if someone has the URL for a thumbnail, they can easily remove the parameters to see a larger image

Billing must be enabled – you will only be charged for usage, however, so you don’t have to spend a cent to use the API. You just have to have billing active.

Deleting an image blob doesn’t delete the image being served from the URL right away – that image will still be available for some time

Images must be uploaded to the blobstore, not the datastore as a blob, so it’s important to understand how the blobstore API works

The URLs of the created images are really, really ugly. If you need pretty URLs, it’s probably a better pattern to create a URL mapping to an HTML page that just displays the image in an IMG tag

Blobstore crash course

It’ll be best if we gave a quick refresher course on the blobstore before we begin. Here’s the standard flow for a blobstore upload:

Create a new blobstore session and generate an upload URL for a form to POST to. This is done using the createUploadUrl() method of BlobstoreService. Pass a callback URL to this method. This URL is where the user will be forwarded after the upload has completed. Present an upload form to the user. The action is the URL generated in step 1. Each URL must be unique: you cannot use the same URL for multiple sessions, as this will cause an error. After the URL has uploaded the file, the user is forwarded to the callback URL in your App Engine application specified in step 1. The key of the uploaded blob, a String blob key, is passed as an URL parameter. Save this URL and pass the user to their final destination

Got it? Now we can talk about image serving.

Using the image serving URL

Once we have a blob key (step 3 of a Blobstore upload), we can do interesting things with it. First, we’ll need to create an instance of the ImagesService:

ImagesService imagesService = ImagesServiceFactory.getImagesService();

Once we have an instance, we pass the blob key to getServingUrl and get back a URL:

String imageUrl = imagesService.getServingUrl(blobKey);

This can sometimes take several hundred milliseconds to a few seconds to generate, so it’s almost always a good idea to run this on write as opposed to first read. Subsequent calls should be faster, but they may not be as fast as reading this value from a datastore entity property or memcache. Since this value doesn’t change, it’s a good idea to store it. On the local dev server, this URL looks something like this:

/_ah/img/eq871HJL_bYxhWQbTeYYoA

In production, however, this will return a URL that looks like this:

http://lh5.ggpht.com/2PQk0vDo8Bn8oiPba2gtGlDfd1ciD0H0MLrixcT12FCDQEm2oyMW9ErJX_-ZzOHBWbYBKzevK0BY6cxdZ3cxf_37

(Cute dogs below)



You’ve already saved yourself the trouble of writing a handler. What’s really nice about this URL is that you can perform operations on it just by appending parameters. Let’s say we wanted to crop our image to be no larger than 200×200, yet retain scale. We’d simply append “=s200” to the end of the image:

http://lh5.ggpht.com/2PQk0vDo8Bn8oiPba2gtGlDfd1ciD0H0MLrixcT12FCDQEm2oyMW9ErJX_-ZzOHBWbYBKzevK0BY6cxdZ3cxf_37=s144

(Looks like this)



We can also crop the image by appending a “-c” to the size parameter:

http://lh5.ggpht.com/2PQk0vDo8Bn8oiPba2gtGlDfd1ciD0H0MLrixcT12FCDQEm2oyMW9ErJX_-ZzOHBWbYBKzevK0BY6cxdZ3cxf_37=s144-c

(Looks like this – compare with above)

Note that we can also generate these URLs programmatically using the overloaded version of getServingUrl that also accepts a size and crop parameter.

Adding GWT

So now that we’ve got all that done, let’s get it working with GWT. It’s important that we understand how it all works, because GWT’s single-page, Javascript-generated content model must be taken into account. Let’s draw our upload widget. We’ll be using UiBinder:

We’ll create our Composite class as follows:

public class UploadPhoto extends Composite { private static UploadPhotoUiBinder uiBinder = GWT.create(UploadPhotoUiBinder.class); UserImageServiceAsync userImageService = GWT.create(UserImageService.class); interface UploadPhotoUiBinder extends UiBinder {} @UiField Button uploadButton; @UiField FormPanel uploadForm; @UiField FileUpload uploadField; public UploadPhoto(final LoginInfo loginInfo) { initWidget(uiBinder.createAndBindUi(this)); } }

Here’s the corresponding XML file:

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"> <g:FormPanel ui:field="uploadForm"> <g:HorizontalPanel> <g:FileUpload ui:field="uploadField"></g:FileUpload> <g:Button ui:field="uploadButton"></g:Button> </g:HorizontalPanel> </g:FormPanel> </ui:UiBinder>

(We’ll add more to this later)

When we discussed the Blobstore, we mentioned that each upload form has a different POST location corresponding to the upload session. We’ll have to add a GWT-RPC component to generate and return a URL. Let’s do that now:

// UserImageService.java @RemoteServiceRelativePath("images") public interface UserImageService extends RemoteService { public String getBlobstoreUploadUrl(); }

Our IDE will nag us to generate the corresponding Async interface if we have a GWT plugin:

// UserImageServiceAsync.java public interface UserImageServiceAsync { public void getBlobstoreUploadUrl(AsyncCallback callback); }

We’ll need to write the code on the server side:

// UserImageServiceImpl.java @SuppressWarnings("serial") public class UserImageServiceImpl extends RemoteServiceServlet implements UserImageService { @Override public String getBlobstoreUploadUrl() { BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); return blobstoreService.createUploadUrl("/upload"); } }

This is pretty straightforward. We’ll want to invoke this service on the client side when we build the form. Let’s add this to UploadPhoto:

public class UploadPhoto extends Composite { private static UploadPhotoUiBinder uiBinder = GWT.create(UploadPhotoUiBinder.class); UserImageServiceAsync userImageService = GWT.create(UserImageService.class); interface UploadPhotoUiBinder extends UiBinder {} @UiField Button uploadButton; @UiField FormPanel uploadForm; @UiField FileUpload uploadField; public UploadPhoto() { initWidget(uiBinder.createAndBindUi(this)); // Disable the button until we get the URL to POST to uploadButton.setText("Loading..."); uploadForm.setEncoding(FormPanel.ENCODING_MULTIPART); uploadForm.setMethod(FormPanel.METHOD_POST); uploadButton.setEnabled(false); uploadField.setName("image"); // Now we use out GWT-RPC service and get an URL startNewBlobstoreSession(); // Once we've hit submit and it's complete, let's set the form to a new session. // We could also have probably done this on the onClick handler uploadForm.addSubmitCompleteHandler(new FormPanel.SubmitCompleteHandler() { @Override public void onSubmitComplete(SubmitCompleteEvent event) { uploadForm.reset(); startNewBlobstoreSession(); } }); } private void startNewBlobstoreSession() { userImageService.getBlobstoreUploadUrl(new AsyncCallback() { @Override public void onSuccess(String result) { uploadForm.setAction(result); uploadButton.setText("Upload"); uploadButton.setEnabled(true); } @Override public void onFailure(Throwable caught) { // We probably want to do something here } }); } @UiHandler("uploadButton") void onSubmit(ClickEvent e) { uploadForm.submit(); } }

This is fairly standard GWT RPC.

So that concludes the GWT part of it. We mentioned an upload callback. Let’s implement that now:

/** * @author Ikai Lan * * This is the servlet that handles the callback after the blobstore * upload has completed. After the blobstore handler completes, it POSTs * to the callback URL, which must return a redirect. We redirect to the * GET portion of this servlet which sends back a key. GWT needs this * Key to make another request to get the image serving URL. This adds * an extra request, but the reason we do this is so that GWT has a Key * to work with to manage the Image object. Note the content-type. We * *need* to set this to get this to work. On the GWT side, we'll take * this and show the image that was uploaded. * */ @SuppressWarnings("serial") public class UploadServlet extends HttpServlet { private static final Logger log = Logger.getLogger(UploadServlet.class .getName()); private BlobstoreService blobstoreService = BlobstoreServiceFactory .getBlobstoreService(); public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { Map blobs = blobstoreService.getUploadedBlobs(req); BlobKey blobKey = blobs.get("image"); if (blobKey == null) { // Uh ... something went really wrong here } else { ImagesService imagesService = ImagesServiceFactory .getImagesService(); // Get the image serving URL String imageUrl = imagesService.getServingUrl(blobKey); // For the sake of clarity, we'll use low-level entities Entity uploadedImage = new Entity("UploadedImage"); uploadedImage.setProperty("blobKey", blobKey); uploadedImage.setProperty(UploadedImage.CREATED_AT, new Date()); // Highly unlikely we'll ever filter on this property uploadedImage.setUnindexedProperty(UploadedImage.SERVING_URL, imageUrl); DatastoreService datastore = DatastoreServiceFactory .getDatastoreService(); datastore.put(uploadedImage); res.sendRedirect("/upload?imageUrl=" + imageUrl); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String imageUrl = req.getParameter("imageUrl"); resp.setHeader("Content-Type", "text/html"); // This is a bit hacky, but it'll work. We'll use this key in an Async // service to // fetch the image and image information resp.getWriter().println(imageUrl); } }

We’ll probably want to display the image we just uploaded in the client. Let’s add a line line of code to register a SubmitCompleteHandler to do this:

public void onSubmitComplete(SubmitCompleteEvent event) { uploadForm.reset(); startNewBlobstoreSession(); // This is what gets the result back - the content-type *must* be // text-html String imageUrl = event.getResults(); Image image = new Image(); image.setUrl(imageUrl); final PopupPanel imagePopup = new PopupPanel(true); imagePopup.setWidget(image); // Add some effects imagePopup.setAnimationEnabled(true); // animate opening the image imagePopup.setGlassEnabled(true); // darken everything under the image imagePopup.setAutoHideEnabled(true); // close image when the user clicks // outside it imagePopup.center(); // center the image }

And we’re done!

Get the code

I’ve got the code for this project here:

http://github.com/ikai/gwt-gae-image-gallery

Just a warning, this is a bit different from the sample code above. I wrote this post after I wrote the code, extrapolating the bare minimum to make this work. The sample code above has experimental tagging, delete and catches logins. I’m adding features to it simply to see what else can be done, so expect changes. I’m aware of a few of the bugs with the code, and I’ll get around to fixing them, but again, it’s a demo project, so keep realistic expectations. As far as I can tell, however, the code above should be runnable locally and deployable (once you have enabled billing for blobstore).

Happy developing!