// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2015-2017 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.appinventor.server.storage.StorageIo;
import com.google.appinventor.server.storage.StorageIoInstanceHolder;
import com.google.appinventor.shared.rpc.component.ComponentImportResponse;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidComponentNode;
import com.google.appinventor.shared.storage.StorageUtil;
import com.google.appinventor.shared.rpc.BlocksTruncatedException;
import com.google.appinventor.shared.rpc.component.ComponentService;
import com.google.appinventor.shared.rpc.project.FileNode;
import com.google.appinventor.shared.rpc.project.ProjectNode;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.json.JSONArray;
import org.json.JSONObject;
public class ComponentServiceImpl extends OdeRemoteServiceServlet
implements ComponentService {
private static final Logger LOG =
Logger.getLogger(ComponentServiceImpl.class.getName());
private final transient StorageIo storageIo = StorageIoInstanceHolder.INSTANCE;
private final FileImporter fileImporter = new FileImporterImpl();
@Override
public ComponentImportResponse importComponentToProject(String fileOrUrl, long projectId,
String folderPath) {
ComponentImportResponse response = new ComponentImportResponse(ComponentImportResponse.Status.FAILED);
if (isUnknownSource(fileOrUrl)) {
response.setStatus(ComponentImportResponse.Status.UNKNOWN_URL);
return response;
}
Map<String, byte[]> contents;
String fileNameToDelete = null;
try {
if (fileOrUrl.startsWith("__TEMP__")) {
fileNameToDelete = fileOrUrl;
contents = extractContents(storageIo.openTempFile(fileOrUrl));
} else {
URL compUrl = new URL(fileOrUrl);
contents = extractContents(compUrl.openStream());
}
importToProject(contents, projectId, folderPath, response);
return response;
} catch (FileImporterException e) {
throw CrashReport.createAndLogError(LOG, null,
collectImportErrorInfo(fileOrUrl, projectId), e);
} catch (IOException e) {
throw CrashReport.createAndLogError(LOG, null,
collectImportErrorInfo(fileOrUrl, projectId), e);
} finally {
if (fileNameToDelete != null) {
try {
storageIo.deleteTempFile(fileNameToDelete);
} catch (Exception e) {
throw CrashReport.createAndLogError(LOG, null,
collectImportErrorInfo(fileOrUrl, projectId), e);
}
}
}
}
@Override
public void renameImportedComponent(String fullyQualifiedName, String newName,
long projectId) {
String fileName = "assets/external_comps/" + fullyQualifiedName + "/component.json";
JSONObject compJson = new JSONObject(storageIo.downloadFile(
userInfoProvider.getUserId(), projectId, fileName, StorageUtil.DEFAULT_CHARSET));
compJson.put("name", newName);
try {
storageIo.uploadFile(projectId, fileName, userInfoProvider.getUserId(),
compJson.toString(2), StorageUtil.DEFAULT_CHARSET);
} catch (BlocksTruncatedException e) {
throw CrashReport.createAndLogError(LOG, null,
"Error renaming the short name of " + fullyQualifiedName + " to " +
newName + " in project " + projectId, e);
}
}
@Override
public void deleteImportedComponent(String fullyQualifiedName, long projectId) {
String directory = "assets/external_comps/" + fullyQualifiedName + "/";
for (String fileId : storageIo.getProjectSourceFiles(userInfoProvider.getUserId(), projectId)) {
if (fileId.startsWith(directory)) {
storageIo.deleteFile(userInfoProvider.getUserId(), projectId, fileId);
storageIo.removeSourceFilesFromProject(userInfoProvider.getUserId(), projectId, false, fileId);
}
}
}
private Map<String, byte[]> extractContents(InputStream inputStream)
throws IOException {
Map<String, byte[]> contents = new HashMap<String, byte[]>();
// assumption: the zip is non-empty
ZipInputStream zip = new ZipInputStream(inputStream);
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory()) continue;
ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
ByteStreams.copy(zip, contentStream);
contents.put(entry.getName(), contentStream.toByteArray());
}
zip.close();
return contents;
}
/**
* Updates the name of any components in newComponents with the name specified in oldComponents.
* This destructively modifies newComponents.
* @param oldComponents an array of component descriptions from the old component[s].json
* @param newComponents an array of component descriptions from the new component[s].json
* @return A mapping of component fully-qualified class names to presentation (UI) names
*/
private Map<String, String> applyRenames(JSONArray oldComponents, JSONArray newComponents) {
Map<String, String> types = new HashMap<>();
for (int i = 0; i < oldComponents.length(); i++) {
JSONObject component = oldComponents.getJSONObject(i);
types.put(component.getString("type"), component.getString("name"));
}
for (int i = 0; i < newComponents.length(); i++) {
JSONObject component = newComponents.getJSONObject(i);
String type = component.getString("type");
if (!types.containsKey(type)) {
types.put(type, component.getString("name"));
} else {
component.put("name", types.get(type));
}
}
return types;
}
/**
* Extract a mapping of component fully-quallyified class names to presentation (UI) names
* @param components an array of component descriptions
* @return A mapping of component fully-qualified class names to presentation (UI) names
*/
private Map<String, String> extractTypes(JSONArray components) {
Map<String, String> types = new HashMap<>();
for (int i = 0; i < components.length(); i++) {
JSONObject component = components.getJSONObject(i);
types.put(component.getString("type"), component.getString("name"));
}
return types;
}
private void importToProject(Map<String, byte[]> contents, long projectId,
String folderPath, ComponentImportResponse response) throws FileImporterException, IOException {
response.setStatus(ComponentImportResponse.Status.IMPORTED);
List<ProjectNode> compNodes = new ArrayList<ProjectNode>();
response.setProjectId(projectId);
List<String> sourceFiles = storageIo.getProjectSourceFiles(userInfoProvider.getUserId(), projectId);
Map<String, String> types = new TreeMap<>();
for (String name : contents.keySet()) {
String destination = folderPath + "/external_comps/" + name;
if (sourceFiles.contains(destination)) { // Check if source File already contains component files
// This is an upgrade, if it replaces old component files
response.setStatus(ComponentImportResponse.Status.UPGRADED);
JSONArray oldComponents, newComponents;
if (StorageUtil.basename(name).equals("component.json")) { // TODO : we need a more secure check
oldComponents = new JSONArray("[" + storageIo.downloadFile(userInfoProvider.getUserId(), projectId, destination, StorageUtil.DEFAULT_CHARSET));
newComponents = new JSONArray("[" + new String(contents.get(name), StorageUtil.DEFAULT_CHARSET) + "]");
types = applyRenames(oldComponents, newComponents);
// upgrade component.json to components.json
contents.remove(name);
name = name.substring(0, name.length() - "component.json".length()) + "components.json";
contents.put(name, newComponents.toString().getBytes(StorageUtil.DEFAULT_CHARSET));
} else if (StorageUtil.basename(name).equals("components.json")) {
oldComponents = new JSONArray(storageIo.downloadFile(userInfoProvider.getUserId(), projectId, destination, StorageUtil.DEFAULT_CHARSET));
newComponents = new JSONArray(new String(contents.get(name), StorageUtil.DEFAULT_CHARSET));
types = applyRenames(oldComponents, newComponents);
contents.put(name, newComponents.toString().getBytes(StorageUtil.DEFAULT_CHARSET));
}
} else if(StorageUtil.basename(name).equals("components.json")) {
String oldDestination = destination.substring(0, destination.length() - "components.json".length()) + "component.json";
if (sourceFiles.contains(oldDestination)) {
// old extension used component.json but new extension is components.json
JSONArray oldComponents, newComponents;
oldComponents = new JSONArray("[" + storageIo.downloadFile(userInfoProvider.getUserId(), projectId, destination, StorageUtil.DEFAULT_CHARSET) + "]");
newComponents = new JSONArray(new String(contents.get(name), StorageUtil.DEFAULT_CHARSET));
types = applyRenames(oldComponents, newComponents);
storageIo.deleteFile(userInfoProvider.getUserId(), projectId, oldDestination);
} else {
// new file
types = extractTypes(new JSONArray(new String(contents.get(name), StorageUtil.DEFAULT_CHARSET)));
}
} else if(StorageUtil.basename(name).equals("component.json")) {
String altDestination = destination.substring(0, destination.length() - "component.json".length()) + "components.json";
String arrayContent = "[" + new String(contents.get(name), StorageUtil.DEFAULT_CHARSET) + "]";
if (sourceFiles.contains(altDestination)) {
// potential downgrade? new extensions have components.json
types = applyRenames(new JSONArray(storageIo.downloadFile(userInfoProvider.getUserId(), projectId, destination, StorageUtil.DEFAULT_CHARSET)), new JSONArray(arrayContent));
// upgrade component.json to components.json
contents.remove(name);
contents.put(altDestination, arrayContent.getBytes(StorageUtil.DEFAULT_CHARSET));
name = altDestination;
} else {
// new file; force upgrade to components.json
types = extractTypes(new JSONArray(arrayContent));
// upgrade component.json to components.json
contents.remove(name);
contents.put(altDestination, arrayContent.getBytes(StorageUtil.DEFAULT_CHARSET));
name = altDestination;
}
}
FileNode fileNode = new YoungAndroidComponentNode(StorageUtil.basename(name), destination);
fileImporter.importFile(userInfoProvider.getUserId(), projectId,
destination, new ByteArrayInputStream(contents.get(name)));
compNodes.add(fileNode);
}
response.setComponentTypes(types);
response.setNodes(compNodes);
}
private String collectImportErrorInfo(String path, long projectId) {
return "Error importing " + path + " to project " + projectId;
}
private static boolean isUnknownSource(String url) {
// TODO: check if the url is from the market place
return false;
}
}