blob: 03dba150c0c29c8089651dd0f86d14685cd1568a [file] [log] [blame]
package com.xtremelabs.robolectric;
import javassist.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
public class AndroidTranslator implements Translator {
/**
* IMPORTANT -- increment this number when the bytecode generated for modified classes changes
* so the cache file can be invalidated.
*/
public static final int CACHE_VERSION = 1;
private static final List<AndroidTranslator> INSTANCES = new ArrayList<AndroidTranslator>();
private int index;
private ClassHandler classHandler;
private Map<String, byte[]> modifiedClasses = new HashMap<String, byte[]>();
public AndroidTranslator(ClassHandler classHandler) {
this.classHandler = classHandler;
index = addInstance(this);
}
synchronized static private int addInstance(AndroidTranslator androidTranslator) {
INSTANCES.add(androidTranslator);
return INSTANCES.size() - 1;
}
synchronized static public AndroidTranslator get(int index) {
return INSTANCES.get(index);
}
@Override
public void start(ClassPool classPool) throws NotFoundException, CannotCompileException {
}
@Override
public void onLoad(ClassPool classPool, String className) throws NotFoundException, CannotCompileException {
boolean needsStripping =
className.startsWith("android.")
|| className.startsWith("org.apache.http")
|| className.startsWith("com.google.android.");
CtClass ctClass = classPool.get(className);
if (needsStripping) {
int modifiers = ctClass.getModifiers();
if (Modifier.isFinal(modifiers)) {
ctClass.setModifiers(modifiers & ~Modifier.FINAL);
}
if (ctClass.isInterface()) return;
classHandler.instrument(ctClass);
fixConstructors(ctClass);
fixMethods(ctClass);
try {
modifiedClasses.put(className, ctClass.toBytecode());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void fixConstructors(CtClass ctClass) throws CannotCompileException, NotFoundException {
boolean needsDefault = true;
for (CtConstructor ctConstructor : ctClass.getConstructors()) {
String methodBody = generateConstructorBody(ctClass, ctConstructor.getParameterTypes());
ctConstructor.setBody("{\n" + methodBody + "\n}");
if (ctConstructor.getParameterTypes().length == 0) {
needsDefault = false;
}
}
if (needsDefault) {
String methodBody = generateConstructorBody(ctClass, new CtClass[0]);
ctClass.addConstructor(CtNewConstructor.make(new CtClass[0], new CtClass[0], methodBody, ctClass));
}
}
private String generateConstructorBody(CtClass ctClass, CtClass[] parameterTypes) throws NotFoundException {
return generateMethodBody(ctClass,
new CtMethod(CtClass.voidType, "<init>", parameterTypes, ctClass),
CtClass.voidType,
Type.VOID,
true,
false);
}
private void fixMethods(CtClass ctClass) throws NotFoundException, CannotCompileException {
for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
int modifiers = ctMethod.getModifiers();
if (Modifier.isNative(modifiers)) {
modifiers = modifiers & ~Modifier.NATIVE;
}
if (Modifier.isFinal(modifiers)) {
modifiers = modifiers & ~Modifier.FINAL;
}
ctMethod.setModifiers(modifiers);
CtClass returnCtClass = ctMethod.getReturnType();
Type returnType = Type.find(returnCtClass);
String methodName = ctMethod.getName();
CtClass[] paramTypes = ctMethod.getParameterTypes();
boolean isAbstract = (ctMethod.getModifiers() & Modifier.ABSTRACT) != 0;
// if (!isAbstract) {
// if (methodName.startsWith("set") && paramTypes.length == 1) {
// String fieldName = "__" + methodName.substring(3);
// if (declareField(ctClass, fieldName, paramTypes[0])) {
// methodBody = fieldName + " = $1;\n" + methodBody;
// }
// } else if (methodName.startsWith("get") && paramTypes.length == 0) {
// String fieldName = "__" + methodName.substring(3);
// if (declareField(ctClass, fieldName, returnType)) {
// methodBody = "return " + fieldName + ";\n";
// }
// }
// }
boolean returnsVoid = returnType.isVoid();
boolean isStatic = Modifier.isStatic(modifiers);
String methodBody;
if (!isAbstract) {
methodBody = generateMethodBody(ctClass, ctMethod, returnCtClass, returnType, returnsVoid, isStatic);
} else {
methodBody = returnsVoid ? "" : "return " + returnType.defaultReturnString() + ";";
}
CtMethod newMethod = CtNewMethod.make(
ctMethod.getModifiers(),
returnCtClass,
methodName,
paramTypes,
ctMethod.getExceptionTypes(),
"{\n" + methodBody + "\n}",
ctClass);
ctMethod.setBody(newMethod, null);
}
}
private String generateMethodBody(CtClass ctClass, CtMethod ctMethod, CtClass returnCtClass, Type returnType, boolean returnsVoid, boolean aStatic) throws NotFoundException {
String methodBody;
StringBuilder buf = new StringBuilder();
if (!returnsVoid) {
buf.append("Object x = ");
}
buf.append(AndroidTranslator.class.getName());
buf.append(".get(");
buf.append(index);
buf.append(").methodInvoked(");
buf.append(ctClass.getName());
buf.append(".class, \"");
buf.append(ctMethod.getName());
buf.append("\", ");
if (!aStatic) {
buf.append("this");
} else {
buf.append("null");
}
buf.append(", ");
appendParamTypeArray(buf, ctMethod);
buf.append(", ");
appendParamArray(buf, ctMethod);
buf.append(")");
buf.append(";\n");
if (!returnsVoid) {
buf.append("if (x != null) return ((");
buf.append(returnType.nonPrimitiveClassName(returnCtClass));
buf.append(") x)");
buf.append(returnType.unboxString());
buf.append(";\n");
buf.append("return ");
buf.append(returnType.defaultReturnString());
buf.append(";");
}
methodBody = buf.toString();
return methodBody;
}
private void appendParamTypeArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
CtClass[] parameterTypes = ctMethod.getParameterTypes();
if (parameterTypes.length == 0) {
buf.append("new String[0]");
} else {
buf.append("new String[] {");
for (int i = 0; i < parameterTypes.length; i++) {
if (i > 0) buf.append(", ");
buf.append("\"");
CtClass parameterType = parameterTypes[i];
buf.append(parameterType.getName());
buf.append("\"");
}
buf.append("}");
}
}
private void appendParamArray(StringBuilder buf, CtMethod ctMethod) throws NotFoundException {
int parameterCount = ctMethod.getParameterTypes().length;
if (parameterCount == 0) {
buf.append("new Object[0]");
} else {
buf.append("new Object[] {");
for (int i = 0; i < parameterCount; i++) {
if (i > 0) buf.append(", ");
buf.append(AndroidTranslator.class.getName());
buf.append(".autobox(");
buf.append("$").append(i + 1);
buf.append(")");
}
buf.append("}");
}
}
@SuppressWarnings({"UnusedDeclaration"})
public Object methodInvoked(Class clazz, String methodName, Object instance, String[] paramTypes, Object[] params) {
return classHandler.methodInvoked(clazz, methodName, instance, paramTypes, params);
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(Object o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(boolean o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(byte o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(char o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(short o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(int o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(long o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(float o) {
return o;
}
@SuppressWarnings({"UnusedDeclaration"})
public static Object autobox(double o) {
return o;
}
private boolean declareField(CtClass ctClass, String fieldName, CtClass fieldType) throws CannotCompileException, NotFoundException {
CtMethod ctMethod = getMethod(ctClass, "get" + fieldName, "");
if (ctMethod == null) {
return false;
}
CtClass getterFieldType = ctMethod.getReturnType();
if (!getterFieldType.equals(fieldType)) {
return false;
}
if (getField(ctClass, fieldName) == null) {
CtField field = new CtField(fieldType, fieldName, ctClass);
field.setModifiers(Modifier.PRIVATE);
ctClass.addField(field);
}
return true;
}
private CtField getField(CtClass ctClass, String fieldName) {
try {
return ctClass.getField(fieldName);
} catch (NotFoundException e) {
return null;
}
}
private CtMethod getMethod(CtClass ctClass, String methodName, String desc) {
try {
return ctClass.getMethod(methodName, desc);
} catch (NotFoundException e) {
return null;
}
}
public void saveAllClassesToCache(File file, Manifest manifest) {
if (modifiedClasses.size() > 0) {
JarOutputStream jarOutputStream = null;
try {
File cacheJarDir = file.getParentFile();
if (!cacheJarDir.exists()) {
//noinspection ResultOfMethodCallIgnored
cacheJarDir.mkdirs();
}
if (file.exists()) {
jarOutputStream = new JarOutputStream(new FileOutputStream(file, true));
} else {
jarOutputStream = new JarOutputStream(new FileOutputStream(file), manifest);
}
for (Map.Entry<String, byte[]> entry : modifiedClasses.entrySet()) {
String key = entry.getKey();
jarOutputStream.putNextEntry(new JarEntry(key.replace('.', '/') + ".class"));
jarOutputStream.write(entry.getValue());
jarOutputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (jarOutputStream != null) {
try {
jarOutputStream.close();
} catch (IOException ignore) {
}
}
}
}
}
}