// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2015 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.scripts;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.io.File;
import org.json.JSONException;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Tool to generate component.json for extension.
*
* @author mouha.oumar@gmail.com (Mouhamadou O. Sall)
*/
public class ExternalComponentGenerator {
private static String externalComponentsDirPath;
private static String androidRuntimeClassDirPath;
private static String buildServerClassDirPath;
private static String externalComponentsTempDirPath;
private static Map<String, List<ExternalComponentInfo>> externalComponentsByPackage =
new TreeMap<String, List<ExternalComponentInfo>>();
/**
* The definitions of the arguments used by this script
*
* args[0]: the path to simple_component.json
* args[1]: the path to simple_component_build_info.json
* args[2]: the path to ExternalComponentAsset.dir: "${local.build.dir}/ExternalComponents"
* args[3]: the path to "${AndroidRuntime-class.dir}"
* args[4]: the path to /build/classes/BuildServer/files
* args[5]: the path to external componentsTemp directory
*/
public static void main(String[] args) throws IOException, JSONException {
String simple_component_json = readFile(args[0], Charset.defaultCharset());
String simple_component_build_info_json = readFile(args[1], Charset.defaultCharset());
externalComponentsDirPath = args[2];
androidRuntimeClassDirPath = args[3];
buildServerClassDirPath = args[4];
externalComponentsTempDirPath = args[5];
JSONArray simpleComponentDescriptors = new JSONArray(simple_component_json);
JSONArray simpleComponentBuildInfos = new JSONArray(simple_component_build_info_json);
Map<String, JSONObject> buildInfos = buildInfoAsMap(simpleComponentBuildInfos);
for (int i = 0; i < simpleComponentDescriptors.length(); i++) {
JSONObject componentDescriptor = (JSONObject) simpleComponentDescriptors.get(i);
if(componentDescriptor.get("external").toString().equals("true")) {
ExternalComponentInfo info = new ExternalComponentInfo(componentDescriptor, buildInfos.get(componentDescriptor.getString("type")));
if (!externalComponentsByPackage.containsKey(info.packageName)) {
externalComponentsByPackage.put(info.packageName, new ArrayList<ExternalComponentInfo>());
}
externalComponentsByPackage.get(info.packageName).add(info);
}
}
generateAllExtensions();
}
/**
* Container class to store information about an extension.
*/
private static class ExternalComponentInfo {
private String type;
private String packageName;
private String className;
private JSONObject descriptor;
private JSONObject buildInfo;
ExternalComponentInfo(JSONObject descriptor, JSONObject buildInfo) {
this.descriptor = descriptor;
this.buildInfo = buildInfo;
this.type = descriptor.optString("type");
this.packageName = type.substring(0, type.lastIndexOf('.'));
this.className = type.substring(type.lastIndexOf('.') + 1);
}
}
private static Map<String, JSONObject> buildInfoAsMap(JSONArray buildInfos) throws JSONException {
Map<String, JSONObject> result = new HashMap<String, JSONObject>();
for (int i = 0; i < buildInfos.length(); i++) {
JSONObject componentBuildInfo = buildInfos.getJSONObject(i);
result.put(componentBuildInfo.getString("type"), componentBuildInfo);
}
return result;
}
private static void generateAllExtensions() throws IOException, JSONException {
System.out.println("\nExtensions : Generating extensions");
for (Map.Entry<String, List<ExternalComponentInfo>> entry : externalComponentsByPackage.entrySet()) {
String logComponentType = "[" + entry.getKey() + "]";
System.out.println("\nExtensions : Generating files " + logComponentType);
generateExternalComponentDescriptors(entry.getKey(), entry.getValue());
for (ExternalComponentInfo info : entry.getValue()) {
copyIcon(entry.getKey(), info.type, info.descriptor);
}
generateExternalComponentBuildFiles(entry.getKey(), entry.getValue());
generateExternalComponentOtherFiles(entry.getKey());
}
}
private static void generateExternalComponentDescriptors(String packageName, List<ExternalComponentInfo> infos)
throws IOException, JSONException {
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (ExternalComponentInfo info : infos) {
if (!first) {
sb.append(',');
} else {
first = false;
}
sb.append(info.descriptor.toString(1));
}
sb.append(']');
String components = sb.toString();
String extensionDirPath = externalComponentsDirPath + File.separator + packageName;
new File(extensionDirPath).mkdirs();
FileWriter jsonWriter = null;
try {
jsonWriter = new FileWriter(extensionDirPath + File.separator + "components.json");
jsonWriter.write(components);
} catch(IOException e) {
e.printStackTrace();
} finally {
if (jsonWriter != null) {
jsonWriter.close();
}
}
}
private static void generateExternalComponentBuildFiles(String packageName, List<ExternalComponentInfo> extensions) throws IOException {
String extensionDirPath = externalComponentsDirPath + File.separator + packageName;
String extensionTempDirPath = externalComponentsTempDirPath + File.separator + packageName;
String extensionFileDirPath = extensionDirPath + File.separator + "files";
String extensionClassPath = packageName.replace('.', File.separatorChar);
String extensionTempClassDirPath = extensionTempDirPath + File.separator + extensionClassPath;
if (!new File(extensionTempClassDirPath).mkdirs()) {
throw new IOException("Unable to create temporary path for extension build");
}
copyRelatedExternalClasses(androidRuntimeClassDirPath, packageName, extensionTempClassDirPath);
JSONArray buildInfos = new JSONArray();
for (ExternalComponentInfo info : extensions) {
JSONObject componentBuildInfo = info.buildInfo;
JSONArray librariesNeeded = componentBuildInfo.getJSONArray("libraries");
for (int j = 0; j < librariesNeeded.length(); ++j) { // Copy Library files for Unjar and Jaring
String library = librariesNeeded.getString(j);
copyFile(buildServerClassDirPath + File.separator + library,
extensionTempDirPath + File.separator + library);
}
componentBuildInfo.put("libraries", new JSONArray()); //empty the libraries meta-data to avoid redundancy
buildInfos.put(componentBuildInfo);
}
// Create component_build_info.json
if (!new File(extensionFileDirPath).mkdirs()) {
throw new IOException("Unable to create path for component_build_info.json");
}
FileWriter extensionBuildInfoFile = new FileWriter(extensionFileDirPath + File.separator + "component_build_info.json");
try {
extensionBuildInfoFile.write(buildInfos.toString());
System.out.println("Extensions : Successfully created " + packageName + " build info file");
} catch (IOException e) {
e.printStackTrace();
} finally {
extensionBuildInfoFile.flush();
extensionBuildInfoFile.close();
}
}
private static void copyIcon(String packageName, String type, JSONObject componentDescriptor) throws IOException, JSONException {
String icon = componentDescriptor.getString("iconName");
if (icon.startsWith("http:") || icon.startsWith("https:")) {
// Icon will be loaded from the web
return;
}
String packagePath = packageName.replace('.', File.separatorChar);
File sourceDir = new File(externalComponentsDirPath + File.separator + ".." + File.separator + ".." + File.separator + "src" + File.separator + packagePath);
File image = new File(sourceDir, icon);
if (image.exists()) {
File dstIcon = new File(externalComponentsDirPath + File.separator + packageName + File.separator + icon);
File dstIconDir = dstIcon.getParentFile();
if (!dstIconDir.exists()) {
if (!dstIconDir.mkdirs()) {
throw new IOException("Unable to create directory " + dstIconDir);
}
}
System.out.println("Extensions : " + "Copying file " + image.getAbsolutePath());
copyFile(image.getAbsolutePath(), dstIcon.getAbsolutePath());
} else {
System.out.println("Extensions : Skipping missing icon " + icon);
}
}
private static void generateExternalComponentOtherFiles(String packageName) throws IOException {
String extensionDirPath = externalComponentsDirPath + File.separator + packageName;
// Create extension.properties
StringBuilder extensionPropertiesString = new StringBuilder();
extensionPropertiesString.append("type=external\n");
FileWriter extensionPropertiesFile = new FileWriter(extensionDirPath + File.separator + "extension.properties");
try {
extensionPropertiesFile.write(extensionPropertiesString.toString());
System.out.println("Extensions : Successfully created " + packageName + " extension properties file");
} catch (IOException e) {
e.printStackTrace();
} finally {
extensionPropertiesFile.flush();
extensionPropertiesFile.close();
}
}
/**
* Read a file and returns its content
*
* @param path the path of the file to be read
* @param encoding the encoding system
*/
private static String readFile(String path, Charset encoding) throws IOException{
byte[] encoded = Files.readAllBytes(Paths.get(path));
return new String(encoded, encoding);
}
/**
* Copy one file to another. If destination file does not exist, it is created.
*
* @param srcPath absolute path to source file
* @param dstPath absolute path to destination file
* @return {@code true} if the copy succeeds, {@code false} otherwise
*/
private static Boolean copyFile(String srcPath, String dstPath) {
try {
FileInputStream in = new FileInputStream(srcPath);
FileOutputStream out = new FileOutputStream(dstPath);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0 ) {
out.write(buf, 0, len);
}
in.close();
out.close();
}
catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Copy a compiled classes related to a given extension in his package folder
*
* @param srcPath the folder in which to check compiled classes
* @param extensionPackage the classpath of the extension
* @param destPath where the compiled classes will be copied
*/
private static void copyRelatedExternalClasses(final String srcPath, String extensionPackage,
final String destPath) throws IOException {
File srcFolder = new File(srcPath);
File[] files = srcFolder.listFiles();
if (files == null) {
return;
}
for (File fileEntry : files){
if (fileEntry.isFile()) {
if (isRelatedExternalClass(fileEntry.getAbsolutePath(), extensionPackage)) {
System.out.println("Extensions : " + "Copying file " +
getClassPackage(fileEntry.getAbsolutePath()).replace(".", File.separator)
+ File.separator + fileEntry.getName());
copyFile(fileEntry.getAbsolutePath(), destPath + File.separator + fileEntry.getName());
}
} else if (fileEntry.isDirectory()) {
copyRelatedExternalClasses(fileEntry.getAbsolutePath(), extensionPackage, destPath);
}
}
}
/**
* Returns true if a class is related to an external component
* Current implementation returns true for all files in the same package as that of the external component
* A better implementation is possible but might be more complex
* @param testClassAbsolutePath absolute path of the class file
* @param extensionPackage package of the external component
* @return {@code true} if the Java class file at {@code testClassAbsolutePath} is a member of
* {@code extensionPackage}, {@code false} otherwise
*/
private static boolean isRelatedExternalClass(final String testClassAbsolutePath, final String extensionPackage ) {
String componentPackagePath = extensionPackage.replace(".", File.separator);
String testClassPath = getClassPackage(testClassAbsolutePath);
testClassPath = testClassPath.replace(".", File.separator);
if (testClassPath.startsWith(componentPackagePath)) {
return true;
}
return false;
}
private static String getClassPackage(String classAbsolutePath) {
String parentPath = "/appinventor/components/build/classes/AndroidRuntime/";
parentPath = parentPath.replace("/", File.separator);
String componentPackage = classAbsolutePath.substring(classAbsolutePath.indexOf(parentPath) + parentPath.length());
componentPackage = componentPackage.substring(0, componentPackage.lastIndexOf(File.separator));
componentPackage = componentPackage.replace(File.separator, ".");
return componentPackage;
}
}