// -*- 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.project;
import com.google.appinventor.server.storage.StorageIo;
import com.google.appinventor.shared.rpc.BlocksTruncatedException;
import com.google.appinventor.shared.rpc.RpcResult;
import com.google.appinventor.shared.rpc.project.ChecksumedLoadFile;
import com.google.appinventor.shared.rpc.project.ChecksumedFileException;
import com.google.appinventor.shared.rpc.project.NewProjectParameters;
import com.google.appinventor.shared.rpc.project.ProjectRootNode;
import com.google.appinventor.shared.rpc.user.User;
import com.google.appinventor.shared.storage.StorageUtil;
import com.google.appinventor.shared.util.Base64Util;
import java.util.List;
/**
* The base class for classes that provide project services for a specific
* project type.
*
* @author lizlooney@google.com (Liz Looney)
*/
public abstract class CommonProjectService {
protected final String projectType;
protected final StorageIo storageIo;
protected CommonProjectService(String projectType, StorageIo storageIo) {
this.projectType = projectType;
this.storageIo = storageIo;
}
/**
* Stores the project settings.
*
* @param userId the user id
* @param projectId project ID
* @param settings project settings
*/
public void storeProjectSettings(String userId, long projectId, String settings) {
storageIo.storeProjectSettings(userId, projectId, settings);
}
/**
* Creates a new project.
*
* @param userId the user id
* @param projectName project name
* @param params optional parameters (project type dependent)
*
* @return new project ID
*/
public abstract long newProject(String userId, String projectName, NewProjectParameters params);
/**
* Copies a project with a new name.
*
* @param userId the user id
* @param oldProjectId old project ID
* @param newName new project name
*/
public abstract long copyProject(String userId, long oldProjectId, String newName);
/**
* Deletes a project.
*
* @param userId the user id
* @param projectId project ID as received by
*/
public void deleteProject(String userId, long projectId) {
storageIo.deleteProject(userId, projectId);
}
/**
* Sets the project's gallery id.
*
* @param userId the user id
* @param projectId project ID as received by
*/
public void setGalleryId(String userId, long projectId, long galleryId) {
storageIo.setProjectGalleryId(userId, projectId, galleryId);
}
/**
* Returns the project root node for the requested project.
*
* @param userId the user id
* @param projectId project ID as received by {@link
* com.google.appinventor.shared.rpc.project.ProjectService#getProjects()}
*
* @return root node of project
*/
public abstract ProjectRootNode getRootNode(String userId, long projectId);
/**
* Adds a file to the given project.
*
* @param userId the user id
* @param projectId project ID
* @param fileId ID of file to delete
* @return modification date for project
*/
public long addFile(String userId, long projectId, String fileId) {
List<String> sourceFiles = storageIo.getProjectSourceFiles(userId, projectId);
if (!sourceFiles.contains(fileId)) {
storageIo.addSourceFilesToProject(userId, projectId, false, fileId);
}
return storageIo.uploadRawFileForce(projectId, fileId, userId, new byte[0]);
}
/**
* Deletes a file in the given project.
*
* @param userId the user id
* @param projectId project ID
* @param fileId ID of file to delete
* @return modification date for project
*/
public long deleteFile(String userId, long projectId, String fileId) {
final long date = storageIo.deleteFile(userId, projectId, fileId);
storageIo.removeSourceFilesFromProject(userId, projectId, false, fileId);
return date;
}
/**
* Deletes all files that are contained directly in the given directory. Files
* in sub-packages are not deleted.
*
* @param userId the user id
* @param projectId project ID
* @param directory path of the directory
*/
public long deleteFiles(String userId, long projectId, String directory) {
// TODO(user): This is not efficient.
for (String fileId : storageIo.getProjectSourceFiles(userId, projectId)) {
if (fileId.startsWith(directory + '/') && fileId.indexOf('/', directory.length() + 1) == -1) {
storageIo.deleteFile(userId, projectId, fileId);
storageIo.removeSourceFilesFromProject(userId, projectId, false, fileId);
}
}
return storageIo.getProjectDateModified(userId, projectId);
}
/**
* Deletes all files and folders that are inside the given directory. The given directory itself is deleted.
* @param userId the user Id
* @param projectId project ID
* @param directoy path of the directory
*/
public long deleteFolder(String userId, long projectId, String directory) {
// TODO(user) : This is also not efficient
for (String fileId : storageIo.getProjectSourceFiles(userId, projectId)) {
if (fileId.startsWith(directory)) {
storageIo.deleteFile(userId, projectId, fileId);
storageIo.removeSourceFilesFromProject(userId, projectId, false, fileId);
}
}
return storageIo.getProjectDateCreated(userId, projectId);
}
/**
* 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 userId the user id
* @param projectId project root node ID
* @param fileId project node whose source should be loaded
*
* @return implementation dependent
*/
public String load(String userId, long projectId, String fileId) {
return storageIo.downloadFile(userId, projectId, fileId, StorageUtil.DEFAULT_CHARSET);
}
/**
* 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 object which includes the file
* content and a SHA-1 hash to validate file integrity accross the network.
*
* @param userId the user id
* @param projectId project root node ID
* @param fileId project node whose source should be loaded
*
* @return ChecksumedLoadFile object
*/
public ChecksumedLoadFile load2(String userId, long projectId, String fileId) throws ChecksumedFileException {
ChecksumedLoadFile retval = new ChecksumedLoadFile();
retval.setContent(storageIo.downloadFile(userId, projectId, fileId, StorageUtil.DEFAULT_CHARSET));
return retval;
}
/**
* Attempt to record the project Id and error message when we detect a corruption
* while loading a project.
*
* @param userId user id
* @param projectId project id
* @param message Error message from the thrown exception
*
*/
public void recordCorruption(String userId, long projectId, String fileId, String message) {
storageIo.recordCorruption(userId, projectId, fileId, message);
}
/**
* Loads the raw content of the associated file.
*
* @param userId the userid
* @param projectId the project root node ID
* @param fileId project node whose content is to be downloaded
* @return the file contents
*/
public byte[] loadraw(String userId, long projectId, String fileId) {
return storageIo.downloadRawFile(userId, projectId, fileId);
}
/**
* Loads the raw content of the associated file, base 64 encodes it
* and returns the resulting base64 encoded string.
*
* @param userId the userid
* @param projectId the project root node ID
* @param fileId project node whose content is to be downloaded
* @param the file contents encoded in base64
*/
public String loadraw2(String userId, long projectId, String fileId) {
byte [] filedata = storageIo.downloadRawFile(userId, projectId, fileId);
return Base64Util.encodeLines(filedata);
}
/**
* Saves the content of the file associated with a node in the project tree.
* This is a backwards compatible version that always sets force to true
* Its primary purpose is to ease the release transition. People running an
* older Ode at release time won't lose. At some point this function should go
* away and save2 renamed back to save (through stages).
*
* @param userId the user id
* @param projectId project root node ID
* @param fileId project node whose source should be loaded
* @param content content to be saved
* @return modification date for project
*
* @see com.google.appinventor.shared.rpc.project.ProjectService#save(String, long, String, String)
*/
public long save(String userId, long projectId, String fileId, String content) {
try {
return save2(userId, projectId, fileId, true, content);
} catch (BlocksTruncatedException e) {
// Won't happen because it isn't thrown when the force argument is true
// This is here just to keep the Java compiler happy. It isn't smart enough
// to know that the exception won't be thrown in this case.
return 0;
}
}
/**
* Saves the content of the file associated with a node in the project tree.
* if force is false, an error is thrown if an attempt is made to save a
* trivial (empty) blocks file workspace that had previously had contents.
*
* @param userId the user id
* @param projectId project root node ID
* @param fileId project node whose source should be loaded
* @param content content to be saved
* @return modification date for project
*
* @see com.google.appinventor.shared.rpc.project.ProjectService#save(String, long, String, String)
*/
public long save2(String userId, long projectId, String fileId, boolean force, String content) throws BlocksTruncatedException {
if (force) {
return storageIo.uploadFileForce(projectId, fileId, userId,
content, StorageUtil.DEFAULT_CHARSET);
} else {
return storageIo.uploadFile(projectId, fileId, userId,
content, StorageUtil.DEFAULT_CHARSET);
}
}
/**
* Saves a screenshot of a current blocks editor. This is called from the client side
* whenever the user leaves a blocks editor. The data is shipped to us in base64 encoding
* which we decode and then store in the project.
*
* @param userId user who owns the projectId
* @param projectId project id for the project
* @param fileId the filename to store the screenshot in
* @param content the base64 encoded content
*/
public RpcResult screenshot(String userId, long projectId, String fileId, String content) {
byte [] binContent = Base64Util.decodeLines(content);
try {
storageIo.uploadRawFile(projectId, fileId, userId, true, binContent);
} catch (BlocksTruncatedException e) {
// should never happen because force is set to true
}
return RpcResult.createSuccessfulRpcResult("", "");
}
/**
* Invokes a build command for the project.
*
* @param user the User that owns the {@code projectId}.
* @param projectId project id to be built
* @param nonce -- random string used to find finished APK
* @param target build target (optional, implementation dependent)
*
* @return build results
*/
public abstract RpcResult build(User user, long projectId, String nonce, String target);
/**
* Gets the result of a build command for the project.
*
* @param user the User that owns the {@code projectId}.
* @param projectId project id to be built
* @param target build target (optional, implementation dependent.
* @return build results. 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.
*/
public abstract RpcResult getBuildResult(User user, long projectId, String target);
}