Ikai Lan says

I say things!

GWT, Blobstore, the new high performance image serving API, and cute dogs on office chairs

with 22 comments

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:

  1. 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.
  2. 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.
  3. 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!

About these ads

Written by Ikai Lan

September 8, 2010 at 5:00 pm

Posted in App Engine, Java, Java

22 Responses

Subscribe to comments with RSS.

  1. Nice tutorial! :)

    Brandon Donnelson

    September 8, 2010 at 5:07 pm

  2. Hi! Nice post.
    I tied uploading a few pictures. Seems slower than what i’d expect. Sometimes it took 20-25s for the image to show up.
    Thanks for the post though.

    Gaurav

    September 8, 2010 at 9:33 pm

  3. I have been using the Blobstore through GWT on my app http://picsoup.appspot.com for quite a while. It resizes the images using the ImageService and then stores them in the datastore. I wrote an entry on my blog a few weeks ago about how it would be great to have a service a bit like Picasa available in App Engine: http://jeremyblythe.blogspot.com/2010/07/google-app-engine-image-storage.html . Shortly after writing that 1.3.6 was released and it looks like the answer!

    The problem I’m having at the moment, and I’m looking for a neat solution, is that I would like to continue to allow the user to upload a large image and then resize it to 800×600 before storing it back in the Blobstore so I can use the new serving method but save quota space. As far as I can tell I will have to write some server-side code to upload the resized image back to Blobstore! This seems a bit messy. It would be great if there was a function like imageService.saveAsBlob(image). Do you have a recommendation or example?

    Jeremy Blythe

    September 8, 2010 at 9:52 pm

  4. I have a question about the images size parameter that can be passed in. In the docs it says I need to use one of these images sizes. http://code.google.com/appengine/docs/python/images/functions.html#imgsize

    But on your example I can pass in any size into the url?

    John Turner

    September 9, 2010 at 7:07 am

  5. Is there a way to query for existing blobstore images? Also, am I being billed for the all the blobs in the store? Since there has to be a two step process to upload photos, and if something happens (say doesn’t fill in a form correctly) then the blob is uploaded and can easily be “lost”.

    Mike!

    Mike Ensor

    September 28, 2010 at 2:20 pm

  6. I know its late, but in response to the previous comment: my app throws IllegalStateExceptons with Invalid crop size or Invalid resize messages if I choose anything except the listed sizes, so it’s best to stick to them :)

    Ben Bedwell

    November 8, 2010 at 1:07 pm

  7. Hey,
    First, thanks for the excellent tutorial! It has really helped me out.
    I just have a quick question that maybe you can answer. When I upload a blob locally in the devserver in eclipse using your instructions everything works fine and it returns a URL for the image. The problem is that the URL that it returns points to an invalid place so I can’t see the image pop-up after the upload. The URL looks like this:
    http://0.0.0.0:8888/_ah/img/5P8xUTZuqXdKEuthotEgnw
    I think it may just be an eclipse setup mistake that I have but I am completely at a loss!

    Thanks for the help!

    Miguel

    November 17, 2010 at 5:27 pm

  8. Actually I just realized that the problem is that the server IP that the datastore generates is not the same as the devserver’s IP sunning in Eclipse. I just had to manually re-do the IP. I put a patch on it for the devserver which I will remove for the production version. Just thought that might help someone :)

    Miguel

    November 17, 2010 at 5:39 pm

  9. Thanks for the enlightening and very useful post. I’m not very experienced in the gae datastore, so perhaps I’m missing something, but don’t you need to delete the related blob when deleting the image in UserImageServiceImpl?

    Maarten

    December 1, 2010 at 3:32 am

  10. You are correct. I realized this after I posted the code and thought no one would catch me! Hah.

    Ikai Lan

    December 3, 2010 at 11:34 am

  11. Ikai, thank you for this tutorial. It helps me a lot.
    Unfortunatly, I have a problem when I run your project localy (using eclipse).
    When I choose an image from FileChooser an click “Upload” button, the popup shows but there is no image but only delete button. After hiding popup there is also no image in gallery.
    I have only two warns but it is not connected with my problem, I think.
    [WARN] No image reader found for format “ico”. An ImageIO plugin must be installed to use this format with the DevAppServer.
    [WARN] No image reader found for format “tif”. An ImageIO plugin must be installed to use this format with the DevAppServer.

    I have no installed any addition stuff/plugins apart from App Engine and GWT plugins in Eclipse.

    Kamil

    January 1, 2011 at 6:57 am

  12. Same problem. Using GWT 2.1.
    @Kamil: Did you find a solution?

    Many thanks!

    Marcus

    Marcus

    January 2, 2011 at 2:07 pm

  13. Unfortunately not :(
    Ikai, please help!

    Kamil

    January 4, 2011 at 9:48 am

  14. Hello,

    Image upload does not work if you access site using https protocol i.e.
    https://ikai-photoshare.appspot.com/

    Basically following line of code gets null as key.
    userImageService.get(key, new AsyncCallback() {

    My web site uses https for everything else and only image upload does not work because of above issue. I tried hard and gave up.

    Please help if you can.

    Thanks,
    Kam

    kam

    January 17, 2011 at 8:19 am

  15. `:; I am very thankful to this topic because it really gives great information “””

    Nappy Rash

    January 24, 2011 at 9:25 am

  16. with imageservice, i try to get original width,height of the image with ImagesServiceFactory.makeImageFromBlob(blobKey).getWidth() , but i get -1, and error java.lang.UnsupportedOperationException: No image data is available.

    can advice on how to get the original file width and height?

    cmetta

    March 5, 2011 at 7:36 am

  17. I can see the difference between the application and the code you shared. Could you please share the latest code?

    Srikanth

    March 9, 2011 at 9:44 pm

  18. I hope somebody can help me with this issue.
    I am having troubles when I use this code at the AppEngine. Sometimes it is throwing an exception when I try to upload the file. The second try always works.

    Here is the Exception from the GAE-log:
    java.lang.IllegalArgumentException: Invalid Key PB: no elements.
    at com.google.appengine.api.datastore.KeyTranslator.createFromPb(KeyTranslator.java:26)
    at com.google.appengine.api.datastore.KeyFactory.stringToKey(KeyFactory.java:197)
    at com.natelio.inTourismoMVP.server.image.UploadedImageDao.get(UploadedImageDao.java:27)
    at com.natelio.inTourismoMVP.server.image.UserImageServiceImpl.get(UserImageServiceImpl.java:24)

    Marcus

    April 14, 2011 at 12:21 pm

  19. Great tutorial… saved me a lot of time!!!!

    Jitendra Rana

    April 28, 2011 at 11:42 am

  20. Nice demo! I use facebook to authorize my user and I keep the user name in my session. Your UploadServlet creates a new session thus I loose the user name session variable. How can I resolve this?

    savilak

    May 21, 2012 at 9:13 pm

  21. I’ve checked, that this hacky redirect to doGet() is not necessary (any more?). So instead of
    res.sendRedirect(“/upload?imageUrl=” + imageUrl);

    you can write a direct response to GWT client in doPost():

    resp.setHeader(“Content-Type”, “text/html”);
    resp.getWriter().println(imageUrl);

    Krzysztof

    March 4, 2013 at 4:00 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s