// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.tools.cloudstorage.GcsFileOptions;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsInputChannel;
import com.google.appengine.tools.cloudstorage.GcsOutputChannel;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.appinventor.common.utils.StringUtils;
import com.google.appinventor.server.flags.Flag;
import com.google.appinventor.server.storage.GalleryStorageIo;
import com.google.appinventor.server.storage.GalleryStorageIoInstanceHolder;
import com.google.appinventor.shared.rpc.project.Email;
import com.google.appinventor.shared.rpc.project.GalleryApp;
import com.google.appinventor.shared.rpc.project.GalleryAppListResult;
import com.google.appinventor.shared.rpc.project.GalleryComment;
import com.google.appinventor.shared.rpc.project.GalleryModerationAction;
import com.google.appinventor.shared.rpc.project.GalleryReportListResult;
import com.google.appinventor.shared.rpc.project.GalleryService;
import com.google.appinventor.shared.rpc.project.GallerySettings;
import com.google.appinventor.shared.rpc.project.ProjectSourceZip;
import com.google.appinventor.shared.rpc.project.RawFile;
/**
* The implementation of the RPC service which runs on the server.
*
* <p>Note that this service must be state-less so that it can be run on
* multiple servers.
*
*/
public class GalleryServiceImpl extends OdeRemoteServiceServlet implements GalleryService {
private static final Logger LOG = Logger.getLogger(GalleryServiceImpl.class.getName());
private static final long serialVersionUID = -8316312003804169166L;
private final transient GalleryStorageIo galleryStorageIo =
GalleryStorageIoInstanceHolder.INSTANCE;
// fileExporter used to get the source code from project being published
private final FileExporter fileExporter = new FileExporterImpl();
private final GallerySettings settings;
public GalleryServiceImpl() {
String bucket = Flag.createFlag("gallery.bucket", "").get();
boolean galleryEnabled = Flag.createFlag("use.gallery",false).get();
String envirnment = SystemProperty.environment.value().toString();
String adminEmail = Flag.createFlag("gallery.admin.email", "").get();
settings = new GallerySettings(galleryEnabled, bucket, envirnment, adminEmail);
}
@Override
public GallerySettings loadGallerySettings() {
return settings;
}
/**
* Publishes a gallery app
* @param projectId id of the project being published
* @param projectName name of project
* @param title title of new gallery app
* @param description description of new gallery app
* @return a {@link GalleryApp} for new galleryApp
*/
@Override
public GalleryApp publishApp(long projectId, String title, String projectName, String description, String moreInfo, String credit) throws IOException {
final String userId = userInfoProvider.getUserId();
GalleryApp app = galleryStorageIo.createGalleryApp(title, projectName, description, moreInfo, credit, projectId, userId);
try {
storeAIA(app.getGalleryAppId(),projectId, projectName);
} catch (IOException e) {
deleteApp(app.getGalleryAppId());
throw e;
}
// see if there is a new image for the app. If so, its in cloud using projectId, need to move
// to cloud using gallery id
setGalleryAppImage(app);
// put meta data in search index
GallerySearchIndex.getInstance().indexApp(app);
return app;
}
/**
* update a gallery app
* @param app info about app being updated
* @param newImage true if the user has submitted a new image
*/
@Override
public void updateApp(GalleryApp app, boolean newImage) throws IOException {
updateAppSource(app.getGalleryAppId(),app.getProjectId(),app.getProjectName());
updateAppMetadata(app);
if (newImage)
setGalleryAppImage(app);
}
/**
* update a gallery app's meta data
* @param app info about app being updated
*
*/
@Override
public void updateAppMetadata(GalleryApp app) {
final String userId = userInfoProvider.getUserId();
galleryStorageIo.updateGalleryApp(app.getGalleryAppId(), app.getTitle(), app.getDescription(), app.getMoreInfo(), app.getCredit(), userId);
// put meta data in search index
GallerySearchIndex.getInstance().indexApp(app);
}
/**
* update a gallery app's source (aia)
* @param galleryId id of gallery app to be updated
* @param projectId id of project so we can grab source
* @param projectName name of project, this is name in new aia
*/
@Override
public void updateAppSource (long galleryId, long projectId, String projectName) throws IOException {
storeAIA(galleryId,projectId, projectName);
}
/**
* index all gallery apps (admin method)
* @param count the max number of apps to index
*/
@Override
public void indexAll(int count) {
List<GalleryApp> apps= getRecentApps(1,count).getApps();
for (GalleryApp app:apps) {
GallerySearchIndex.getInstance().indexApp(app);
}
}
/**
* Returns total number of galleryApps
* @return number of galleryApps
*/
@Override
public Integer getNumApps() {
return galleryStorageIo.getNumGalleryApps();
}
/**
* Returns a wrapped class which contains list of most recently
* updated galleryApps and total number of results in database
* @param start starting index
* @param count number of apps to return
* @return list of GalleryApps
*/
@Override
public GalleryAppListResult getRecentApps(int start,int count) {
return galleryStorageIo.getRecentGalleryApps(start,count);
}
/**
* Returns a wrapped class which contains list of featured gallery app
* @param start start index
* @param count count number
* @return list of gallery app
*/
public GalleryAppListResult getFeaturedApp(int start, int count){
return galleryStorageIo.getFeaturedApp(start, count);
}
/**
* Returns a wrapped class which contains list of tutorial gallery app
* @param start start index
* @param count count number
* @return list of gallery app
*/
public GalleryAppListResult getTutorialApp(int start, int count){
return galleryStorageIo.getTutorialApp(start, count);
}
/**
* check if app is featured already
* @param galleryId gallery id
* @return true if featured, otherwise false
*/
public boolean isFeatured(long galleryId){
return galleryStorageIo.isFeatured(galleryId);
}
/**
* check if app is tutorial already
* @param galleryId gallery id
* @return true if tutorial, otherwise false
*/
public boolean isTutorial(long galleryId){
return galleryStorageIo.isTutorial(galleryId);
}
/**
* mark an app as featured
* @param galleryId gallery id
* @return true if successful
*/
public boolean markAppAsFeatured(long galleryId){
return galleryStorageIo.markAppAsFeatured(galleryId);
}
/**
* mark an app as tutorial
* @param galleryId gallery id
* @return true if successful
*/
public boolean markAppAsTutorial(long galleryId){
return galleryStorageIo.markAppAsTutorial(galleryId);
}
/**
* Returns a wrapped class which contains a list of galleryApps
* by a particular developer and total number of results in database
* @param userId id of the developer
* @param start starting index
* @param count number of apps to return
* @return list of GalleryApps
*/
@Override
public GalleryAppListResult getDeveloperApps(String userId, int start,int count) {
return galleryStorageIo.getDeveloperApps(userId, start,count);
}
/**
* Returns a GalleryApp object for the given id
* @param galleryId gallery ID as received by
* {@link #getRecentGalleryApps()}
*
* @return gallery app object
*/
@Override
public GalleryApp getApp(long galleryId) {
return galleryStorageIo.getGalleryApp(galleryId);
}
/**
* Returns a wrapped class which contains a list of galleryApps and
* total number of results in database
* @param keywords keywords to search for
* @param start starting index
* @param count number of apps to return
* @return list of GalleryApps
*/
@Override
public GalleryAppListResult findApps(String keywords, int start, int count) {
return GallerySearchIndex.getInstance().find(keywords, start, count);
}
/**
* Returns a wrapped class which contains a list of most downloaded
* gallery apps and total number of results in database
* @param start starting index
* @param count number of apps to return
* @return list of GalleryApps
*/
@Override
public GalleryAppListResult getMostDownloadedApps(int start, int count) {
return galleryStorageIo.getMostDownloadedApps(start,count);
}
/**
* Returns a wrapped class which contains a list of most liked
* gallery apps and total number of results in database
* @param start starting index
* @param count number of apps to return
* @return list of GalleryApps
*/
@Override
public GalleryAppListResult getMostLikedApps(int start, int count) {
return galleryStorageIo.getMostLikedApps(start,count);
}
/**
* Deletes a new gallery app
* @param galleryId id of app to delete
*/
@Override
public void deleteApp(long galleryId) {
// get rid of comments and app from database
galleryStorageIo.deleteApp(galleryId);
// remove the search index entry
GallerySearchIndex.getInstance().unIndexApp(galleryId);
// remove its image/aia from cloud
deleteAIA(galleryId);
deleteImage(galleryId);
}
/**
* record fact that app was downloaded
* @param galleryId id of app that was downloaded
*/
@Override
public void appWasDownloaded(long galleryId) {
GallerySettings settings = loadGallerySettings();
galleryStorageIo.incrementDownloads(galleryId);
}
/**
* Returns the comments for an app
* @param galleryId gallery ID as received by
* {@link #getRecentGalleryApps()}
* @return a list of comments
*/
@Override
public List<GalleryComment> getComments(long galleryId) {
return galleryStorageIo.getComments(galleryId);
}
/**
* publish a comment for a gallery app
* @param galleryId the id of the app
* @param comment the comment
*/
@Override
public long publishComment(long galleryId, String comment) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.addComment(galleryId, userId, comment);
}
/**
* increase likes for a gallery app
* @param galleryId the id of the app
* @return num of like
*/
@Override
public int increaseLikes(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.increaseLikes(galleryId, userId);
}
/**
* decrease likes for a gallery app
* @param galleryId the id of the app
* @return num of like
*/
@Override
public int decreaseLikes(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.decreaseLikes(galleryId, userId);
}
/**
* get num of likes for a gallery app
* @param galleryId the id of the app
*/
@Override
public int getNumLikes(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.getNumLikes(galleryId);
}
/**
* check if an app is liked by a user
* @param galleryId the id of the app
*/
@Override
public boolean isLikedByUser(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.isLikedByUser(galleryId, userId);
}
/**
* salvage the gallery app by given galleryId
*/
@Override
public void salvageGalleryApp(long galleryId) {
galleryStorageIo.salvageGalleryApp(galleryId);
}
/**
* adds a report (flag) to a gallery app
* @param galleryId id of gallery app that was commented on
* @param report report
* @return the id of the new report
*/
@Override
public long addAppReport(GalleryApp app, String reportText) {
final String reporterId = userInfoProvider.getUserId();
String offenderId = app.getDeveloperId();
return galleryStorageIo.addAppReport(reportText, app.getGalleryAppId(), offenderId,reporterId);
}
/**
* gets recent reports
* @param start start index
* @param count number to retrieve
* @return the list of reports
*/
@Override
public GalleryReportListResult getRecentReports(int start, int count) {
return galleryStorageIo.getAppReports(start,count);
}
/**
* gets existing reports
* @param start start index
* @param count number to retrieve
* @return the list of reports
*/
@Override
public GalleryReportListResult getAllAppReports(int start, int count){
return galleryStorageIo.getAllAppReports(start,count);
}
/**
* check if an app is reprted by a user
* @param galleryId the id of the app
*/
@Override
public boolean isReportedByUser(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.isReportedByUser(galleryId, userId);
}
/**
* save attribution for a gallery app
* @param galleryId the id of the app
* @param attributionId the id of the attribution app
* @return num of like
*/
@Override
public long saveAttribution(long galleryId, long attributionId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.saveAttribution(galleryId, attributionId);
}
/**
* get the attribution id for a gallery app
* @param galleryId the id of the app
* @return attribution id
*/
@Override
public long remixedFrom(long galleryId) {
final String userId = userInfoProvider.getUserId();
return galleryStorageIo.remixedFrom(galleryId);
}
/**
* get the children ids of an app
* @param galleryId the id of the app
* @return list of children gallery app
*/
@Override
public List<GalleryApp> remixedTo(long galleryId) {
return galleryStorageIo.remixedTo(galleryId);
}
/**
* mark an report as resolved
* @param reportId the id of the app
*/
@Override
public boolean markReportAsResolved(long reportId, long galleryId) {
return galleryStorageIo.markReportAsResolved(reportId, galleryId);
}
/**
* deactivate app
* @param galleryId the id of the gallery app
*/
@Override
public boolean deactivateGalleryApp(long galleryId) {
return galleryStorageIo.deactivateGalleryApp(galleryId);
}
/**
* check if gallery app is Activated
* @param galleryId the id of the gallery app
*/
@Override
public boolean isGalleryAppActivated(long galleryId){
return galleryStorageIo.isGalleryAppActivated(galleryId);
}
/**
* store aia file on cloud server
* @param galleryId gallery id
* @param projectId project id
* @param projectName project name
*/
private void storeAIA(long galleryId, long projectId, String projectName) throws IOException {
final String userId = userInfoProvider.getUserId();
// build the aia file name using the ai project name and code stolen
// from DownloadServlet to normalize...
String aiaName = StringUtils.normalizeForFilename(projectName) + ".aia";
// grab the data for the aia file using code from DownloadServlet
RawFile aiaFile = null;
byte[] aiaBytes= null;
ProjectSourceZip zipFile = fileExporter.exportProjectSourceZip(userId,
projectId, true, false, aiaName, false, false, false, true);
aiaFile = zipFile.getRawFile();
aiaBytes = aiaFile.getContent();
LOG.log(Level.INFO, "aiaFile numBytes:"+aiaBytes.length);
// now stick the aia file into the gcs
//String galleryKey = GalleryApp.getSourceKey(galleryId);//String.valueOf(galleryId);
GallerySettings settings = loadGallerySettings();
String galleryKey = settings.getSourceKey(galleryId);
// setup cloud
GcsService gcsService = GcsServiceFactory.createGcsService();
//GcsFilename filename = new GcsFilename(GalleryApp.GALLERYBUCKET, galleryKey);
GcsFilename filename = new GcsFilename(settings.getBucket(), galleryKey);
GcsFileOptions options = new GcsFileOptions.Builder().mimeType("application/zip")
.acl("public-read").cacheControl("no-cache").addUserMetadata("title", aiaName).build();
GcsOutputChannel writeChannel = gcsService.createOrReplace(filename, options);
writeChannel.write(ByteBuffer.wrap(aiaBytes));
// Now finalize
writeChannel.close();
}
/**
* delete aia file based on given gallery id
* @param galleryId gallery id
*/
private void deleteAIA(long galleryId) {
try {
//String galleryKey = GalleryApp.getSourceKey(galleryId);
GallerySettings settings = loadGallerySettings();
String galleryKey = settings.getSourceKey(galleryId);
// setup cloud
GcsService gcsService = GcsServiceFactory.createGcsService();
//GcsFilename filename = new GcsFilename(GalleryApp.GALLERYBUCKET, galleryKey);
GcsFilename filename = new GcsFilename(settings.getBucket(), galleryKey);
gcsService.delete(filename);
} catch (IOException e) {
// TODO Auto-generated catch block
LOG.log(Level.INFO, "FAILED GCS delete");
e.printStackTrace();
}
}
private void deleteImage(long galleryId) {
try {
//String galleryKey = GalleryApp.getImageKey(galleryId);
GallerySettings settings = loadGallerySettings();
String galleryKey = settings.getSourceKey(galleryId);
// setup cloud
GcsService gcsService = GcsServiceFactory.createGcsService();
//GcsFilename filename = new GcsFilename(GalleryApp.GALLERYBUCKET, galleryKey);
GcsFilename filename = new GcsFilename(settings.getBucket(), galleryKey);
gcsService.delete(filename);
} catch (IOException e) {
// TODO Auto-generated catch block
LOG.log(Level.INFO, "FAILED GCS delete");
e.printStackTrace();
}
}
/**
* when an app is published/updated, we need to move the image
* that was temporarily uploaded into projects/projectid/image
* into the gallery image
* @param app gallery app
*/
private void setGalleryAppImage(GalleryApp app) {
// best thing would be if GCS has a mv op, we can just do that.
// don't think that is there, though, so for now read one and write to other
// First, read the file from projects name
boolean lockForRead = false;
//String projectImageKey = app.getProjectImageKey();
GallerySettings settings = loadGallerySettings();
String projectImageKey = settings.getProjectImageKey(app.getProjectId());
try {
GcsService gcsService = GcsServiceFactory.createGcsService();
//GcsFilename filename = new GcsFilename(GalleryApp.GALLERYBUCKET, projectImageKey);
GcsFilename filename = new GcsFilename(settings.getBucket(), projectImageKey);
GcsInputChannel readChannel = gcsService.openReadChannel(filename, 0);
InputStream gcsis = Channels.newInputStream(readChannel);
byte[] buffer = new byte[8000];
int bytesRead = 0;
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((bytesRead = gcsis.read(buffer)) != -1) {
bao.write(buffer, 0, bytesRead);
}
// close the project image file
readChannel.close();
// if image is greater than 200 X 200, it will be scaled (200 X 200).
// otherwise, it will be stored as origin.
byte[] oldImageData = bao.toByteArray();
byte[] newImageData;
ImagesService imagesService = ImagesServiceFactory.getImagesService();
Image oldImage = ImagesServiceFactory.makeImage(oldImageData);
//if image size is too big, scale it to a smaller size.
if(oldImage.getWidth() > 200 && oldImage.getHeight() > 200){
Transform resize = ImagesServiceFactory.makeResize(200, 200);
Image newImage = imagesService.applyTransform(resize, oldImage);
newImageData = newImage.getImageData();
}else{
newImageData = oldImageData;
}
// set up the cloud file (options)
// After publish, copy the /projects/projectId image into /apps/appId
//String galleryKey = app.getImageKey();
String galleryKey = settings.getImageKey(app.getGalleryAppId());
//GcsFilename outfilename = new GcsFilename(GalleryApp.GALLERYBUCKET, galleryKey);
GcsFilename outfilename = new GcsFilename(settings.getBucket(), galleryKey);
GcsFileOptions options = new GcsFileOptions.Builder().mimeType("image/jpeg")
.acl("public-read").cacheControl("no-cache").build();
GcsOutputChannel writeChannel = gcsService.createOrReplace(outfilename, options);
writeChannel.write(ByteBuffer.wrap(newImageData));
// Now finalize
writeChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
LOG.log(Level.INFO, "FAILED WRITING IMAGE TO GCS");
e.printStackTrace();
}
}
/**
* Send an email to user
* @param senderId id of user sending this email
* @param receiverId id of user receiving this email
* @param receiverEmail receiver of email
* @param title title of email
* @param body body of email
*/
@Override
public long sendEmail(String senderId, String receiverId, String receiverEmail, String title, String body) {
GallerySettings settings = loadGallerySettings();
return galleryStorageIo.sendEmail(senderId, receiverId, settings.getAdminEmail(), receiverEmail, title, body);
}
/**
* Get email based on emailId
* @param emailId id of the email
* @return Email email
*/
@Override
public Email getEmail(long emailId) {
return galleryStorageIo.getEmail(emailId);
}
/**
* check if ready to send app stats to user
* @param userId
* @param galleryId
* @param adminEmail
* @param currentHost
*/
public boolean checkIfSendAppStats(String userId, long galleryId, String adminEmail, String currentHost){
return galleryStorageIo.checkIfSendAppStats(userId, galleryId, adminEmail, currentHost);
}
/**
* Store moderation actions based on actionType
* @param reportId
* @param galleryId
* @param emailId
* @param moderatorId
* @param actionType
*/
public void storeModerationAction(long reportId, long galleryId, long emailId, String moderatorId, int actionType, String moderatorName, String emailPreview){
galleryStorageIo.storeModerationAction(reportId, galleryId, emailId, moderatorId, actionType, moderatorName, emailPreview);
}
/**
* Get moderation actions based on given reportId
* @param reportId
*/
public List<GalleryModerationAction> getModerationActions(long reportId){
return galleryStorageIo.getModerationActions(reportId);
}
/**
* It will return a dev server serving url for given image url
* @param url image url
*/
@Override
public String getBlobServingUrl(String url) {
BlobKey bk = BlobstoreServiceFactory.getBlobstoreService().createGsBlobKey(url);
String u = null;
try {
u = ImagesServiceFactory.getImagesService().getServingUrl(bk);
} catch (Exception IllegalArgumentException) {
LOG.info("Could not read blob");
}
return u;
}
}