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 
createFromShadowProviders(Iterable<ShadowProvider> shadowProviders)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 
ShadowMap(ImmutableMap<String, String> defaultShadows, Map<String, ShadowInfo> overriddenShadows)43   ShadowMap(ImmutableMap<String, String> defaultShadows, Map<String, ShadowInfo> overriddenShadows) {
44     this(defaultShadows, overriddenShadows, Collections.emptyMap());
45   }
46 
ShadowMap(ImmutableMap<String, String> defaultShadows, Map<String, ShadowInfo> overriddenShadows, Map<String, String> shadowPickers)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 
getShadowInfo(Class<?> clazz, int apiLevel)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...
checkShadowPickers(String instrumentedClassName, Class<?> clazz)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 
obtainShadowInfo(Class<?> clazz)118   public static ShadowInfo obtainShadowInfo(Class<?> clazz) {
119     return obtainShadowInfo(clazz, false);
120   }
121 
obtainShadowInfo(Class<?> clazz, boolean mayBeNonShadow)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")
getInvalidatedClasses(ShadowMap previous)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
convertToShadowName(String className)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 
newBuilder()172   public Builder newBuilder() {
173     return new Builder(this);
174   }
175 
176   @Override
equals(Object o)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
hashCode()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 
Builder()198     public Builder () {
199       defaultShadows = ImmutableMap.of();
200       overriddenShadows = new HashMap<>();
201       shadowPickers = new HashMap<>();
202     }
203 
Builder(ShadowMap shadowMap)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 
addShadowClasses(Class<?>.... shadowClasses)210     public Builder addShadowClasses(Class<?>... shadowClasses) {
211       for (Class<?> shadowClass : shadowClasses) {
212         addShadowClass(shadowClass);
213       }
214       return this;
215     }
216 
addShadowClass(Class<?> shadowClass)217     Builder addShadowClass(Class<?> shadowClass) {
218       addShadowInfo(obtainShadowInfo(shadowClass));
219       return this;
220     }
221 
addShadowClass( String realClassName, String shadowClassName, boolean callThroughByDefault, boolean looseSignatures)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 
addShadowInfo(ShadowInfo shadowInfo)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 
build()241     public ShadowMap build() {
242       return new ShadowMap(defaultShadows, overriddenShadows, shadowPickers);
243     }
244   }
245 }
246