// -*- 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 com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsInputChannel;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.appinventor.common.version.AppInventorFeatures;
import com.google.appinventor.server.flags.Flag;
import com.google.appinventor.server.project.CommonProjectService;
import com.google.appinventor.server.project.youngandroid.YoungAndroidProjectService;
import com.google.appinventor.server.storage.StorageIo;
import com.google.appinventor.server.storage.StorageIoInstanceHolder;
import com.google.appinventor.shared.rpc.BlocksTruncatedException;
import com.google.appinventor.shared.rpc.InvalidSessionException;
import com.google.appinventor.shared.rpc.RpcResult;
import com.google.appinventor.shared.rpc.project.ChecksumedFileException;
import com.google.appinventor.shared.rpc.project.ChecksumedLoadFile;
import com.google.appinventor.shared.rpc.project.FileDescriptor;
import com.google.appinventor.shared.rpc.project.FileDescriptorWithContent;
import com.google.appinventor.shared.rpc.project.NewProjectParameters;
import com.google.appinventor.shared.rpc.project.ProjectRootNode;
import com.google.appinventor.shared.rpc.project.ProjectService;
import com.google.appinventor.shared.rpc.project.UserProject;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode;
import com.google.appinventor.shared.util.Base64Util;
import com.google.common.collect.Lists;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 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 ProjectServiceImpl extends OdeRemoteServiceServlet implements ProjectService {
private static final Logger LOG = Logger.getLogger(ProjectServiceImpl.class.getName());
private static final long serialVersionUID = -8316312003804169166L;
private final transient StorageIo storageIo = StorageIoInstanceHolder.INSTANCE;
// RPC implementation for YoungAndroid projects
private final transient YoungAndroidProjectService youngAndroidProject =
new YoungAndroidProjectService(storageIo);
private static final boolean DEBUG = Flag.createFlag("appinventor.debugging", false).get();
/**
* Creates a new project.
* @param projectType type of new project
* @param projectName name of new project
* @param params optional parameter (project type dependent)
*
* @return a {@link UserProject} for new project
*/
@Override
public UserProject newProject(String projectType, String projectName,
NewProjectParameters params) {
final String userId = userInfoProvider.getUserId();
long projectId = getProjectRpcImpl(userId, projectType).
newProject(userId, projectName, params);
return makeUserProject(userId, projectId);
}
/**
* Creates a new project from a zip file that is already stored
* on the server.
* @param projectName name of new project
* @param pathToZip path the to template's zip file
*
* @return a {@link UserProject} for new project
*/
@Override
public UserProject newProjectFromTemplate(String projectName, String pathToZip) {
//Window.alert("newProjectFromTemplate " + host + pathToZip);
// System.out.println("newProjectFromTemplate = " + host + pathToZip);
UserProject userProject = null;
try {
FileInputStream fis = new FileInputStream(pathToZip);
FileImporter fileImporter = new FileImporterImpl();
userProject = fileImporter.importProject(userInfoProvider.getUserId(), projectName, fis);
} catch (IOException e) {
LOG.log(Level.SEVERE, "I/O Error importing from template project", e);
} catch (FileImporterException e) {
LOG.log(Level.SEVERE, "FileImporterException Error importing from template project", e);
}
return userProject;
}
/**
* This service is passed a base64 encoded string representing the Zip file.
* It converts it to a byte array and imports the project using FileImporter.
*
* @see http://stackoverflow.com/questions/6409587/
* generating-an-inline-image-with-java-gwt/6495356#6495356
*/
@Override
public UserProject newProjectFromExternalTemplate(String projectName, String zipData) {
System.out.println(">>>>> ProjectService newProjectFromExternalTemplate name = " + projectName);
UserProject userProject = null;
// Convert base64 string to byte[]
// NOTE: GWT's Base64Utils uses a non-standard algorithm.
// @see: https://code.google.com/p/google-web-toolkit/issues/detail?id=3880
byte[] binData = null;
binData = Base64Util.decode(zipData);
// Import the project
ByteArrayInputStream bais = null;
FileImporter fileImporter = new FileImporterImpl();
try {
bais = new ByteArrayInputStream(binData);
userProject = fileImporter.importProject(userInfoProvider.getUserId(),
projectName, bais);
} catch (FileNotFoundException e) { // Create a new empty project if no Zip
LOG.log(Level.SEVERE, "File Not Found importing from template project (external)", e);
} catch (IOException e) {
LOG.log(Level.SEVERE, "I/O Error importing from template project (external)", e);
} catch (FileImporterException e) {
LOG.log(Level.SEVERE, "FileImporterException Error importing from template project (external)", e);
}
return userProject;
}
/**
* Reads the template data from a JSON File
* @param pathToTemplatesDir pathname of the templates directory which may contain
* 0 or more template instances, each of which consists of a JSON file describing
* the template, plus a zip file and image files.
*
* @return A json-formatted String consisting of an array of template objects
*/
@Override
public String retrieveTemplateData(String pathToTemplatesDir) {
String json = "[";
File templatesRepository = new File(pathToTemplatesDir);
File templateFolder[] = templatesRepository.listFiles();
for (File file: templateFolder) {
String templateName = file.getName();
if (file.isDirectory()) { // Should be a template folder
File templateFiles[] = file.listFiles();
for (File f: templateFiles) {
if (f.isFile() && f.getName().equals(templateName + ".json")) {
try {
BufferedReader in = new BufferedReader(
new FileReader(pathToTemplatesDir + "/" + templateName + "/" + templateName + ".json"));
json += in.readLine() + ", ";
} catch (IOException e) {
LOG.log(Level.SEVERE, "I/O Exception reading template json file", e);
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(), null,
new IllegalArgumentException("Cannot Read Internal Project Template"));
}
}
}
}
}
return json + "]";
}
/**
* Copies a project with a new name.
* @param oldProjectId old project ID
* @param newName new project name
*
* @return a {@link UserProject} for new project
*/
@Override
public UserProject copyProject(long oldProjectId, String newName){
final String userId = userInfoProvider.getUserId();
long projectId = getProjectRpcImpl(userId, oldProjectId).
copyProject(userId, oldProjectId, newName);
return makeUserProject(userId, projectId);
}
/**
* Deletes a project.
* @param projectId project ID
*/
@Override
public void deleteProject(long projectId) {
final String userId = userInfoProvider.getUserId();
getProjectRpcImpl(userId, projectId).deleteProject(userId, projectId);
}
/**
* On publish this sets the project's gallery id
* @param projectId project ID
* @param galleryId gallery ID
*/
public void setGalleryId(long projectId, long galleryId) {
final String userId = userInfoProvider.getUserId();
getProjectRpcImpl(userId, projectId).setGalleryId(userId, projectId, galleryId);
}
/**
* Returns an array with project IDs.
*
* @return IDs of projects found by the back-end
*/
@Override
public long[] getProjects() {
List<Long> projects = storageIo.getProjects(userInfoProvider.getUserId());
long[] projectIds = new long[projects.size()];
int i = 0;
for (Long project : projects) {
projectIds[i++] = project;
}
return projectIds;
}
/**
* Returns a list with pairs of project id and name.
*
* @return list of pairs of project IDs names found by backend
*/
@Override
public List<UserProject> getProjectInfos() {
String userId = userInfoProvider.getUserId();
List<Long> projectIds = storageIo.getProjects(userId);
return makeUserProjects(userId, projectIds);
}
/**
* Returns the root node for the given project.
* @param projectId project ID as received by {@link #getProjects()}
*
* @return root node of project
*/
@Override
public ProjectRootNode getProject(long projectId) {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).getRootNode(userId, projectId);
}
/**
* Returns a string with the project settings.
* @param projectId project ID
*
* @return settings
*/
@Override
public String loadProjectSettings(long projectId) {
String userId = userInfoProvider.getUserId();
return storageIo.loadProjectSettings(userId, projectId);
}
/**
* Stores a string with the project settings.
* @param sessionId session id
* @param projectId project ID
* @param settings project settings
*/
@Override
public void storeProjectSettings(String sessionId, long projectId, String settings) throws InvalidSessionException {
validateSessionId(sessionId);
String userId = userInfoProvider.getUserId();
getProjectRpcImpl(userId, projectId).storeProjectSettings(userId, projectId, settings);
}
/**
* Deletes a file in the given project.
* @param sessionId session id
* @param projectId project ID
* @param fileId ID of file to delete
* @return modification date for project
*/
@Override
public long deleteFile(String sessionId, long projectId, String fileId) throws InvalidSessionException {
validateSessionId(sessionId);
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).deleteFile(userId, projectId, fileId);
}
/**
* Deletes all files that are contained directly in the given directory. Files
* in subdirectories are not deleted.
* @param sessionId session id
* @param projectId project ID
* @param directory path of the directory
* @return modification date for project
*/
@Override
public long deleteFiles(String sessionId, long projectId, String directory) throws InvalidSessionException {
validateSessionId(sessionId);
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).deleteFiles(userId, projectId,
directory);
}
/**
* Deletes all files and folders that are contained inside the given directory. The given directory itself is deleted.
* @param sessionId session id
* @param projectId project ID
* @param directory path of the directory
* @return modification date for project
*/
@Override
public long deleteFolder(String sessionId, long projectId, String directory) throws InvalidSessionException {
validateSessionId(sessionId);
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).deleteFolder(userId, projectId,
directory);
}
/**
* Loads the file information associated with a node in the project tree. The
* actual return value depends on the file kind. Source (text) files should
* typically return their contents. Image files will be more likely to return
* the URL that the browser can find them at.
*
* @param projectId project ID
* @param fileId project node whose source should be loaded
*
* @return implementation dependent
*/
@Override
public String load(long projectId, String fileId) {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).load(userId, projectId, fileId);
}
/**
* Loads the file information associated with a node in the project tree. The
* actual return value depends on the file kind. Source (text) files should
* typically return their contents. Image files will be more likely to return
* the URL that the browser can find them at.
*
* This version returns a ChecksumedLoadFile which contains the file content
* and a MD5 checksum.
*
* @param projectId project ID
* @param fileId project node whose source should be loaded
*
* @return implementation dependent
*/
@Override
public ChecksumedLoadFile load2(long projectId, String fileId) throws ChecksumedFileException {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).load2(userId, projectId, fileId);
}
/**
* Attempt to record the project Id and error message when we detect a corruption
* while loading a project.
*
* @param projectId project id
* @param message Error message from the thrown exception
*
*/
@Override
public void recordCorruption(long projectId, String fileId, String message) {
final String userId = userInfoProvider.getUserId();
getProjectRpcImpl(userId, projectId).recordCorruption(userId, projectId, fileId, message);
}
/**
* Loads the file information associated with a node in the project tree. The
* actual return value is the raw file contents.
*
* @param projectId project ID
* @param fileId project node whose source should be loaded
*
* @return raw file content
*/
@Override
public byte [] loadraw(long projectId, String fileId) {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).loadraw(userId, projectId, fileId);
}
/**
* Loads the file information associated with a node in the project tree. The
* actual return value is the raw file contents encoded as base64.
*
* @param projectId project ID
* @param fileId project node whose source should be loaded
*
* @return raw file content as base 64
*/
@Override
public String loadraw2(long projectId, String fileId) {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).loadraw2(userId, projectId, fileId);
}
/**
* Loads the contents of multiple files.
*
* @param files list containing file descriptor of files to be loaded
* @return list containing file descriptors and their associated content
*/
@Override
public List<FileDescriptorWithContent> load(List<FileDescriptor> files) {
List<FileDescriptorWithContent> result = Lists.newArrayList();
final String userId = userInfoProvider.getUserId();
for (FileDescriptor file : files) {
long projectId = file.getProjectId();
String fileId = file.getFileId();
result.add(new FileDescriptorWithContent(
projectId, fileId,
getProjectRpcImpl(userId, projectId).load(userId, projectId, fileId)));
}
return result;
}
/**
* Saves the content of the file associated with a node in the project tree.
*
* @param sessionId session id
* @param projectId project ID
* @param fileId project node whose source should be saved
* @param content content to be saved
* @return modification date for project
*
* @see #load(long, String)
*/
@Override
public long save(String sessionId, long projectId, String fileId, String content) throws InvalidSessionException {
validateSessionId(sessionId);
// Log parameters except for content
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).save(userId, projectId, fileId,
content);
}
/**
* Saves the content of the file associated with a node in the project tree.
* This version takes a "force" argument which if false will result in an
* exception of a trivial (empty) blocks workspace is attempted to be saved
*
* @param sessionId session id
* @param projectId project ID
* @param fileId project node whose source should be saved
* @param force whether to write an empty blocks workspace
* @param content content to be saved
* @return modification date for project
*
* @see #load(long, String)
*/
@Override
public long save2(String sessionId, long projectId, String fileId, boolean force, String content) throws InvalidSessionException,
BlocksTruncatedException {
validateSessionId(sessionId);
// Log parameters except for content
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).save2(userId, projectId, fileId, force,
content);
}
/**
* Saves the contents of multiple files.
*
* @param sessionId session id
* @param filesAndContent list containing file descriptors and their
* associated content
* @return modification date for last modified project of list
*/
@Override
public long save(String sessionId, List<FileDescriptorWithContent> filesAndContent) throws InvalidSessionException,
BlocksTruncatedException {
validateSessionId(sessionId);
final String userId = userInfoProvider.getUserId();
long date = 0;
for (FileDescriptorWithContent fileAndContent : filesAndContent) {
long projectId = fileAndContent.getProjectId();
date = getProjectRpcImpl(userId, projectId).
save(userId, projectId, fileAndContent.getFileId(), fileAndContent.getContent());
}
return date;
}
@Override
public RpcResult screenshot(String sessionId, long projectId, String fileId, String content)
throws InvalidSessionException {
validateSessionId(sessionId);
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).screenshot(userId, projectId, fileId,
content);
}
/**
* Invokes a build command for the project on the back-end.
*
* @param projectId project ID
* @param target build target (optional, implementation dependent)
*
* @return results of build
*/
@Override
public RpcResult build(long projectId, String nonce, String target) {
// Dispatch
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).build(
userInfoProvider.getUser(), projectId, nonce, target);
}
/**
* Gets the result of a build command for the project.
*
* @param projectId project ID
* @param target build target (optional, implementation dependent)
*
* @return results of build. The following values may be in RpcResult.result:
* 0: Build is done and was successful
* 1: Build is done and was unsuccessful
* -1: Build is not yet done.
*/
@Override
public RpcResult getBuildResult(long projectId, String target) {
// Dispatch
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).getBuildResult(
userInfoProvider.getUser(), projectId, target);
}
/*
* Write the serialized response out to stdout. This is a very unusual thing
* to do, but it allows us to create a static file version of the response
* without deploying a servlet.
*
* Commented out by JIS 11/12/13
*/
@Override
protected void onAfterResponseSerialized(String serializedResponse) {
// System.out.println(serializedResponse); // COV_NF_LINE
}
private UserProject makeUserProject(String userId, long projectId) {
return storageIo.getUserProject(userId, projectId);
}
// Bulk fetch UserProjects -- efficiently get all project infos asked for
// using a minimum number of datastore API calls
private List<UserProject> makeUserProjects(String userId, List<Long> projectIds) {
return storageIo.getUserProjects(userId, projectIds);
}
/*
* Returns the RPC implementation for the given project type.
*/
private CommonProjectService getProjectRpcImpl(final String userId, long projectId) {
String projectType = storageIo.getProjectType(userId, projectId);
if (!projectType.isEmpty()) {
return getProjectRpcImpl(userId, projectType);
} else {
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(),
"user=" + userId + ", project=" + projectId,
new IllegalArgumentException("Can't find project " + projectId));
}
}
private CommonProjectService getProjectRpcImpl(final String userId, String projectType) {
if (projectType.equals(YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE)) {
return youngAndroidProject;
} else {
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(), null,
new IllegalArgumentException("Unknown project type:" + projectType));
}
}
@Override
public long addFile(long projectId, String fileId) {
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).addFile(userId, projectId, fileId);
}
/**
* This service is passed a URL to an aia file in GCS, of the form
* /gallery/apps/<galleryid>/aia
* It converts it to a byte array and imports the project using FileImporter.
* It also sets the attributionId of the project to point to the galleryID
* it is remixing.
*/
@Override
public UserProject newProjectFromGallery(String projectName, String galleryPath,
long galleryId) {
try {
GcsService fileService = GcsServiceFactory.createGcsService();
GcsFilename readableFile = new GcsFilename(Flag.createFlag("gallery.bucket", "").get(), galleryPath);
GcsInputChannel readChannel = fileService.openPrefetchingReadChannel(readableFile, 0, 16384);
if (DEBUG) {
LOG.log(Level.INFO, "#### in newProjectFromGallery, past readChannel");
}
InputStream gcsis = Channels.newInputStream(readChannel);
// ok, we don't want to send the gcs stream because it can time out as we
// process the zip. We need to copy to a byte buffer first, then send a bytestream
byte[] buffer = new byte[16384];
int bytesRead = 0;
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((bytesRead = gcsis.read(buffer)) != -1) {
bao.write(buffer, 0, bytesRead);
}
InputStream bais = new ByteArrayInputStream(bao.toByteArray());
if (DEBUG) {
LOG.log(Level.INFO, "#### in newProjectFromGallery, past newInputStream");
}
// close the gcs
readChannel.close();
// now use byte stream to process aia file
FileImporter fileImporter = new FileImporterImpl();
UserProject userProject = fileImporter.importProject(userInfoProvider.getUserId(),
projectName, bais);
if (DEBUG) {
LOG.log(Level.INFO, "#### in newProjectFromGallery, past importProject");
}
// set the attribution id of the project
storageIo.setProjectAttributionId(userInfoProvider.getUserId(), userProject.getProjectId(),galleryId);
//To-Do: this is a temperory fix for the error that getAttributionId before setAttributionId
userProject.setAttributionId(galleryId);
return userProject;
} catch (FileNotFoundException e) {
e.printStackTrace();
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(), galleryPath,
e);
} catch (IOException e) {
e.printStackTrace();
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(), galleryPath+":"+projectName,
e);
} catch (FileImporterException e) {
e.printStackTrace();
throw CrashReport.createAndLogError(LOG, getThreadLocalRequest(), galleryPath,
e);
}
}
@Override
public void log(String message) {
LOG.warning(message);
}
private void validateSessionId(String sessionId) throws InvalidSessionException {
String storedSessionId = userInfoProvider.getSessionId();
if (DEBUG) {
if (storedSessionId == null) {
LOG.info("storedSessionId is null");
} else {
LOG.info("storedSessionId = " + storedSessionId);
}
}
if (sessionId.equals("force")) { // If we are forcing our way -- no check
return;
}
if (!storedSessionId.equals(sessionId))
if (AppInventorFeatures.requireOneLogin()) {
throw new InvalidSessionException("A more recent login has occurred since we started. No further changes will be saved.");
}
}
}