blob: 8befab0c74049959ed22b5dbc6798bc1b5e6722f [file] [log] [blame]
package org.robolectric.internal.bytecode;
import static java.lang.invoke.MethodHandles.constant;
import static java.lang.invoke.MethodHandles.dropArguments;
import static java.lang.invoke.MethodHandles.foldArguments;
import static java.lang.invoke.MethodHandles.identity;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.Function;
import org.robolectric.util.ReflectionHelpers;
public class ShadowWrangler implements ClassHandler {
public static final Function<Object, Object> DO_NOTHING_HANDLER = new Function<Object, Object>() {
@Override
public Object call(Class<?> theClass, Object value, Object[] params) {
return null;
}
};
public static final Plan DO_NOTHING_PLAN = new Plan() {
@Override
public Object run(Object instance, Object roboData, Object[] params) throws Exception {
return null;
}
@Override
public String describe() {
return "do nothing";
}
};
public static final Plan CALL_REAL_CODE_PLAN = null;
public static final MethodHandle CALL_REAL_CODE = null;
public static final MethodHandle DO_NOTHING = constant(Void.class, null).asType(methodType(void.class));
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final boolean STRIP_SHADOW_STACK_TRACES = true;
private static final ShadowConfig NO_SHADOW_CONFIG = new ShadowConfig(Object.class.getName(), true, false, false, -1, -1);
static final Object NO_SHADOW = new Object();
private static final MethodHandle NO_SHADOW_HANDLE = constant(Object.class, NO_SHADOW);
private final ShadowMap shadowMap;
private final Interceptors interceptors;
private final int apiLevel;
private final Map<Class, MetaShadow> metaShadowMap = new HashMap<>();
private final Map<String, Plan> planCache =
Collections.synchronizedMap(new LinkedHashMap<String, Plan>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Plan> eldest) {
return size() > 500;
}
});
private final Map<Class, ShadowConfig> shadowConfigCache = new ConcurrentHashMap<>();
private final ClassValue<ShadowConfig> shadowConfigs = new ClassValue<ShadowConfig>() {
@Override protected ShadowConfig computeValue(Class<?> type) {
return shadowMap.get(type);
}
};
public ShadowWrangler(ShadowMap shadowMap, int apiLevel, Interceptors interceptors) {
this.shadowMap = shadowMap;
this.apiLevel = apiLevel;
this.interceptors = interceptors;
}
public static Class<?> loadClass(String paramType, ClassLoader classLoader) {
Class primitiveClass = RoboType.findPrimitiveClass(paramType);
if (primitiveClass != null) return primitiveClass;
int arrayLevel = 0;
while (paramType.endsWith("[]")) {
arrayLevel++;
paramType = paramType.substring(0, paramType.length() - 2);
}
Class<?> clazz = RoboType.findPrimitiveClass(paramType);
if (clazz == null) {
try {
clazz = classLoader.loadClass(paramType);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
while (arrayLevel-- > 0) {
clazz = Array.newInstance(clazz, 0).getClass();
}
return clazz;
}
@Override
public void classInitializing(Class clazz) {
Class<?> shadowClass = findDirectShadowClass(clazz);
if (shadowClass != null) {
try {
Method method = shadowClass.getMethod(ShadowConstants.STATIC_INITIALIZER_METHOD_NAME);
if (!Modifier.isStatic(method.getModifiers())) {
throw new RuntimeException(shadowClass.getName() + "." + method.getName() + " is not static");
}
method.setAccessible(true);
method.invoke(null);
} catch (NoSuchMethodException e) {
RobolectricInternals.performStaticInitialization(clazz);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
} else {
RobolectricInternals.performStaticInitialization(clazz);
}
}
@Override
public Object initializing(Object instance) {
return createShadowFor(instance);
}
@Override
public Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) {
if (planCache.containsKey(signature)) {
return planCache.get(signature);
}
Plan plan = calculatePlan(signature, isStatic, theClass);
planCache.put(signature, plan);
return plan;
}
@Override public MethodHandle findShadowMethod(Class<?> caller, String name, MethodType type,
boolean isStatic) throws IllegalAccessException {
ShadowConfig shadowConfig = shadowConfigs.get(caller);
if (shadowConfig == null) return CALL_REAL_CODE;
ClassLoader classLoader = caller.getClassLoader();
MethodType actualType = isStatic ? type : type.dropParameterTypes(0, 1);
Method method = findShadowMethod(classLoader, shadowConfig, name, actualType.parameterArray());
if (method == null) {
return shadowConfig.callThroughByDefault ? CALL_REAL_CODE : DO_NOTHING;
}
Class<?> declaredShadowedClass = getShadowedClass(method);
if (declaredShadowedClass.equals(Object.class)) {
// e.g. for equals(), hashCode(), toString()
return CALL_REAL_CODE;
}
boolean shadowClassMismatch = !declaredShadowedClass.equals(caller);
if (shadowClassMismatch && !shadowConfig.inheritImplementationMethods) {
return CALL_REAL_CODE;
} else {
MethodHandle mh = LOOKUP.unreflect(method);
// Robolectric doesn't actually look for static, this for example happens
// in MessageQueue.nativeInit() which used to be void non-static in 4.2.
if (!isStatic && Modifier.isStatic(method.getModifiers())) {
return dropArguments(mh, 0, Object.class);
} else {
return mh;
}
}
}
private Plan calculatePlan(String signature, boolean isStatic, Class<?> theClass) {
final InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, theClass.getClassLoader());
ShadowConfig shadowConfig = getShadowConfig(theClass);
if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) {
return CALL_REAL_CODE_PLAN;
} else {
try {
final ClassLoader classLoader = theClass.getClassLoader();
Class<?>[] types = invocationProfile.getParamClasses(classLoader);
Method shadowMethod = findShadowMethod(classLoader, shadowConfig, invocationProfile.methodName, types);
if (shadowMethod == null) {
return shadowConfig.callThroughByDefault
? CALL_REAL_CODE_PLAN
: strict(invocationProfile) ? CALL_REAL_CODE_PLAN : DO_NOTHING_PLAN;
}
final Class<?> declaredShadowedClass = getShadowedClass(shadowMethod);
if (declaredShadowedClass.equals(Object.class)) {
// e.g. for equals(), hashCode(), toString()
return CALL_REAL_CODE_PLAN;
}
boolean shadowClassMismatch = !declaredShadowedClass.equals(invocationProfile.clazz);
if (shadowClassMismatch && (!shadowConfig.inheritImplementationMethods || strict(invocationProfile))) {
return CALL_REAL_CODE_PLAN;
} else {
return new ShadowMethodPlan(shadowMethod);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
private Method findShadowMethod(ClassLoader classLoader, ShadowConfig config, String name, Class<?>[] types) {
try {
Class<?> shadowClass = Class.forName(config.shadowClassName, false, classLoader);
Method method = findShadowMethodInternal(shadowClass, name, types);
if (method == null && config.looseSignatures) {
Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray();
method = findShadowMethodInternal(shadowClass, name, genericTypes);
}
return method;
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}
@SuppressWarnings("ReferenceEquality")
private ShadowConfig getShadowConfig(Class clazz) {
ShadowConfig shadowConfig = shadowConfigCache.get(clazz);
if (shadowConfig == null) {
shadowConfig = shadowMap.get(clazz);
shadowConfigCache.put(clazz, shadowConfig == null ? NO_SHADOW_CONFIG : shadowConfig);
return shadowConfig;
} else {
return (shadowConfig == NO_SHADOW_CONFIG) ? null : shadowConfig;
}
}
private boolean isAndroidSupport(InvocationProfile invocationProfile) {
return invocationProfile.clazz.getName().startsWith("android.support");
}
private boolean strict(InvocationProfile invocationProfile) {
return isAndroidSupport(invocationProfile) || invocationProfile.isDeclaredOnObject();
}
private Method findShadowMethodInternal(Class<?> shadowClass, String methodName, Class<?>[] paramClasses) throws ClassNotFoundException {
try {
Method method = shadowClass.getMethod(methodName, paramClasses);
Implementation implementation = getImplementationAnnotation(method);
return matchesSdk(implementation) ? method : null;
// todo: allow per-version overloading
// if (method == null) {
// String methodPrefix = name + "$$";
// for (Method candidateMethod : shadowClass.getMethods()) {
// if (candidateMethod.getName().startsWith(methodPrefix)) {
//
// }
// }
// }
} catch (NoSuchMethodException e) {
return null;
}
}
private boolean matchesSdk(Implementation implementation) {
return implementation.minSdk() <= apiLevel && (implementation.maxSdk() == -1 || implementation.maxSdk() >= apiLevel);
}
private Class<?> getShadowedClass(Method shadowMethod) {
Class<?> shadowingClass = shadowMethod.getDeclaringClass();
if (shadowingClass.equals(Object.class)) {
return Object.class;
}
Implements implementsAnnotation = shadowingClass.getAnnotation(Implements.class);
if (implementsAnnotation == null) {
throw new RuntimeException(shadowingClass + " has no @" + Implements.class.getSimpleName() + " annotation");
}
String shadowedClassName = implementsAnnotation.className();
if (shadowedClassName.isEmpty()) {
return implementsAnnotation.value();
} else {
try {
return shadowingClass.getClassLoader().loadClass(shadowedClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
private Implementation getImplementationAnnotation(Method method) {
if (method == null) {
return null;
}
Implementation implementation = method.getAnnotation(Implementation.class);
return implementation == null
? ReflectionHelpers.defaultsFor(Implementation.class)
: implementation;
}
@Override
public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable {
final MethodSignature methodSignature = MethodSignature.parse(signature);
return interceptors.getInterceptionHandler(methodSignature).call(theClass, instance, params);
}
@Override
public <T extends Throwable> T stripStackTrace(T throwable) {
if (STRIP_SHADOW_STACK_TRACES) {
List<StackTraceElement> stackTrace = new ArrayList<>();
String previousClassName = null;
String previousMethodName = null;
String previousFileName = null;
for (StackTraceElement stackTraceElement : throwable.getStackTrace()) {
String methodName = stackTraceElement.getMethodName();
String className = stackTraceElement.getClassName();
String fileName = stackTraceElement.getFileName();
if (methodName.equals(previousMethodName)
&& className.equals(previousClassName)
&& fileName != null && fileName.equals(previousFileName)
&& stackTraceElement.getLineNumber() < 0) {
continue;
}
if (className.equals(ShadowMethodPlan.class.getName())) {
continue;
}
if (methodName.startsWith(ShadowConstants.ROBO_PREFIX)) {
methodName = methodName.substring(ShadowConstants.ROBO_PREFIX.length());
stackTraceElement = new StackTraceElement(className, methodName,
stackTraceElement.getFileName(), stackTraceElement.getLineNumber());
}
if (className.startsWith("sun.reflect.") || className.startsWith("java.lang.reflect.")) {
continue;
}
stackTrace.add(stackTraceElement);
previousClassName = className;
previousMethodName = methodName;
previousFileName = fileName;
}
throwable.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()]));
}
return throwable;
}
public Object createShadowFor(Object instance) {
String shadowClassName = getShadowClassName(instance.getClass());
if (shadowClassName == null) return NO_SHADOW;
try {
Class<?> shadowClass = loadClass(shadowClassName, instance.getClass().getClassLoader());
Object shadow = shadowClass.getDeclaredConstructor().newInstance();
injectRealObjectOn(shadow, shadowClass, instance);
return shadow;
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException
| InvocationTargetException e) {
throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e);
}
}
@Override public MethodHandle getShadowCreator(Class<?> caller) {
String shadowClassName = getShadowClassNameInvoke(caller);
if (shadowClassName == null) return dropArguments(NO_SHADOW_HANDLE, 0, caller);
try {
Class<?> shadowClass = Class.forName(shadowClassName, false, caller.getClassLoader());
MethodHandle constructor = LOOKUP.findConstructor(shadowClass, methodType(void.class));
MetaShadow metaShadow = getMetaShadow(shadowClass);
MethodHandle mh = identity(shadowClass); // (instance)
mh = dropArguments(mh, 1, caller); // (instance)
for (Field field : metaShadow.realObjectFields) {
MethodHandle setter = LOOKUP.unreflectSetter(field);
MethodType setterType = mh.type().changeReturnType(void.class);
mh = foldArguments(mh, setter.asType(setterType));
}
mh = foldArguments(mh, constructor); // (shadow, instance)
return mh; // (instance)
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not instantiate shadow", e);
}
}
private String getShadowClassNameInvoke(Class<?> cl) {
Class clazz = cl;
ShadowConfig shadowConfig = null;
while (shadowConfig == null && clazz != null) {
shadowConfig = shadowConfigs.get(clazz);
clazz = clazz.getSuperclass();
}
return shadowConfig == null ? null : shadowConfig.shadowClassName;
}
private String getShadowClassName(Class<?> cl) {
Class clazz = cl;
ShadowConfig shadowConfig = null;
while ((shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) && clazz != null) {
shadowConfig = getShadowConfig(clazz);
clazz = clazz.getSuperclass();
}
return shadowConfig == null ? null : shadowConfig.shadowClassName;
}
private void injectRealObjectOn(Object shadow, Class<?> shadowClass, Object instance) {
MetaShadow metaShadow = getMetaShadow(shadowClass);
for (Field realObjectField : metaShadow.realObjectFields) {
writeField(shadow, instance, realObjectField);
}
}
private MetaShadow getMetaShadow(Class<?> shadowClass) {
synchronized (metaShadowMap) {
if (!metaShadowMap.containsKey(shadowClass)) {
metaShadowMap.put(shadowClass, new MetaShadow(shadowClass));
}
return metaShadowMap.get(shadowClass);
}
}
private Class<?> findDirectShadowClass(Class<?> originalClass) {
ShadowConfig shadowConfig = getShadowConfig(originalClass);
if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) {
return null;
}
return loadClass(shadowConfig.shadowClassName, originalClass.getClassLoader());
}
private static void writeField(Object target, Object value, Field realObjectField) {
try {
realObjectField.set(target, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static class ShadowMethodPlan implements Plan {
private final Method shadowMethod;
public ShadowMethodPlan(Method shadowMethod) {
this.shadowMethod = shadowMethod;
}
@Override
public Object run(Object instance, Object roboData, Object[] params) throws Throwable {
//noinspection UnnecessaryLocalVariable
Object shadow = roboData;
try {
return shadowMethod.invoke(shadow, params);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("attempted to invoke " + shadowMethod
+ (shadow == null ? "" : " on instance of " + shadow.getClass() + ", but " + shadow.getClass().getSimpleName() + " doesn't extend " + shadowMethod.getDeclaringClass().getSimpleName()));
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
@Override
public String describe() {
return shadowMethod.toString();
}
}
private static class MetaShadow {
final List<Field> realObjectFields = new ArrayList<>();
public MetaShadow(Class<?> shadowClass) {
while (shadowClass != null) {
for (Field field : shadowClass.getDeclaredFields()) {
if (field.isAnnotationPresent(RealObject.class)) {
if (Modifier.isStatic(field.getModifiers())) {
String message = "@RealObject must be on a non-static field, " + shadowClass;
System.err.println(message);
throw new IllegalArgumentException(message);
}
field.setAccessible(true);
realObjectFields.add(field);
}
}
shadowClass = shadowClass.getSuperclass();
}
}
}
}