For applications using Google Cloud platform, a common pattern of data storage and retrieval is the storing of binary data in Google Cloud Storage (GCS) and data related to the binary data, such as keys, owner, timestamp, etc., in other storage, such as App Engine Datastore or Google Cloud SQL.
The Photofeed sample illustrates this pattern in its storage of photo
image binaries in GCS and the storage of the corresponding photo metadata
in the Datastore as
Photo
entities, with the metadata including
the keys needed to serve the photo image binaries.
Synopsis
In the Photofeed sample, when the main JSP page
(
war/photofeed.jsp
) loads, all of the photos are
displayed in the order uploaded, with all comments for each photo displayed
under the photo, in the order the comments were uploaded.
To understand what is happening in the photo and comment iteration, we
need to remember that the
Comment
objects and the
Photo
"metadata" objects are stored in datastore while the
actual photo binaries are stored elsewhere, in Google Cloud Storage. So,
we iterate through each
Photo
object to get the blob key we
need to serve the corresponding photo binary from Google Cloud Storage,
using the
blobstoreService
.
The list of
Photo
objects is
retrieved from the datastore and is iterated. In each iteration, the photo
ID and photo owner ID are used by the
PhotoServiceManager
in
a GET request to the
DownloadServlet
running at
/download
. This servlet gets the blob key from the
Photo
object being iterated, and supplies this blob key to
the
blobstoreService
, which serves the photo binary.
After the binary is served, all of the
Comment
objects for
the
Photo
are displayed.
Let's take a closer look at the code.
Serving Photos in the Photofeed JSP
The following code in
/photo-sharing-demo/war/photofeed.jsp
shows the start of the iteration through the
Photo
objects
stored in the Datastore:
<% Iterable<Photo> photoIter = photoManager.getActivePhotos(); ArrayList<Photo> photos = new ArrayList<Photo>(); for (Photo photo : photoIter) { photos.add(photo); } int count = 0; for (Photo photo : photos) { String firstClass = ""; String lastClass = ""; if (count == 0) { firstClass = "first"; } if (count == photos.size() - 1) { lastClass = "last"; } %> <div class="feed<%= firstClass %><%= lastClass %>"> <div class="post group"> <div class="image-wrap"> <img class="photo-image" src="<%= serviceManager.getImageDownloadUrl(photo)%>" alt="Photo Image" /> </div> <div class="owner group"> <img src="<%= ServletUtils.getUserIconImageUrl(photo.getOwnerId()) %>" alt="" /> <div class="desc"> <h3><%= ServletUtils.getProtectedUserNickname(photo.getOwnerNickname()) %></h3> <p><%= photo.getTitle() %> <p> <p class="timestamp"><%= ServletUtils.formatTimestamp(photo.getUploadTime()) %></p> </div> <!-- /.desc --> </div> <!-- /.usr --> </div>
In the code above, the
PhotoManager
interface is used to
get a list of all active
Photo
objects from the datastore.
For more details about active versus inactive Photos see "
How to do "Transactional Deletes" across Storages
.
The list of Photos is iterated, with each iteration resulting in the
serving and display of the photo image from Google Cloud Storage along with
the display of the related user icon, name, and timestamp obtained directly
from the datastore
Photo
being iterated.
The serving of the image begins here:
serviceManager.getImageDownloadUrl(photo)
If you look at the definition of this method in
PhotoServiceManager.java
you'll see that it builds a GET
request that includes the photo ID and the photo owner ID and sends it
to the URL
/download
. Taking a look at
war/WEB-INF/web.xml
, the servlet that handles requests at that
URL is
DownloadServlet.java
:
public class DownloadServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { String user = req.getParameter("user"); String id = req.getParameter("id"); Long photoId = ServletUtils.validatePhotoId(id); if (photoId != null && user != null) { Photo photo = AppContext.getAppContext().getPhotoManager().getPhoto(user, photoId); BlobKey blobKey = photo.getBlobKey(); BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); blobstoreService.serve(blobKey, res); } else { res.sendError(400, "One or more parameters are not set"); } } }
The servlet code shows how to serve a blob in Google Cloud Storage using
blobstoreService
. The blob key is extracted from the Datastore
object containing the metadata for the blob, and the blob key is used to get
the image binary and serve it in the response.
Continuing the Iteration: Comments
During the iteration of each photo, after the photo image is served, the comments for the photo are retrieved and iterated:
Iterable<Comment> comments = commentManager.getComments(photo); for (Comment comment : comments) { %> <div class="post group"> <div class="usr"> <img src="<%= ServletUtils.getUserIconImageUrl(comment.getCommentOwnerId()) %>" alt="" /> <div class="comment"> <h3><%= ServletUtils.getProtectedUserNickname(comment.getCommentOwnerName()) %></h3> <p><%= comment.getContent() %> <p> <p class="timestamp"><%= ServletUtils.formatTimestamp(comment.getTimestamp()) %></p> </div> <!-- /.comment --> </div> <!-- /.usr --> </div> <!-- /.post --> <% } %>
In this code, the
CommentManager
model interface returns
a list of all comments for the photo currently being iterated. Those
comments are iterated and displayed along with details such the comment
author, user icon, and timestamp.