Java tutorial
/* * Copyright 2011 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.walkaround.wave.server.attachment; import com.google.appengine.api.blobstore.BlobInfo; import com.google.appengine.api.blobstore.BlobInfoFactory; import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.blobstore.BlobstoreService; import com.google.appengine.api.memcache.Expiration; import com.google.appengine.api.memcache.MemcacheService; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.walkaround.util.server.appengine.CheckedDatastore; import com.google.walkaround.util.server.appengine.MemcacheTable; import com.google.walkaround.util.server.servlet.NotFoundException; import com.google.walkaround.util.shared.Assert; import com.google.walkaround.wave.server.Flag; import com.google.walkaround.wave.server.FlagName; import com.google.walkaround.wave.server.attachment.AttachmentMetadata.ImageMetadata; import com.google.walkaround.wave.server.attachment.ThumbnailDirectory.ThumbnailData; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Facilities for dealing with attachments. Caches various results in memcache * and the datastore. * * @author danilatos@google.com (Daniel Danilatos) */ public class AttachmentService { // Don't save thumbnails larger than 50K (entity max size is 1MB). // They should usually be around 2-3KB each. @SuppressWarnings("unused") private static final Logger log = Logger.getLogger(AttachmentService.class.getName()); static final int INVALID_ID_CACHE_EXPIRY_SECONDS = 600; private static final String MEMCACHE_TAG = "AT2"; private final RawAttachmentService rawService; private final BlobstoreService blobstore; private final MetadataDirectory metadataDirectory; // We cache Optional.absent() for invalid attachment ids. private final MemcacheTable<AttachmentId, Optional<AttachmentMetadata>> metadataCache; private final ThumbnailDirectory thumbnailDirectory; private final int maxThumbnailSavedSizeBytes; @Inject public AttachmentService(RawAttachmentService rawService, BlobstoreService blobStore, CheckedDatastore datastore, MemcacheTable.Factory memcacheFactory, @Flag(FlagName.MAX_THUMBNAIL_SAVED_SIZE_BYTES) int maxThumbnailSavedSizeBytes) { this.rawService = rawService; this.blobstore = blobStore; this.metadataDirectory = new MetadataDirectory(datastore); this.metadataCache = memcacheFactory.create(MEMCACHE_TAG); this.thumbnailDirectory = new ThumbnailDirectory(datastore); this.maxThumbnailSavedSizeBytes = maxThumbnailSavedSizeBytes; } /** * Checks if the browser has the data cached. * @return true if it was, and there's nothing more to do in serving this request. */ private boolean maybeCached(HttpServletRequest req, HttpServletResponse resp, String context) { // Attachments are immutable, so we don't need to check the date. // If the id was previously requested and yielded a 404, the browser shouldn't be // using this header. String ifModifiedSinceStr = req.getHeader("If-Modified-Since"); if (ifModifiedSinceStr != null) { log.info("Telling browser to use cached attachment (" + context + ")"); resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return true; } return false; } /** * Serves the attachment with cache control. * * @param req Only used to check the If-Modified-Since header. */ public void serveDownload(AttachmentId id, HttpServletRequest req, HttpServletResponse resp) throws IOException { if (maybeCached(req, resp, "download, id=" + id)) { return; } AttachmentMetadata metadata = getMetadata(id); if (metadata == null) { throw NotFoundException.withInternalMessage("Attachment id unknown: " + id); } BlobKey key = metadata.getBlobKey(); BlobInfo info = new BlobInfoFactory().loadBlobInfo(key); String disposition = "attachment; filename=\"" // TODO(ohler): Investigate what escaping we need here, and whether the // blobstore service has already done some escaping that we need to undo // (it seems to do percent-encoding on " characters). + info.getFilename().replace("\"", "\\\"").replace("\\", "\\\\") + "\""; log.info("Serving " + info + " with Content-Disposition: " + disposition); resp.setHeader("Content-Disposition", disposition); blobstore.serve(key, resp); } public Void serveThumbnail(AttachmentId id, HttpServletRequest req, HttpServletResponse resp) throws IOException { if (maybeCached(req, resp, "thumbnail, id=" + id)) { return null; } AttachmentMetadata metadata = getMetadata(id); if (metadata == null) { throw NotFoundException.withInternalMessage("Attachment id unknown: " + id); } BlobKey key = metadata.getBlobKey(); // TODO(danilatos): Factor out some of this code into a separate method so that // thumbnails can be eagerly created at upload time. ThumbnailData thumbnail = thumbnailDirectory.getWithoutTx(key); if (thumbnail == null) { log.info("Generating and storing thumbnail for " + key); ImageMetadata thumbDimensions = metadata.getThumbnail(); if (thumbDimensions == null) { // TODO(danilatos): Provide a default thumbnail throw NotFoundException.withInternalMessage("No thumbnail available for attachment " + id); } byte[] thumbnailBytes = rawService.getResizedImageBytes(key, thumbDimensions.getWidth(), thumbDimensions.getHeight()); thumbnail = new ThumbnailData(key, thumbnailBytes); if (thumbnailBytes.length > maxThumbnailSavedSizeBytes) { log.warning( "Thumbnail for " + key + " too large to store " + "(" + thumbnailBytes.length + " bytes)"); // TODO(danilatos): Cache this condition in memcache. throw NotFoundException.withInternalMessage("Thumbnail too large for attachment " + id); } thumbnailDirectory.getOrAdd(thumbnail); } else { log.info("Using already stored thumbnail for " + key); } // TODO(danilatos): Other headers for mime type, fileName + "Thumbnail", etc? resp.getOutputStream().write(thumbnail.getBytes()); return null; } @Nullable public AttachmentMetadata getMetadata(AttachmentId id) throws IOException { Preconditions.checkNotNull(id, "Null id"); Map<AttachmentId, Optional<AttachmentMetadata>> result = getMetadata(ImmutableList.of(id), null); return result.get(id).isPresent() ? result.get(id).get() : null; } /** * @param maxTimeMillis Maximum time to take. -1 for indefinite. If the time * runs out, some data may not be returned, so the resulting map may * be missing some of the input ids. Callers may retry to get the * remaining data for the missing ids. * * @return a map of input id to attachment metadata for each id. invalid ids * will map to Optional.absent(). Some ids may be missing due to the time limit. * * At least one id is guaranteed to be returned. */ public Map<AttachmentId, Optional<AttachmentMetadata>> getMetadata(List<AttachmentId> ids, @Nullable Long maxTimeMillis) throws IOException { Stopwatch stopwatch = new Stopwatch().start(); Map<AttachmentId, Optional<AttachmentMetadata>> result = Maps.newHashMap(); for (AttachmentId id : ids) { // TODO(danilatos): To optimise, re-arrange the code so that // 1. Query all the ids from memcache in one go // 2. Those that failed, query all remaining ids from the data store in one go // 3. Finally, query all remaining ids from the raw service in one go (the // raw service api should be changed to accept a list, and it needs to // query the __BlobInfo__ entities directly. Optional<AttachmentMetadata> metadata = metadataCache.get(id); if (metadata == null) { AttachmentMetadata storedMetadata = metadataDirectory.getWithoutTx(id); if (storedMetadata != null) { metadata = Optional.of(storedMetadata); metadataCache.put(id, metadata); } else { metadata = Optional.absent(); metadataCache.put(id, metadata, Expiration.byDeltaSeconds(INVALID_ID_CACHE_EXPIRY_SECONDS), MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); } } Assert.check(metadata != null, "Null metadata"); result.put(id, metadata); if (maxTimeMillis != null && stopwatch.elapsedMillis() > maxTimeMillis) { break; } } Assert.check(!result.isEmpty(), "Should return at least one id"); return result; } }