Package com.google.ytd.picasa

Source Code of com.google.ytd.picasa.PicasaApiHelper

/*
* Copyright (c) 2010 Google Inc.
*
* 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.ytd.picasa;

import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import com.google.gdata.client.photos.PicasawebService;
import com.google.gdata.data.Link;
import com.google.gdata.data.ParseSource;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.photos.AlbumEntry;
import com.google.gdata.data.photos.AlbumFeed;
import com.google.gdata.data.photos.GphotoAccess;
import com.google.gdata.data.photos.PhotoEntry;
import com.google.gdata.data.photos.UserFeed;
import com.google.gdata.util.ParseUtil;
import com.google.gdata.util.ServiceException;
import com.google.inject.Inject;
import com.google.ytd.dao.AdminConfigDao;
import com.google.ytd.dao.AssignmentDao;
import com.google.ytd.dao.DataChunkDao;
import com.google.ytd.util.Util;

import org.apache.commons.lang.StringEscapeUtils;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Class to handle interfacing with the Google Data Java Client Library's Picasa
* support.
*/
public class PicasaApiHelper {
  private static final Logger LOG = Logger.getLogger(PicasaApiHelper.class.getName());

  // CONSTANTS
  private static final String USER_FEED_URL =
      "http://picasaweb.google.com/data/feed/api/user/default";
  private static final String ALBUM_FEED_URL =
    "http://picasaweb.google.com/data/feed/api/user/default?kind=album&max-results=1000";
  private static final String RESUMABLE_UPLOADS_URL_FORMAT =
    "http://picasaweb.google.com/data/upload/resumable/photos/create-session/feed/api/user/default/albumid/%s";
  private static final String UPLOAD_ENTRY_XML_FORMAT =
    "<?xml version='1.0' encoding='UTF-8'?>\n"
    + "<entry xmlns='http://www.w3.org/2005/Atom' "
    + "xmlns:georss='http://www.georss.org/georss' xmlns:gml='http://www.opengis.net/gml'>"
    + "\n  <title>%s</title>"
    + "\n  <summary>%s</summary>"
    + "\n  <category scheme='http://schemas.google.com/g/2005#kind' "
    + "term='http://schemas.google.com/photos/2007#photo'/>"
    + "\n%s</entry>";
  private static final String GEO_RSS_XML_FORMAT = "<georss:where><gml:Point><gml:pos>%f %f"
      + "</gml:pos></gml:Point></georss:where>";
  // The connect + read timeout needs to be <= 10 seconds, due to App Engine limitations.
  private static final int CONNECT_TIMEOUT = 1000 * 2; // In milliseconds
  private static final int READ_TIMEOUT = 1000 * 8; // In milliseconds
  // The size of each resumable upload chunk we send. Due to App Engine limitations, this needs to
  // be less than 1MB.
  private static final int CHUNK_SIZE = 950 * 1024; // 950KB

  private PicasawebService service = null;
  private Util util = null;
  private AdminConfigDao adminConfigDao = null;
  private DataChunkDao dataChunkDao = null;

  @Inject
  public PicasaApiHelper(AdminConfigDao adminConfigDao, AssignmentDao assignmentDao,
      DataChunkDao dataChunkDao) {
    this.service = new PicasawebService(Util.CLIENT_ID_PREFIX + SystemProperty.applicationId.get());
    this.util = Util.get();
    this.adminConfigDao = adminConfigDao;
    this.dataChunkDao = dataChunkDao;

    setAuthSubTokenFromConfig();

    service.setConnectTimeout(CONNECT_TIMEOUT);
    service.setReadTimeout(READ_TIMEOUT);
  }

  public boolean isAuthenticated() {
    return service.getAuthTokenFactory().getAuthToken() != null;
  }
 
  public void setAuthSubTokenFromConfig() {
    String authSubToken = adminConfigDao.getAdminConfig().getPicasaAuthSubToken();
    if (!util.isNullOrEmpty(authSubToken)) {
      service.setAuthSubToken(authSubToken);
    }
  }

  public void setAuthSubToken(String token) {
    service.setAuthSubToken(token);
  }

  public String getCurrentUsername() throws IOException, ServiceException {
    try {
      UserFeed userFeed = service.getFeed(new URL(USER_FEED_URL), UserFeed.class);
      return userFeed.getUsername();
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }
 
  public List<AlbumEntry> getAllAlbums() {
    ArrayList<AlbumEntry> albums = new ArrayList<AlbumEntry>();

    try {
      URL feedUrl = new URL(ALBUM_FEED_URL);

      while (feedUrl != null) {
        UserFeed albumFeed = service.getFeed(feedUrl, UserFeed.class);
        albums.addAll(albumFeed.getAlbumEntries());
       
        Link nextLink = albumFeed.getNextLink();
        if (nextLink == null) {
          feedUrl = null;
        } else {
          feedUrl = new URL(nextLink.getHref());
        }
      }
     
      return albums;
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }

  public String createAlbum(String title, String description, boolean privateAlbum) {
    LOG.info(String.format("Attempting to create %s Picasa album...",
        privateAlbum ? "private" : "public"));
    AlbumEntry album = new AlbumEntry();

    if (privateAlbum) {
      album.setAccess(GphotoAccess.Value.PRIVATE);
    } else {
      album.setAccess(GphotoAccess.Value.PUBLIC);
    }

    album.setTitle(new PlainTextConstruct(title));
    album.setDescription(new PlainTextConstruct(description));

    try {
      AlbumEntry albumEntry = service.insert(new URL(USER_FEED_URL), album);
      String albumUrl = albumEntry.getFeedLink().getHref();
      LOG.info(String.format("Created %s Picasa album: %s",
          privateAlbum ? "private" : "public", albumUrl));

      return albumUrl;
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }

  public String moveToNewAlbum(String photoUrl, String newAlbumUrl) {
    LOG.info(String.format("Preparing to move '%s' to album '%s'...", photoUrl, newAlbumUrl));

    // We only need to get the feed's id from the feed metadata here, so there's no need to
    // retrieve more than one entry.
    String urlWithParam;
    if (newAlbumUrl.indexOf("?") != -1) {
      urlWithParam = newAlbumUrl + "&max-results=1";
    } else {
      urlWithParam = newAlbumUrl + "?max-results=1";
    }
   
    AlbumFeed albumFeed = getAlbumFeedFromUrl(urlWithParam);
    if (albumFeed == null) {
      throw new IllegalArgumentException(String.format("Could not retrieve album from URL '%s'.",
          urlWithParam));
    }
    String newAlbumId = albumFeed.getGphotoId();

    com.google.gdata.data.photos.PhotoEntry photoEntry = getPhotoEntryFromUrl(photoUrl);
    if (photoEntry == null) {
      throw new IllegalArgumentException(String.format("Could not get photo from URL '%s'.",
          photoUrl));
    }

    photoEntry.setAlbumId(newAlbumId);
    try {
      photoEntry = photoEntry.update();
      LOG.info("Move was successful.");

      return photoEntry.getEditLink().getHref();
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }

  private com.google.gdata.data.photos.PhotoEntry getPhotoEntryFromUrl(String photoUrl) {
    try {
      return service.getEntry(new URL(photoUrl), com.google.gdata.data.photos.PhotoEntry.class);
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }

  private AlbumFeed getAlbumFeedFromUrl(String albumUrl) {
    try {
      return service.getFeed(new URL(albumUrl), AlbumFeed.class);
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }

  public String getResumableUploadUrl(com.google.ytd.model.PhotoEntry photoEntry, String title,
      String description, String albumId, Double latitude, Double longitude) throws IllegalArgumentException {
    LOG.info(String.format("Resumable upload request.\nTitle: %s\nDescription: %s\nAlbum: %s",
        title, description, albumId));
   
    // Picasa API resumable uploads are not currently documented publicly, but they're essentially
    // the same as what YouTube API offers:
    // http://code.google.com/apis/youtube/2.0/developers_guide_protocol_resumable_uploads.html
    // The Java client library does offer support for resumable uploads, but its use of threads
    // and some other assumptions makes it unsuitable for our purposes.
    try {
      URL url = new URL(String.format(RESUMABLE_UPLOADS_URL_FORMAT, albumId));
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.setDoOutput(true);
      connection.setConnectTimeout(CONNECT_TIMEOUT);
      connection.setReadTimeout(READ_TIMEOUT);
      connection.setRequestMethod("POST");

      // Set all the GData request headers. These strings should probably be moved to CONSTANTS.
      connection.setRequestProperty("Content-Type", "application/atom+xml;charset=UTF-8");
      connection.setRequestProperty("Authorization", String.format("AuthSub token=\"%s\"", adminConfigDao.getAdminConfig().getPicasaAuthSubToken()));
      connection.setRequestProperty("GData-Version", "2.0");
      connection.setRequestProperty("Slug", photoEntry.getOriginalFileName());
      connection.setRequestProperty("X-Upload-Content-Type", photoEntry.getFormat());
      connection.setRequestProperty("X-Upload-Content-Length",
          String.valueOf(photoEntry.getOriginalFileSize()));
     
      // If we're given lat/long then create the element to geotag the picture; otherwise, pass in
      // and empty string for no geotag.
      String geoRss = "";
      if (latitude != null && longitude != null) {
        geoRss = String.format(GEO_RSS_XML_FORMAT, latitude, longitude);
        LOG.info("Geo RSS XML: " + geoRss);
      }
     
      String atomXml = String.format(UPLOAD_ENTRY_XML_FORMAT, StringEscapeUtils.escapeXml(title),
          StringEscapeUtils.escapeXml(description), geoRss);

      OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
      writer.write(atomXml);
      writer.close();
     
      if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
        String uploadUrl = connection.getHeaderField("Location");
        if (util.isNullOrEmpty(uploadUrl)) {
          throw new IllegalArgumentException("No Location header found in HTTP response.");
        } else {
          LOG.info("Resumable upload URL is " + uploadUrl);
       
          return uploadUrl;
        }
      } else {
        LOG.warning(String.format("HTTP POST to %s returned status %d (%s).", url.toString(),
            connection.getResponseCode(), connection.getResponseMessage()));
      }
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
     
      throw new IllegalArgumentException(e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }
 
  public PhotoEntry doResumableUpload(com.google.ytd.model.PhotoEntry photoEntry)
      throws IllegalArgumentException {
    if (util.isNullOrEmpty(photoEntry.getResumableUploadUrl())) {
      throw new IllegalArgumentException(String.format("No resumable upload URL found for "
          + "PhotoEntry id '%s'.", photoEntry.getId()));
    }

    try {
      URL url = new URL(photoEntry.getResumableUploadUrl());

      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.setInstanceFollowRedirects(false);
      connection.setConnectTimeout(CONNECT_TIMEOUT);
      connection.setReadTimeout(READ_TIMEOUT);
      connection.setRequestMethod("PUT");

      connection.setRequestProperty("Content-Range", "bytes */*");

      // Response code 308 is specific to this use case and doesn't appear to have a
      // HttpURLConnection constant.
      if (connection.getResponseCode() == 308) {
        long previousByte = 0;

        String rangeHeader = connection.getHeaderField("Range");
        if (!util.isNullOrEmpty(rangeHeader)) {
          LOG.info("Range header in 308 response is " + rangeHeader);
         
          String[] rangeHeaderSplits = rangeHeader.split("-", 2);
          if (rangeHeaderSplits.length == 2) {
            previousByte = Long.valueOf(rangeHeaderSplits[1]).longValue() + 1;
          }
        }

        connection = (HttpURLConnection) url.openConnection();
        connection.setInstanceFollowRedirects(false);
        connection.setDoOutput(true);
        connection.setConnectTimeout(CONNECT_TIMEOUT);
        connection.setReadTimeout(READ_TIMEOUT);
        connection.setRequestMethod("PUT");
       
        byte[] bytes;
        String contentRangeHeader;
       
        if (photoEntry.getBlobKey() != null) {
          long lastByte = previousByte + CHUNK_SIZE;
          if (lastByte > (photoEntry.getOriginalFileSize() - 1)) {
            lastByte = photoEntry.getOriginalFileSize() - 1;
          }

          contentRangeHeader = String.format("bytes %d-%d/%d", previousByte, lastByte,
            photoEntry.getOriginalFileSize());
         
          BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
          bytes = blobstoreService.fetchData(photoEntry.getBlobKey(), previousByte, lastByte);
        } else {
          bytes = dataChunkDao.getBytes(photoEntry.getId(), previousByte);
         
          if (bytes == null) {
            throw new IllegalArgumentException(String.format("PhotoEntry with id '%s' does not "
                + "have a valid blob key. Additionally, there is no DataChunk entry for the "
                + "initial byte '%d'.", photoEntry.getId(), previousByte));
          }
         
          contentRangeHeader = String.format("bytes %d-%d/%d", previousByte,
              previousByte + bytes.length - 1, photoEntry.getOriginalFileSize());
        }
       
        connection.setRequestProperty("Content-Length", String.valueOf(bytes.length));
       
        LOG.info("Using the following for Content-Range header: " + contentRangeHeader);
        connection.setRequestProperty("Content-Range", contentRangeHeader);

        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(bytes);
        outputStream.close();

        if (connection.getResponseCode() == HttpURLConnection.HTTP_CREATED) {
          LOG.info("Resumable upload is complete and successful.");

          return (PhotoEntry) ParseUtil.readEntry(new ParseSource(connection.getInputStream()));
        }
      } else if (connection.getResponseCode() == HttpURLConnection.HTTP_CREATED) {
        // It's possible that the Picasa upload associated with the specific resumable upload URL
        // had previously completed successfully. In that case, the response to the initial */* PUT
        // will be a 201 Created with the new PhotoEntry. This is probably an edge case.
        LOG.info("Resumable upload is complete and successful.");

        return (PhotoEntry) ParseUtil.readEntry(new ParseSource(connection.getInputStream()));
      } else {
        // The IllegalArgumentException should be treated by the calling code as
        // something that is not recoverable, which is to say the resumable upload attempt
        // should be stopped.
        throw new IllegalArgumentException(String.format("HTTP POST to %s returned status %d (%s).",
            url.toString(), connection.getResponseCode(), connection.getResponseMessage()));
      }
    } catch (MalformedURLException e) {
      LOG.log(Level.WARNING, "", e);
     
      throw new IllegalArgumentException(e);
    } catch (IOException e) {
      LOG.log(Level.WARNING, "", e);
    } catch (ServiceException e) {
      LOG.log(Level.WARNING, "", e);
    }

    return null;
  }
}
TOP

Related Classes of com.google.ytd.picasa.PicasaApiHelper

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.