Package com.google.walkaround.wave.server.attachment

Source Code of com.google.walkaround.wave.server.attachment.AttachmentService

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

Related Classes of com.google.walkaround.wave.server.attachment.AttachmentService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.