Home | History | Annotate | Download | only in bytecode
      1 package org.robolectric.internal.bytecode;
      2 
      3 import static java.lang.invoke.MethodHandles.constant;
      4 import static java.lang.invoke.MethodHandles.dropArguments;
      5 import static java.lang.invoke.MethodHandles.foldArguments;
      6 import static java.lang.invoke.MethodHandles.identity;
      7 import static java.lang.invoke.MethodType.methodType;
      8 
      9 import java.lang.invoke.MethodHandle;
     10 import java.lang.invoke.MethodHandles;
     11 import java.lang.invoke.MethodType;
     12 import java.lang.reflect.Array;
     13 import java.lang.reflect.Field;
     14 import java.lang.reflect.InvocationTargetException;
     15 import java.lang.reflect.Method;
     16 import java.lang.reflect.Modifier;
     17 import java.util.ArrayList;
     18 import java.util.Collections;
     19 import java.util.HashMap;
     20 import java.util.LinkedHashMap;
     21 import java.util.List;
     22 import java.util.Map;
     23 import java.util.concurrent.ConcurrentHashMap;
     24 import org.robolectric.annotation.Implementation;
     25 import org.robolectric.annotation.Implements;
     26 import org.robolectric.annotation.RealObject;
     27 import org.robolectric.util.Function;
     28 import org.robolectric.util.ReflectionHelpers;
     29 
     30 public class ShadowWrangler implements ClassHandler {
     31   public static final Function<Object, Object> DO_NOTHING_HANDLER = new Function<Object, Object>() {
     32     @Override
     33     public Object call(Class<?> theClass, Object value, Object[] params) {
     34       return null;
     35     }
     36   };
     37   public static final Plan DO_NOTHING_PLAN = new Plan() {
     38     @Override
     39     public Object run(Object instance, Object roboData, Object[] params) throws Exception {
     40       return null;
     41     }
     42 
     43     @Override
     44     public String describe() {
     45       return "do nothing";
     46     }
     47   };
     48   public static final Plan CALL_REAL_CODE_PLAN = null;
     49   public static final MethodHandle CALL_REAL_CODE = null;
     50   public static final MethodHandle DO_NOTHING = constant(Void.class, null).asType(methodType(void.class));
     51   private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
     52   private static final boolean STRIP_SHADOW_STACK_TRACES = true;
     53   private static final ShadowConfig NO_SHADOW_CONFIG = new ShadowConfig(Object.class.getName(), true, false, false, -1, -1);
     54   static final Object NO_SHADOW = new Object();
     55   private static final MethodHandle NO_SHADOW_HANDLE = constant(Object.class, NO_SHADOW);
     56   private final ShadowMap shadowMap;
     57   private final Interceptors interceptors;
     58   private final int apiLevel;
     59   private final Map<Class, MetaShadow> metaShadowMap = new HashMap<>();
     60   private final Map<String, Plan> planCache =
     61       Collections.synchronizedMap(new LinkedHashMap<String, Plan>() {
     62         @Override
     63         protected boolean removeEldestEntry(Map.Entry<String, Plan> eldest) {
     64           return size() > 500;
     65         }
     66       });
     67   private final Map<Class, ShadowConfig> shadowConfigCache = new ConcurrentHashMap<>();
     68   private final ClassValue<ShadowConfig> shadowConfigs = new ClassValue<ShadowConfig>() {
     69     @Override protected ShadowConfig computeValue(Class<?> type) {
     70       return shadowMap.get(type);
     71     }
     72   };
     73 
     74   public ShadowWrangler(ShadowMap shadowMap, int apiLevel, Interceptors interceptors) {
     75     this.shadowMap = shadowMap;
     76     this.apiLevel = apiLevel;
     77     this.interceptors = interceptors;
     78   }
     79 
     80   public static Class<?> loadClass(String paramType, ClassLoader classLoader) {
     81     Class primitiveClass = RoboType.findPrimitiveClass(paramType);
     82     if (primitiveClass != null) return primitiveClass;
     83 
     84     int arrayLevel = 0;
     85     while (paramType.endsWith("[]")) {
     86       arrayLevel++;
     87       paramType = paramType.substring(0, paramType.length() - 2);
     88     }
     89 
     90     Class<?> clazz = RoboType.findPrimitiveClass(paramType);
     91     if (clazz == null) {
     92       try {
     93         clazz = classLoader.loadClass(paramType);
     94       } catch (ClassNotFoundException e) {
     95         throw new RuntimeException(e);
     96       }
     97     }
     98 
     99     while (arrayLevel-- > 0) {
    100       clazz = Array.newInstance(clazz, 0).getClass();
    101     }
    102 
    103     return clazz;
    104   }
    105 
    106   @Override
    107   public void classInitializing(Class clazz) {
    108     Class<?> shadowClass = findDirectShadowClass(clazz);
    109     if (shadowClass != null) {
    110       try {
    111         Method method = shadowClass.getDeclaredMethod(ShadowConstants.STATIC_INITIALIZER_METHOD_NAME);
    112         if (!Modifier.isStatic(method.getModifiers())) {
    113           throw new RuntimeException(shadowClass.getName() + "." + method.getName() + " is not static");
    114         }
    115         method.setAccessible(true);
    116         method.invoke(null);
    117       } catch (NoSuchMethodException e) {
    118         RobolectricInternals.performStaticInitialization(clazz);
    119       } catch (InvocationTargetException | IllegalAccessException e) {
    120         throw new RuntimeException(e);
    121       }
    122     } else {
    123       RobolectricInternals.performStaticInitialization(clazz);
    124     }
    125   }
    126 
    127   @Override
    128   public Object initializing(Object instance) {
    129     return createShadowFor(instance);
    130   }
    131 
    132   @Override
    133   public Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) {
    134     if (planCache.containsKey(signature)) {
    135       return planCache.get(signature);
    136     }
    137     Plan plan = calculatePlan(signature, isStatic, theClass);
    138     planCache.put(signature, plan);
    139     return plan;
    140   }
    141 
    142   @Override public MethodHandle findShadowMethod(Class<?> caller, String name, MethodType type,
    143       boolean isStatic) throws IllegalAccessException {
    144     ShadowConfig shadowConfig = shadowConfigs.get(caller);
    145     if (shadowConfig == null) return CALL_REAL_CODE;
    146 
    147     ClassLoader classLoader = caller.getClassLoader();
    148     MethodType actualType = isStatic ? type : type.dropParameterTypes(0, 1);
    149     Method method = findShadowMethod(classLoader, shadowConfig, name, actualType.parameterArray());
    150     if (method == null) {
    151       return shadowConfig.callThroughByDefault ? CALL_REAL_CODE : DO_NOTHING;
    152     }
    153 
    154     Class<?> declaredShadowedClass = getShadowedClass(method);
    155     if (declaredShadowedClass.equals(Object.class)) {
    156       // e.g. for equals(), hashCode(), toString()
    157       return CALL_REAL_CODE;
    158     }
    159 
    160     boolean shadowClassMismatch = !declaredShadowedClass.equals(caller);
    161     if (shadowClassMismatch && !shadowConfig.inheritImplementationMethods) {
    162       return CALL_REAL_CODE;
    163     } else {
    164       MethodHandle mh = LOOKUP.unreflect(method);
    165 
    166       // Robolectric doesn't actually look for static, this for example happens
    167       // in MessageQueue.nativeInit() which used to be void non-static in 4.2.
    168       if (!isStatic && Modifier.isStatic(method.getModifiers())) {
    169         return dropArguments(mh, 0, Object.class);
    170       } else {
    171         return mh;
    172       }
    173     }
    174   }
    175 
    176   private Plan calculatePlan(String signature, boolean isStatic, Class<?> theClass) {
    177     final InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, theClass.getClassLoader());
    178     ShadowConfig shadowConfig = getShadowConfig(theClass);
    179 
    180     if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) {
    181       return CALL_REAL_CODE_PLAN;
    182     } else {
    183       try {
    184         final ClassLoader classLoader = theClass.getClassLoader();
    185         Class<?>[] types = invocationProfile.getParamClasses(classLoader);
    186         Method shadowMethod = findShadowMethod(classLoader, shadowConfig, invocationProfile.methodName, types);
    187         if (shadowMethod == null) {
    188           return shadowConfig.callThroughByDefault
    189               ? CALL_REAL_CODE_PLAN
    190               : strict(invocationProfile) ? CALL_REAL_CODE_PLAN : DO_NOTHING_PLAN;
    191         }
    192 
    193         final Class<?> declaredShadowedClass = getShadowedClass(shadowMethod);
    194 
    195         if (declaredShadowedClass.equals(Object.class)) {
    196           // e.g. for equals(), hashCode(), toString()
    197           return CALL_REAL_CODE_PLAN;
    198         }
    199 
    200         boolean shadowClassMismatch = !declaredShadowedClass.equals(invocationProfile.clazz);
    201         if (shadowClassMismatch && (!shadowConfig.inheritImplementationMethods || strict(invocationProfile))) {
    202           return CALL_REAL_CODE_PLAN;
    203         } else {
    204           return new ShadowMethodPlan(shadowMethod);
    205         }
    206       } catch (ClassNotFoundException e) {
    207         throw new RuntimeException(e);
    208       }
    209     }
    210   }
    211 
    212   private Method findShadowMethod(ClassLoader classLoader, ShadowConfig config, String name, Class<?>[] types) {
    213     Class<?> shadowClass;
    214     try {
    215       shadowClass = Class.forName(config.shadowClassName, false, classLoader);
    216     } catch (ClassNotFoundException e) {
    217       throw new IllegalStateException(e);
    218     }
    219 
    220     return findShadowMethod(config, shadowClass, name, types);
    221   }
    222 
    223   private Method findShadowMethod(ShadowConfig config, Class<?> shadowClass, String name, Class<?>[] types) {
    224     Method method = findShadowMethodInternal(shadowClass, name, types);
    225 
    226     if (method == null && config.looseSignatures) {
    227       Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray();
    228       method = findShadowMethodInternal(shadowClass, name, genericTypes);
    229     }
    230 
    231     Class<?> superclass;
    232     if (method == null && config.inheritImplementationMethods && (superclass = shadowClass.getSuperclass()) != null) {
    233       return findShadowMethod(config, superclass, name, types);
    234     }
    235 
    236     return method;
    237   }
    238 
    239   @SuppressWarnings("ReferenceEquality")
    240   private ShadowConfig getShadowConfig(Class clazz) {
    241     ShadowConfig shadowConfig = shadowConfigCache.get(clazz);
    242     if (shadowConfig == null) {
    243       shadowConfig = shadowMap.get(clazz);
    244       shadowConfigCache.put(clazz, shadowConfig == null ? NO_SHADOW_CONFIG : shadowConfig);
    245       return shadowConfig;
    246     } else {
    247       return (shadowConfig == NO_SHADOW_CONFIG) ? null : shadowConfig;
    248     }
    249   }
    250 
    251   private boolean isAndroidSupport(InvocationProfile invocationProfile) {
    252     return invocationProfile.clazz.getName().startsWith("android.support");
    253   }
    254 
    255   private boolean strict(InvocationProfile invocationProfile) {
    256     return isAndroidSupport(invocationProfile) || invocationProfile.isDeclaredOnObject();
    257   }
    258 
    259   private Method findShadowMethodInternal(Class<?> shadowClass, String methodName, Class<?>[] paramClasses) {
    260     try {
    261       Method method = shadowClass.getDeclaredMethod(methodName, paramClasses);
    262       method.setAccessible(true);
    263       Implementation implementation = getImplementationAnnotation(method);
    264       return matchesSdk(implementation) ? method : null;
    265 
    266       // todo: allow per-version overloading
    267 //      if (method == null) {
    268 //        String methodPrefix = name + "$$";
    269 //        for (Method candidateMethod : shadowClass.getMethods()) {
    270 //          if (candidateMethod.getName().startsWith(methodPrefix)) {
    271 //
    272 //          }
    273 //        }
    274 //      }
    275 
    276     } catch (NoSuchMethodException e) {
    277       return null;
    278     }
    279   }
    280 
    281   private boolean matchesSdk(Implementation implementation) {
    282     return implementation.minSdk() <= apiLevel && (implementation.maxSdk() == -1 || implementation.maxSdk() >= apiLevel);
    283   }
    284 
    285   private Class<?> getShadowedClass(Method shadowMethod) {
    286     Class<?> shadowingClass = shadowMethod.getDeclaringClass();
    287     if (shadowingClass.equals(Object.class)) {
    288       return Object.class;
    289     }
    290 
    291     Implements implementsAnnotation = shadowingClass.getAnnotation(Implements.class);
    292     if (implementsAnnotation == null) {
    293       throw new RuntimeException(shadowingClass + " has no @" + Implements.class.getSimpleName() + " annotation");
    294     }
    295     String shadowedClassName = implementsAnnotation.className();
    296     if (shadowedClassName.isEmpty()) {
    297       return implementsAnnotation.value();
    298     } else {
    299       try {
    300         return shadowingClass.getClassLoader().loadClass(shadowedClassName);
    301       } catch (ClassNotFoundException e) {
    302         throw new RuntimeException(e);
    303       }
    304     }
    305   }
    306 
    307   private Implementation getImplementationAnnotation(Method method) {
    308     if (method == null) {
    309       return null;
    310     }
    311     Implementation implementation = method.getAnnotation(Implementation.class);
    312     return implementation == null
    313         ? ReflectionHelpers.defaultsFor(Implementation.class)
    314         : implementation;
    315   }
    316 
    317   @Override
    318   public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable {
    319     final MethodSignature methodSignature = MethodSignature.parse(signature);
    320     return interceptors.getInterceptionHandler(methodSignature).call(theClass, instance, params);
    321   }
    322 
    323   @Override
    324   public <T extends Throwable> T stripStackTrace(T throwable) {
    325     if (STRIP_SHADOW_STACK_TRACES) {
    326       List<StackTraceElement> stackTrace = new ArrayList<>();
    327 
    328       String previousClassName = null;
    329       String previousMethodName = null;
    330       String previousFileName = null;
    331 
    332       for (StackTraceElement stackTraceElement : throwable.getStackTrace()) {
    333         String methodName = stackTraceElement.getMethodName();
    334         String className = stackTraceElement.getClassName();
    335         String fileName = stackTraceElement.getFileName();
    336 
    337         if (methodName.equals(previousMethodName)
    338             && className.equals(previousClassName)
    339             && fileName != null && fileName.equals(previousFileName)
    340             && stackTraceElement.getLineNumber() < 0) {
    341           continue;
    342         }
    343 
    344         if (className.equals(ShadowMethodPlan.class.getName())) {
    345           continue;
    346         }
    347 
    348         if (methodName.startsWith(ShadowConstants.ROBO_PREFIX)) {
    349           methodName = methodName.substring(ShadowConstants.ROBO_PREFIX.length());
    350           stackTraceElement = new StackTraceElement(className, methodName,
    351               stackTraceElement.getFileName(), stackTraceElement.getLineNumber());
    352         }
    353 
    354         if (className.startsWith("sun.reflect.") || className.startsWith("java.lang.reflect.")) {
    355           continue;
    356         }
    357 
    358         stackTrace.add(stackTraceElement);
    359 
    360         previousClassName = className;
    361         previousMethodName = methodName;
    362         previousFileName = fileName;
    363       }
    364       throwable.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()]));
    365     }
    366     return throwable;
    367   }
    368 
    369   public Object createShadowFor(Object instance) {
    370     String shadowClassName = getShadowClassName(instance.getClass());
    371 
    372     if (shadowClassName == null) return NO_SHADOW;
    373 
    374     try {
    375       Class<?> shadowClass = loadClass(shadowClassName, instance.getClass().getClassLoader());
    376       Object shadow = shadowClass.getDeclaredConstructor().newInstance();
    377       injectRealObjectOn(shadow, shadowClass, instance);
    378 
    379       return shadow;
    380     } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
    381         | InvocationTargetException e) {
    382       throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e);
    383     }
    384   }
    385 
    386   @Override public MethodHandle getShadowCreator(Class<?> caller) {
    387     String shadowClassName = getShadowClassNameInvoke(caller);
    388 
    389     if (shadowClassName == null) return dropArguments(NO_SHADOW_HANDLE, 0, caller);
    390 
    391     try {
    392       Class<?> shadowClass = Class.forName(shadowClassName, false, caller.getClassLoader());
    393       MethodHandle constructor = LOOKUP.findConstructor(shadowClass, methodType(void.class));
    394       MetaShadow metaShadow = getMetaShadow(shadowClass);
    395 
    396       MethodHandle mh = identity(shadowClass); // (instance)
    397       mh = dropArguments(mh, 1, caller); // (instance)
    398       for (Field field : metaShadow.realObjectFields) {
    399         MethodHandle setter = LOOKUP.unreflectSetter(field);
    400         MethodType setterType = mh.type().changeReturnType(void.class);
    401         mh = foldArguments(mh, setter.asType(setterType));
    402       }
    403       mh = foldArguments(mh, constructor);  // (shadow, instance)
    404 
    405       return mh; // (instance)
    406     } catch (NoSuchMethodException | IllegalAccessException e) {
    407       throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e);
    408     } catch (ClassNotFoundException e) {
    409       throw new RuntimeException("Could not instantiate shadow", e);
    410     }
    411   }
    412 
    413   private String getShadowClassNameInvoke(Class<?> cl) {
    414     Class clazz = cl;
    415     ShadowConfig shadowConfig = null;
    416     while (shadowConfig == null && clazz != null) {
    417       shadowConfig = shadowConfigs.get(clazz);
    418       clazz = clazz.getSuperclass();
    419     }
    420     return shadowConfig == null ? null : shadowConfig.shadowClassName;
    421   }
    422 
    423   private String getShadowClassName(Class<?> cl) {
    424     Class clazz = cl;
    425     ShadowConfig shadowConfig = null;
    426     while ((shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) && clazz != null) {
    427       shadowConfig = getShadowConfig(clazz);
    428       clazz = clazz.getSuperclass();
    429     }
    430     return shadowConfig == null ? null : shadowConfig.shadowClassName;
    431   }
    432 
    433   private void injectRealObjectOn(Object shadow, Class<?> shadowClass, Object instance) {
    434     MetaShadow metaShadow = getMetaShadow(shadowClass);
    435     for (Field realObjectField : metaShadow.realObjectFields) {
    436       writeField(shadow, instance, realObjectField);
    437     }
    438   }
    439 
    440   private MetaShadow getMetaShadow(Class<?> shadowClass) {
    441     synchronized (metaShadowMap) {
    442       if (!metaShadowMap.containsKey(shadowClass)) {
    443         metaShadowMap.put(shadowClass, new MetaShadow(shadowClass));
    444       }
    445       return metaShadowMap.get(shadowClass);
    446     }
    447   }
    448 
    449   private Class<?> findDirectShadowClass(Class<?> originalClass) {
    450     ShadowConfig shadowConfig = getShadowConfig(originalClass);
    451     if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) {
    452       return null;
    453     }
    454     return loadClass(shadowConfig.shadowClassName, originalClass.getClassLoader());
    455   }
    456 
    457   private static void writeField(Object target, Object value, Field realObjectField) {
    458     try {
    459       realObjectField.set(target, value);
    460     } catch (IllegalAccessException e) {
    461       throw new RuntimeException(e);
    462     }
    463   }
    464 
    465   private static class ShadowMethodPlan implements Plan {
    466     private final Method shadowMethod;
    467 
    468     public ShadowMethodPlan(Method shadowMethod) {
    469       this.shadowMethod = shadowMethod;
    470     }
    471 
    472     @Override
    473     public Object run(Object instance, Object roboData, Object[] params) throws Throwable {
    474       //noinspection UnnecessaryLocalVariable
    475       Object shadow = roboData;
    476       try {
    477         return shadowMethod.invoke(shadow, params);
    478       } catch (IllegalArgumentException e) {
    479         throw new IllegalArgumentException("attempted to invoke " + shadowMethod
    480             + (shadow == null ? "" : " on instance of " + shadow.getClass() + ", but " + shadow.getClass().getSimpleName() + " doesn't extend " + shadowMethod.getDeclaringClass().getSimpleName()));
    481       } catch (InvocationTargetException e) {
    482         throw e.getCause();
    483       }
    484     }
    485 
    486     @Override
    487     public String describe() {
    488       return shadowMethod.toString();
    489     }
    490   }
    491 
    492   private static class MetaShadow {
    493     final List<Field> realObjectFields = new ArrayList<>();
    494 
    495     public MetaShadow(Class<?> shadowClass) {
    496       while (shadowClass != null) {
    497         for (Field field : shadowClass.getDeclaredFields()) {
    498           if (field.isAnnotationPresent(RealObject.class)) {
    499             if (Modifier.isStatic(field.getModifiers())) {
    500               String message = "@RealObject must be on a non-static field, " + shadowClass;
    501               System.err.println(message);
    502               throw new IllegalArgumentException(message);
    503             }
    504             field.setAccessible(true);
    505             realObjectFields.add(field);
    506           }
    507         }
    508         shadowClass = shadowClass.getSuperclass();
    509       }
    510     }
    511   }
    512 }
    513