com.google.walkaround.wave.server.attachment.AttachmentService.java Source code

Java tutorial

Introduction

Here is the source code for com.google.walkaround.wave.server.attachment.AttachmentService.java

Source

/*
 * 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;
    }
}