package org.nativescript.staticbindinggenerator; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.jar.JarInputStream; import java.util.zip.ZipEntry; import org.apache.bcel.classfile.ClassParser; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; import org.apache.bcel.generic.Type; public class Generator { private static final String JAVA_EXT = ".java"; private static final String CLASS_EXT = ".class"; private static final String DEFAULT_PACKAGE_NAME = "com.tns.gen"; private final String outputDir; private final String[] libs; private final Map<String, JavaClass> classes; public Generator(String outputDir, String[] libs) throws IOException { this(outputDir, libs, false); } public Generator(String outputDir, String[] libs, boolean throwOnError) throws IOException { this.outputDir = outputDir; this.libs = libs; this.classes = readClasses(libs, throwOnError); } public void writeBindings(String filename) throws IOException { Binding[] bindings = generateBindings(filename); List<File> writenFiles = new ArrayList<File>(); for (Binding b : bindings) { if(!writenFiles.contains(b.getFile())) { writenFiles.add(b.getFile()); try (PrintStream ps = new PrintStream(b.getFile())) { ps.append(b.getContent()); } } else { throw new IOException("File already exists. This may lead to undesired behavior.\nPlease change the name of one of the extended classes.\n" + b.getFile()); } } } public Binding[] generateBindings(String filename) throws IOException { List<DataRow> rows = getRows(filename); Binding[] generatedFiles = processRows(rows); return generatedFiles; } public Binding generateBinding(DataRow dataRow, HashSet interfaceNames) { JavaClass clazz = classes.get(dataRow.getBaseClassname()); boolean hasSpecifiedName = !dataRow.getFilename().isEmpty(); String packageName = hasSpecifiedName ? getBaseDir(dataRow.getFilename()) : (DEFAULT_PACKAGE_NAME + "." + clazz.getPackageName()); String baseDirPath = packageName.replace('.', '/'); File baseDir = new File(outputDir, baseDirPath); if (!baseDir.exists()) { boolean success = baseDir.mkdirs(); } String name; Boolean isInterface = clazz.isInterface(); if (hasSpecifiedName) { name = getSimpleClassname(dataRow.getFilename()); } else { name = getSimpleClassname(clazz.getClassName()); if (!isInterface) { name += dataRow.getSuffix(); } } if (isInterface && interfaceNames.contains(name)) { return null; } else if (isInterface) { interfaceNames.add(name); } String normalizedName = getNormalizedName(name); Writer w = new Writer(); writeBinding(w, dataRow, clazz, packageName, name); String classname = dataRow.getFilename(); return new Binding(new File(baseDir, normalizedName + JAVA_EXT), w.toString(), classname); } public Binding generateBinding(DataRow dataRow) { return generateBinding(dataRow, new HashSet()); } private List<DataRow> getRows(String filename) throws IOException { List<DataRow> rows = new ArrayList<DataRow>(); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream(filename))); String line; while ((line = br.readLine()) != null) { DataRow row = new DataRow(line); rows.add(row); } } finally { if (br != null) { br.close(); } } return rows; } private Binding[] processRows(List<DataRow> rows) throws IOException { ArrayList<Binding> bindings = new ArrayList<>(); HashSet interfaceNames = new HashSet(); for (DataRow dataRow : rows) { String classname = dataRow.getBaseClassname(); boolean isJavaExtend = classes.containsKey(classname); if (isJavaExtend) { // System.out.println("SBG: DataRow: baseClassName: " + classname + ", suffix: " + dataRow.getSuffix() + ", interfaces: " + String.join(", ", Arrays.asList(dataRow.getInterfaces())) + ", jsFileName: " + dataRow.getJsFilename()); Binding binding = generateBinding(dataRow, interfaceNames); if (binding != null) { bindings.add(binding); } } } return bindings.toArray(new Binding[bindings.size()]); } private void collectImplementedInterfaces(String[] interfaces, JavaClass clazz) { String[] implInterfaces = interfaces; if (implInterfaces.length > 0 && !implInterfaces[0].isEmpty()) { // since JavaClass.setInterfaceNames overwrites all interfaces, we need to preserve any // original interfaces implemented by the class/interface ArrayList<String> interfacesList = new ArrayList<String>(); String[] nativeInterfaces = clazz.getInterfaceNames(); if (nativeInterfaces.length > 0) { for (String intface : nativeInterfaces) { interfacesList.add(intface); } } for (String intface : implInterfaces) { interfacesList.add(intface); } String[] arr = interfacesList.toArray(new String[interfacesList.size()]); clazz.setInterfaceNames(arr); } } private String getNormalizedName(String filename) { StringBuilder sb = new StringBuilder(filename.length()); for (char ch : filename.toCharArray()) { char c = Character.isJavaIdentifierPart(ch) ? ch : '_'; sb.append(c); } String name = sb.toString(); return name; } private Map<String, List<Method>> getPublicApi(JavaClass clazz) { Map<String, List<Method>> api = new HashMap<String, List<Method>>(); JavaClass currentClass = clazz; String clazzName = clazz.getClassName(); while (true) { String currentClassname = currentClass.getClassName(); boolean shouldCollectMethods = !(!clazzName.equals(currentClassname) && currentClass.isAbstract()); if (shouldCollectMethods || currentClass.isInterface()) { // Don't include abstract parent class's methods to avoid compilation issues // where a child class has 2 methods, of the same type, with just a // return type/parameter type that differs by being of a superclass of the class being extended. // see Test testCanCompileBindingClassExtendingAnExtendedClassWithMethodsWithTheSameSignature List<Method> methods = new ArrayList<Method>(); for (Method m : currentClass.getMethods()) { methods.add(m); } // System.out.println("SBG: getPublicApi:collectInterfaceMethods classname: " + currentClassname); collectInterfaceMethods(clazz, methods); for (Method m : methods) { if (!m.isSynthetic() && (m.isPublic() || m.isProtected()) && !m.isStatic()) { String name = m.getName(); List<Method> methodGroup; if (api.containsKey(name)) { methodGroup = api.get(name); } else { methodGroup = new ArrayList<Method>(); api.put(name, methodGroup); } boolean found = false; String methodSig = m.getSignature(); for (Method m1 : methodGroup) { found = methodSig.equals(m1.getSignature()); if (found) { break; } } if (!found) { methodGroup.add(m); } } } } if (currentClassname.equals("java.lang.Object")) { break; } else { String superClassName = currentClass.getSuperclassName(); currentClass = classes.get(superClassName.replace('$', '.')); } } return api; } private Map<String, JavaClass> readClasses(String[] libs, boolean throwOnError) throws FileNotFoundException, IOException { Map<String, JavaClass> map = new HashMap<String, JavaClass>(); if (libs != null) { for (String lib : libs) { File f = new File(lib); Map<String, JavaClass> classes = f.isFile() ? readJar(lib, throwOnError) : readDir(lib, throwOnError); map.putAll(classes); } } return map; } private Map<String, JavaClass> readJar(String path, boolean throwOnError) throws FileNotFoundException, IOException { Map<String, JavaClass> classes = new HashMap<String, JavaClass>(); JarInputStream jis = null; try { String name = null; jis = new JarInputStream(new FileInputStream(path)); for (ZipEntry ze = jis.getNextEntry(); ze != null; ze = jis.getNextEntry()) { try { name = ze.getName(); if (name.endsWith(CLASS_EXT)) { name = name.substring(0, name.length() - CLASS_EXT.length()).replace('/', '.').replace('$', '.'); ClassParser cp = new ClassParser(jis, name); JavaClass clazz = cp.parse(); classes.put(name, clazz); } } catch (Exception e) { if (throwOnError) { throw new RuntimeException(e); } } } } finally { if (jis != null) { jis.close(); } } return classes; } private Map<String, JavaClass> readDir(String path, boolean throwOnError) throws FileNotFoundException, IOException { Map<String, JavaClass> classes = new HashMap<String, JavaClass>(); ArrayDeque<File> d = new ArrayDeque<File>(); d.add(new File(path)); while (!d.isEmpty()) { File cur = d.pollFirst(); File[] files = cur.listFiles(); for (File f: files) { if (f.isFile() && f.getName().endsWith(CLASS_EXT)) { ClassParser cp = new ClassParser(f.getAbsolutePath()); JavaClass clazz = cp.parse(); String name = clazz.getClassName(); name = name.replace('/', '.').replace('$', '.'); classes.put(name, clazz); } else if (f.isDirectory()) { d.addLast(f); } } } return classes; } private String getBaseDir(String classname) { int idx = classname.lastIndexOf('.'); String baseDir = classname.substring(0, idx); return baseDir; } private String getSimpleClassname(String classname) { int idx = classname.lastIndexOf('.'); String name = classname.substring(idx + 1, classname.length()).replace("$", "_"); return name; } private String getFullMethodSignature(Method m) { String sig = m.getName() + m.getSignature(); return sig; } private void writeBinding(Writer w, DataRow dataRow, JavaClass clazz, String packageName, String name) { String[] implInterfaces = dataRow.getInterfaces(); collectImplementedInterfaces(implInterfaces, clazz); Map<String, List<Method>> api = getPublicApi(clazz); w.writeln("package " + packageName + ";"); w.writeln(); boolean isApplicationClass = isApplicationClass(clazz, classes); if (isApplicationClass && !packageName.equals("com.tns")) { w.writeln("import com.tns.RuntimeHelper;"); w.writeln("import com.tns.Runtime;"); w.writeln(); } boolean hasSpecifiedName = !dataRow.getFilename().isEmpty(); if (hasSpecifiedName) { w.writeln("@com.tns.JavaScriptImplementation(javaScriptFile = \"./" + dataRow.getJsFilename() + "\")"); } w.write("public class " + name); boolean isInterface = clazz.isInterface(); String extendKeyword = isInterface ? " implements " : " extends "; w.write(extendKeyword); w.write(clazz.getClassName().replace('$', '.')); if (!isInterface) { w.write(" implements"); w.write(" com.tns.NativeScriptHashCodeProvider"); if (implInterfaces.length > 0 && !implInterfaces[0].isEmpty()) { for (String intface : implInterfaces) { w.write(", " + intface); } } } w.writeln(" {"); if (isClassApplication(clazz)) { //get instance method w.write("\t"); w.writeln("private static " + clazz.getClassName().replace('$', '.') + " thiz;"); w.writeln(); } boolean hasInitMethod = false; String[] methods = dataRow.getMethods(); for (String m : methods) { hasInitMethod = m.equals("init"); if (hasInitMethod) { break; } } boolean hasInitMethod2 = isApplicationClass ? false : hasInitMethod; writeConstructors(clazz, name, hasInitMethod2, isApplicationClass, w); if (isInterface) { Set<String> objectMethods = new HashSet<String>(); for (Method objMethod : classes.get("java.lang.Object").getMethods()) { if (!objMethod.isStatic() && (objMethod.isPublic() || objMethod.isProtected())) { String sig = getFullMethodSignature(objMethod); objectMethods.add(sig); } } Set<Method> notImplementedObjectMethods = new HashSet<Method>(); Method[] currentIfaceMethods = clazz.getMethods(); ArrayList<Method> ifaceMethods = new ArrayList<Method>(); for (Method m : currentIfaceMethods) { if (!m.getName().equals("<clinit>")) { ifaceMethods.add(m); } } ArrayDeque<String> interfaceNames = new ArrayDeque<String>(); for (String iname : clazz.getInterfaceNames()) { interfaceNames.add(iname); } while (!interfaceNames.isEmpty()) { String iname = interfaceNames.pollFirst(); JavaClass iface = classes.get(iname.replace('$', '.')); for (String iname2 : iface.getInterfaceNames()) { interfaceNames.add(iname2.replace('$', '.')); } Method[] ims = iface.getMethods(); for (Method m : ims) { ifaceMethods.add(m); } } Set<String> methodOverrides = new HashSet<String>(); for (String methodName : dataRow.getMethods()) { methodOverrides.add(methodName); } for (Method ifaceMethod : ifaceMethods) { if (!ifaceMethod.isStatic()) { String sig = getFullMethodSignature(ifaceMethod); if (objectMethods.contains(sig) && !methodOverrides.contains(ifaceMethod.getName())) { notImplementedObjectMethods.add(ifaceMethod); } } } for (Method m : ifaceMethods) { if (!notImplementedObjectMethods.contains(m)) { writeMethodBody(m, w, isApplicationClass, true); } } } else { List<Method> interfaceMethods = new ArrayList<Method>(); collectInterfaceMethods(clazz, interfaceMethods); for (String methodName : dataRow.getMethods()) { if (api.containsKey(methodName)) { List<Method> methodGroup = api.get(methodName); for (Method m : methodGroup) { boolean isInterfaceMethod = false; if (interfaceMethods.contains(m)) { isInterfaceMethod = true; } writeMethodBody(m, w, isApplicationClass, isInterfaceMethod); } } } } if (!isInterface) { writeHashCodeProviderImplementationMethods(w); } if (isClassApplication(clazz)) { w.write("\t"); w.write("public static "); w.write(clazz.getClassName().replace('$', '.')); w.writeln(" getInstance() {"); w.write("\t\t"); w.writeln("return thiz;"); w.write("\t"); w.writeln("}"); } w.writeln("}"); } private boolean isClassApplication(JavaClass clazz) { String className = clazz.getClassName(); return className.equals("android.app.Application") || className.equals("android.support.multidex.MultiDexApplication") || className.equals("android.test.mock.MockApplication"); } private void writeMethodBody(Method m, Writer w, boolean isApplicationClass, boolean isInterfaceMethod) { String visibility = m.isPublic() ? "public" : "protected"; w.write("\t"); w.write(visibility); w.write(" "); writeType(m.getReturnType(), w); w.write(" "); w.write(m.getName()); writeMethodSignature(m, w); w.write(" "); writeThrowsClause(m, w); w.writeln(" {"); writeMethodBody(m, false, isApplicationClass, w, isInterfaceMethod); w.writeln("\t}"); w.writeln(); } private void writeHashCodeProviderImplementationMethods(Writer w) { w.write("\t"); w.writeln("public boolean equals__super(java.lang.Object other) {"); w.write("\t\t"); w.writeln("return super.equals(other);"); w.write("\t"); w.writeln("}"); w.writeln(); w.write("\t"); w.writeln("public int hashCode__super() {"); w.write("\t\t"); w.writeln("return super.hashCode();"); w.write("\t"); w.writeln("}"); w.writeln(); } private void writeMethodSignature(Method m, Writer w) { w.write('('); Type[] args = m.getArgumentTypes(); for (int i = 0; i < args.length; i++) { if (i > 0) { w.write(", "); } writeType(args[i], w); w.write(" param_"); w.write(i); } w.write(')'); } private void writeThrowsClause(Method m, Writer w) { } private void writeConstructors(JavaClass clazz, String classname, boolean hasInitMethod, boolean isApplicationClass, Writer w) { boolean isInterface = clazz.isInterface(); if (isInterface) { w.write("\tpublic "); w.write(classname); w.writeln("() {"); w.writeln("\t\tcom.tns.Runtime.initInstance(this);"); w.writeln("\t}"); w.writeln(); } else { List<Method> ctors = new ArrayList<Method>(); for (Method m : clazz.getMethods()) { if (m.getName().equals("<init>")) { ctors.add(m); } } for (Method c : ctors) { if (c.isPublic() || c.isProtected()) { String visibility = c.isPublic() ? "public" : "protected"; w.write("\t"); w.write(visibility); w.write(" "); w.write(classname); writeMethodSignature(c, w); w.writeln("{"); w.write("\t\tsuper("); Type[] ctorArgs = c.getArgumentTypes(); for (int i = 0; i < ctorArgs.length; i++) { if (i > 0) { w.write(", "); } w.write("param_"); w.write(i); } w.writeln(");"); if (!isApplicationClass) { w.writeln("\t\tcom.tns.Runtime.initInstance(this);"); } if (hasInitMethod) { writeMethodBody(c, true, false, w, false); } if (isClassApplication(clazz)) { //get instance method w.write("\t\t"); w.writeln("thiz = this;"); } w.writeln("\t}"); w.writeln(); } } } } private void writeMethodBody(Method m, boolean isConstructor, boolean isApplicationClass, Writer w, boolean isInterfaceMethod) { if (m.getName().equals("onCreate") && isApplicationClass) { w.writeln("\t\tcom.tns.Runtime runtime = RuntimeHelper.initRuntime(this);"); } if (isApplicationClass && !isInterfaceMethod) { w.writeln("\t\tif (!Runtime.isInitialized()) {"); boolean isVoid = m.getReturnType().equals(Type.VOID); w.write("\t\t\t"); if (!isVoid) { w.write("return "); } w.write("super." + m.getName() + "("); int paramCount = m.getArgumentTypes().length; for (int i = 0; i < paramCount; i++) { if (i > 0) { w.write(", "); } w.write("param_" + i); } w.writeln(");"); if (isVoid) { w.writeln("\t\t\treturn;"); } w.writeln("\t\t}"); } Type[] args = m.getArgumentTypes(); int argLen = args.length + (isConstructor ? 1 : 0); w.write("\t\tjava.lang.Object[] args = "); if (argLen == 0) { w.writeln("null;"); } else { w.write("new java.lang.Object["); w.write(argLen); w.writeln("];"); } for (int i = 0; i < args.length; i++) { w.write("\t\targs["); w.write(i); w.write("] = param_"); w.write(i); w.writeln(";"); } String name = isConstructor ? "init" : m.getName(); if (name.equals("init")) { w.write("\t\targs["); w.write(argLen - 1); w.write("] = "); w.write(isConstructor); w.writeln(";"); } w.write("\t\t"); Type ret = m.getReturnType(); if (!ret.equals(Type.VOID)) { w.write("return ("); writeType(ret, w); w.write(')'); } w.write("com.tns.Runtime.callJSMethod(this, \""); w.write(name); w.write("\", "); writeType(ret, w); w.writeln(".class, args);"); if (m.getName().equals("onCreate") && isApplicationClass) { w.writeln("\t\tif (runtime != null) {"); w.writeln("\t\t\truntime.run();"); w.writeln("\t\t}"); } } private void writeType(Type t, Writer w) { String type = t.toString().replace('$', '.'); w.write(type); } private void collectInterfaceMethods(JavaClass clazz, List<Method> methods) { JavaClass currentClass = clazz; while (true) { String currentClassname = currentClass.getClassName(); Queue<String> queue = new ArrayDeque<String>(); for (String name : clazz.getInterfaceNames()) { queue.add(name); } while (!queue.isEmpty()) { String ifaceName = queue.poll(); JavaClass currentInterface = classes.get(ifaceName.replace('$', '.')); Method[] ifaceMethods = currentInterface.getMethods(); for (Method m : ifaceMethods) { methods.add(m); } for (String name : currentInterface.getInterfaceNames()) { queue.add(name); } } if (currentClassname.equals("java.lang.Object")) { break; } else { String superClassName = currentClass.getSuperclassName(); currentClass = classes.get(superClassName.replace('$', '.')); } } } private boolean isApplicationClass(JavaClass clazz, Map<String, JavaClass> classes) { boolean isApplicationClass = false; String applicationClassname = "android.app.Application"; JavaClass currentClass = clazz; while (true) { String currentClassname = currentClass.getClassName(); isApplicationClass = currentClassname.equals(applicationClassname); if (isApplicationClass) { break; } if (currentClassname.endsWith("java.lang.Object")) { break; } String superClassName = currentClass.getSuperclassName(); currentClass = classes.get(superClassName.replace('$', '.')); } return isApplicationClass; } }