Home | History | Annotate | Download | only in bytecode
      1 package org.robolectric.internal.bytecode;
      2 
      3 import com.google.common.collect.ImmutableMap;
      4 import java.lang.reflect.InvocationTargetException;
      5 import java.util.Collections;
      6 import java.util.HashMap;
      7 import java.util.HashSet;
      8 import java.util.Map;
      9 import java.util.Set;
     10 import org.robolectric.annotation.Implements;
     11 import org.robolectric.internal.ShadowProvider;
     12 import org.robolectric.shadow.api.ShadowPicker;
     13 
     14 /**
     15  * Maps from instrumented class to shadow class.
     16  *
     17  * We deal with class names rather than actual classes here, since a ShadowMap is built outside of
     18  * any sandboxes, but instrumented and shadowed classes must be loaded through a
     19  * {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
     20  *
     21  * Once constructed, instances are immutable.
     22  */
     23 @SuppressWarnings("NewApi")
     24 public class ShadowMap {
     25 
     26   static final ShadowMap EMPTY = new ShadowMap(ImmutableMap.of(), ImmutableMap.of());
     27 
     28   private final ImmutableMap<String, String> defaultShadows;
     29   private final ImmutableMap<String, ShadowInfo> overriddenShadows;
     30   private final ImmutableMap<String, String> shadowPickers;
     31 
     32   public static ShadowMap createFromShadowProviders(Iterable<ShadowProvider> shadowProviders) {
     33     final Map<String, String> shadowMap = new HashMap<>();
     34     final Map<String, String> shadowPickerMap = new HashMap<>();
     35     for (ShadowProvider provider : shadowProviders) {
     36        shadowMap.putAll(provider.getShadowMap());
     37        shadowPickerMap.putAll(provider.getShadowPickerMap());
     38     }
     39     return new ShadowMap(ImmutableMap.copyOf(shadowMap), Collections.emptyMap(),
     40         ImmutableMap.copyOf(shadowPickerMap));
     41   }
     42 
     43   ShadowMap(ImmutableMap<String, String> defaultShadows, Map<String, ShadowInfo> overriddenShadows) {
     44     this(defaultShadows, overriddenShadows, Collections.emptyMap());
     45   }
     46 
     47   private ShadowMap(ImmutableMap<String, String> defaultShadows,
     48       Map<String, ShadowInfo> overriddenShadows,
     49       Map<String, String> shadowPickers) {
     50     this.defaultShadows = defaultShadows;
     51     this.overriddenShadows = ImmutableMap.copyOf(overriddenShadows);
     52     this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
     53   }
     54 
     55   public ShadowInfo getShadowInfo(Class<?> clazz, int apiLevel) {
     56     String instrumentedClassName = clazz.getName();
     57 
     58     ShadowInfo shadowInfo = overriddenShadows.get(instrumentedClassName);
     59     if (shadowInfo == null) {
     60       shadowInfo = checkShadowPickers(instrumentedClassName, clazz);
     61     }
     62 
     63     if (shadowInfo == null && clazz.getClassLoader() != null) {
     64       try {
     65         final String shadowName = defaultShadows.get(clazz.getCanonicalName());
     66         if (shadowName != null) {
     67           Class<?> shadowClass = clazz.getClassLoader().loadClass(shadowName);
     68           shadowInfo = obtainShadowInfo(shadowClass);
     69           if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
     70             // somehow we got the wrong shadow class?
     71             shadowInfo = null;
     72           }
     73         }
     74       } catch (ClassNotFoundException | IncompatibleClassChangeError e) {
     75         return null;
     76       }
     77     }
     78 
     79     if (shadowInfo != null && !shadowInfo.supportsSdk(apiLevel)) {
     80       return null;
     81     }
     82 
     83     return shadowInfo;
     84   }
     85 
     86   // todo: some caching would probably be nice here...
     87   private ShadowInfo checkShadowPickers(String instrumentedClassName, Class<?> clazz) {
     88     String shadowPickerClassName = shadowPickers.get(instrumentedClassName);
     89     if (shadowPickerClassName == null) {
     90       return null;
     91     }
     92 
     93     ClassLoader classLoader = clazz.getClassLoader();
     94     try {
     95       Class<? extends ShadowPicker<?>> shadowPickerClass =
     96           (Class<? extends ShadowPicker<?>>) classLoader.loadClass(shadowPickerClassName);
     97       ShadowPicker<?> shadowPicker = shadowPickerClass.getDeclaredConstructor().newInstance();
     98       Class<?> selectedShadowClass = shadowPicker.pickShadowClass();
     99       if (selectedShadowClass == null) {
    100         return obtainShadowInfo(Object.class, true);
    101       }
    102       ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);
    103 
    104       if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
    105         throw new IllegalArgumentException("Implemented class for "
    106             + selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
    107             + instrumentedClassName);
    108       }
    109 
    110       return shadowInfo;
    111     } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
    112         | IllegalAccessException | InstantiationException e) {
    113       throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
    114           e);
    115     }
    116   }
    117 
    118   public static ShadowInfo obtainShadowInfo(Class<?> clazz) {
    119     return obtainShadowInfo(clazz, false);
    120   }
    121 
    122   static ShadowInfo obtainShadowInfo(Class<?> clazz, boolean mayBeNonShadow) {
    123     Implements annotation = clazz.getAnnotation(Implements.class);
    124     if (annotation == null) {
    125       if (mayBeNonShadow) {
    126         return null;
    127       } else {
    128         throw new IllegalArgumentException(clazz + " is not annotated with @Implements");
    129       }
    130     }
    131 
    132     String className = annotation.className();
    133     if (className.isEmpty()) {
    134       className = annotation.value().getName();
    135     }
    136     return new ShadowInfo(className, clazz.getName(), annotation);
    137   }
    138 
    139   @SuppressWarnings("ReferenceEquality")
    140   public Set<String> getInvalidatedClasses(ShadowMap previous) {
    141     if (this == previous && shadowPickers.isEmpty()) return Collections.emptySet();
    142 
    143     Map<String, ShadowInfo> invalidated = new HashMap<>(overriddenShadows);
    144 
    145     for (Map.Entry<String, ShadowInfo> entry : previous.overriddenShadows.entrySet()) {
    146       String className = entry.getKey();
    147       ShadowInfo previousConfig = entry.getValue();
    148       ShadowInfo currentConfig = invalidated.get(className);
    149       if (currentConfig == null) {
    150         invalidated.put(className, previousConfig);
    151       } else if (previousConfig.equals(currentConfig)) {
    152         invalidated.remove(className);
    153       }
    154     }
    155 
    156     HashSet<String> classNames = new HashSet<>(invalidated.keySet());
    157     classNames.addAll(shadowPickers.keySet());
    158     return classNames;
    159   }
    160 
    161   /**
    162    * @deprecated do not use
    163    */
    164   @Deprecated
    165   public static String convertToShadowName(String className) {
    166     String shadowClassName =
    167         "org.robolectric.shadows.Shadow" + className.substring(className.lastIndexOf(".") + 1);
    168     shadowClassName = shadowClassName.replaceAll("\\$", "\\$Shadow");
    169     return shadowClassName;
    170   }
    171 
    172   public Builder newBuilder() {
    173     return new Builder(this);
    174   }
    175 
    176   @Override
    177   public boolean equals(Object o) {
    178     if (this == o) return true;
    179     if (!(o instanceof ShadowMap)) return false;
    180 
    181     ShadowMap shadowMap = (ShadowMap) o;
    182 
    183     if (!overriddenShadows.equals(shadowMap.overriddenShadows)) return false;
    184 
    185     return true;
    186   }
    187 
    188   @Override
    189   public int hashCode() {
    190     return overriddenShadows.hashCode();
    191   }
    192 
    193   public static class Builder {
    194     private final ImmutableMap<String, String> defaultShadows;
    195     private final Map<String, ShadowInfo> overriddenShadows;
    196     private final Map<String, String> shadowPickers;
    197 
    198     public Builder () {
    199       defaultShadows = ImmutableMap.of();
    200       overriddenShadows = new HashMap<>();
    201       shadowPickers = new HashMap<>();
    202     }
    203 
    204     public Builder(ShadowMap shadowMap) {
    205       this.defaultShadows = shadowMap.defaultShadows;
    206       this.overriddenShadows = new HashMap<>(shadowMap.overriddenShadows);
    207       this.shadowPickers = new HashMap<>(shadowMap.shadowPickers);
    208     }
    209 
    210     public Builder addShadowClasses(Class<?>... shadowClasses) {
    211       for (Class<?> shadowClass : shadowClasses) {
    212         addShadowClass(shadowClass);
    213       }
    214       return this;
    215     }
    216 
    217     Builder addShadowClass(Class<?> shadowClass) {
    218       addShadowInfo(obtainShadowInfo(shadowClass));
    219       return this;
    220     }
    221 
    222     Builder addShadowClass(
    223         String realClassName,
    224         String shadowClassName,
    225         boolean callThroughByDefault,
    226         boolean looseSignatures) {
    227       addShadowInfo(
    228           new ShadowInfo(
    229               realClassName, shadowClassName, callThroughByDefault, looseSignatures, -1, -1, null));
    230       return this;
    231     }
    232 
    233     private void addShadowInfo(ShadowInfo shadowInfo) {
    234       overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
    235       if (shadowInfo.hasShadowPicker()) {
    236         shadowPickers
    237             .put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
    238       }
    239     }
    240 
    241     public ShadowMap build() {
    242       return new ShadowMap(defaultShadows, overriddenShadows, shadowPickers);
    243     }
    244   }
    245 }
    246