blob: 401cae184a96114016e6c7245859831889969d09 [file] [log] [blame]
Erich Douglass1874abe2014-12-17 19:22:16 -08001package org.robolectric.internal.bytecode;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -08002
Michael Hoisie7e005cf2022-05-24 10:47:07 -07003import com.google.common.collect.ArrayListMultimap;
4import com.google.common.collect.ImmutableList;
5import com.google.common.collect.ImmutableListMultimap;
Christian Williams95511512018-01-05 16:43:12 -08006import com.google.common.collect.ImmutableMap;
christianwb15df5a2018-08-07 11:59:33 -07007import java.lang.reflect.InvocationTargetException;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -08008import java.util.Collections;
9import java.util.HashMap;
christianw0e4184c2019-03-06 13:32:35 -080010import java.util.List;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -080011import java.util.Map;
Christian Williams851f2a92017-07-25 18:12:59 -070012import java.util.Set;
13import org.robolectric.annotation.Implements;
14import org.robolectric.internal.ShadowProvider;
Christian Williamsf5371b52020-03-06 11:19:38 -080015import org.robolectric.sandbox.ShadowMatcher;
christianwb15df5a2018-08-07 11:59:33 -070016import org.robolectric.shadow.api.ShadowPicker;
Rex Hoffman9b763742022-11-15 20:26:41 +000017import org.robolectric.util.Logger;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -080018
Christian Williamse71f9192018-01-08 13:17:59 -080019/**
20 * Maps from instrumented class to shadow class.
21 *
22 * We deal with class names rather than actual classes here, since a ShadowMap is built outside of
23 * any sandboxes, but instrumented and shadowed classes must be loaded through a
24 * {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
25 *
26 * Once constructed, instances are immutable.
27 */
christianwb15df5a2018-08-07 11:59:33 -070028@SuppressWarnings("NewApi")
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -080029public class ShadowMap {
christianwb15df5a2018-08-07 11:59:33 -070030
Michael Hoisie7e005cf2022-05-24 10:47:07 -070031 static final ShadowMap EMPTY = new ShadowMap(ImmutableListMultimap.of(), ImmutableMap.of());
Erich Douglass9a8b34e2015-06-16 23:05:12 -070032
Michael Hoisie7e005cf2022-05-24 10:47:07 -070033 private final ImmutableListMultimap<String, String> defaultShadows;
Christian Williamse71f9192018-01-08 13:17:59 -080034 private final ImmutableMap<String, ShadowInfo> overriddenShadows;
christianwb15df5a2018-08-07 11:59:33 -070035 private final ImmutableMap<String, String> shadowPickers;
Christian Williamse71f9192018-01-08 13:17:59 -080036
brettchabot57254272019-02-13 01:49:27 -080037 @SuppressWarnings("AndroidJdkLibsChecker")
christianw0e4184c2019-03-06 13:32:35 -080038 public static ShadowMap createFromShadowProviders(List<ShadowProvider> sortedProviders) {
Michael Hoisie7e005cf2022-05-24 10:47:07 -070039 final ArrayListMultimap<String, String> shadowMap = ArrayListMultimap.create();
christianwb15df5a2018-08-07 11:59:33 -070040 final Map<String, String> shadowPickerMap = new HashMap<>();
brettchabot57254272019-02-13 01:49:27 -080041
Michael Hoisieaab24ad2022-10-06 00:18:05 -070042 // These are sorted in descending order (higher priority providers are first).
Rex Hoffman9b763742022-11-15 20:26:41 +000043 Logger.debug("Shadow providers: " + sortedProviders);
brettchabot57254272019-02-13 01:49:27 -080044 for (ShadowProvider provider : sortedProviders) {
Rex Hoffman9b763742022-11-15 20:26:41 +000045 Logger.debug("Shadow provider: " + provider.getClass().getName());
Michael Hoisie7e005cf2022-05-24 10:47:07 -070046 for (Map.Entry<String, String> entry : provider.getShadows()) {
47 shadowMap.put(entry.getKey(), entry.getValue());
48 }
Michael Hoisieaab24ad2022-10-06 00:18:05 -070049 provider.getShadowPickerMap().forEach(shadowPickerMap::putIfAbsent);
Erich Douglass9a8b34e2015-06-16 23:05:12 -070050 }
Michael Hoisie7e005cf2022-05-24 10:47:07 -070051 return new ShadowMap(
52 ImmutableListMultimap.copyOf(shadowMap),
53 Collections.emptyMap(),
christianwb15df5a2018-08-07 11:59:33 -070054 ImmutableMap.copyOf(shadowPickerMap));
Erich Douglass9a8b34e2015-06-16 23:05:12 -070055 }
Christian Williams29a83592013-05-13 15:05:09 -070056
christianw0e4184c2019-03-06 13:32:35 -080057 ShadowMap(
Michael Hoisie7e005cf2022-05-24 10:47:07 -070058 ImmutableListMultimap<String, String> defaultShadows,
christianw0e4184c2019-03-06 13:32:35 -080059 Map<String, ShadowInfo> overriddenShadows) {
christianwb15df5a2018-08-07 11:59:33 -070060 this(defaultShadows, overriddenShadows, Collections.emptyMap());
61 }
62
Michael Hoisie7e005cf2022-05-24 10:47:07 -070063 private ShadowMap(
64 ImmutableListMultimap<String, String> defaultShadows,
christianwb15df5a2018-08-07 11:59:33 -070065 Map<String, ShadowInfo> overriddenShadows,
66 Map<String, String> shadowPickers) {
Michael Hoisie7e005cf2022-05-24 10:47:07 -070067 this.defaultShadows = ImmutableListMultimap.copyOf(defaultShadows);
Christian Williamse71f9192018-01-08 13:17:59 -080068 this.overriddenShadows = ImmutableMap.copyOf(overriddenShadows);
christianwb15df5a2018-08-07 11:59:33 -070069 this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
Christian Williams29a83592013-05-13 15:05:09 -070070 }
71
Christian Williamsf5371b52020-03-06 11:19:38 -080072 public ShadowInfo getShadowInfo(Class<?> clazz, ShadowMatcher shadowMatcher) {
Christian Williamse71f9192018-01-08 13:17:59 -080073 String instrumentedClassName = clazz.getName();
christianwb15df5a2018-08-07 11:59:33 -070074
Christian Williamse71f9192018-01-08 13:17:59 -080075 ShadowInfo shadowInfo = overriddenShadows.get(instrumentedClassName);
christianwb15df5a2018-08-07 11:59:33 -070076 if (shadowInfo == null) {
77 shadowInfo = checkShadowPickers(instrumentedClassName, clazz);
christianw1caa33a2019-04-05 14:26:24 -070078 } else if (shadowInfo.hasShadowPicker()) {
79 shadowInfo = pickShadow(instrumentedClassName, clazz, shadowInfo);
christianwb15df5a2018-08-07 11:59:33 -070080 }
Erich Douglass9a8b34e2015-06-16 23:05:12 -070081
Christian Williamse71f9192018-01-08 13:17:59 -080082 if (shadowInfo == null && clazz.getClassLoader() != null) {
83 try {
Michael Hoisie7e005cf2022-05-24 10:47:07 -070084 final ImmutableList<String> shadowNames = defaultShadows.get(clazz.getCanonicalName());
85 for (String shadowName : shadowNames) {
86 if (shadowName != null) {
87 Class<?> shadowClass = clazz.getClassLoader().loadClass(shadowName);
88 shadowInfo = obtainShadowInfo(shadowClass);
89 if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
90 // somehow we got the wrong shadow class?
91 shadowInfo = null;
92 }
93 if (shadowInfo != null && shadowMatcher.matches(shadowInfo)) {
94 return shadowInfo;
95 }
Christian Williamse71f9192018-01-08 13:17:59 -080096 }
Erich Douglass9a8b34e2015-06-16 23:05:12 -070097 }
Christian Williamse71f9192018-01-08 13:17:59 -080098 } catch (ClassNotFoundException | IncompatibleClassChangeError e) {
99 return null;
Erich Douglass9a8b34e2015-06-16 23:05:12 -0700100 }
Christian Williamse71f9192018-01-08 13:17:59 -0800101 }
102
Christian Williamsf5371b52020-03-06 11:19:38 -0800103 if (shadowInfo != null && !shadowMatcher.matches(shadowInfo)) {
Erich Douglass9a8b34e2015-06-16 23:05:12 -0700104 return null;
105 }
Christian Williamse71f9192018-01-08 13:17:59 -0800106
107 return shadowInfo;
Erich Douglass9a8b34e2015-06-16 23:05:12 -0700108 }
109
christianwb15df5a2018-08-07 11:59:33 -0700110 // todo: some caching would probably be nice here...
111 private ShadowInfo checkShadowPickers(String instrumentedClassName, Class<?> clazz) {
112 String shadowPickerClassName = shadowPickers.get(instrumentedClassName);
113 if (shadowPickerClassName == null) {
114 return null;
115 }
116
christianw1caa33a2019-04-05 14:26:24 -0700117 return pickShadow(instrumentedClassName, clazz, shadowPickerClassName);
118 }
119
120 private ShadowInfo pickShadow(String instrumentedClassName, Class<?> clazz,
121 String shadowPickerClassName) {
122 ClassLoader sandboxClassLoader = clazz.getClassLoader();
christianwb15df5a2018-08-07 11:59:33 -0700123 try {
124 Class<? extends ShadowPicker<?>> shadowPickerClass =
christianw1caa33a2019-04-05 14:26:24 -0700125 (Class<? extends ShadowPicker<?>>) sandboxClassLoader.loadClass(shadowPickerClassName);
christianwb15df5a2018-08-07 11:59:33 -0700126 ShadowPicker<?> shadowPicker = shadowPickerClass.getDeclaredConstructor().newInstance();
christianw23ea4942018-08-16 11:53:35 -0700127 Class<?> selectedShadowClass = shadowPicker.pickShadowClass();
128 if (selectedShadowClass == null) {
129 return obtainShadowInfo(Object.class, true);
130 }
131 ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);
christianwb15df5a2018-08-07 11:59:33 -0700132
133 if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
134 throw new IllegalArgumentException("Implemented class for "
christianw23ea4942018-08-16 11:53:35 -0700135 + selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
christianwb15df5a2018-08-07 11:59:33 -0700136 + instrumentedClassName);
137 }
138
139 return shadowInfo;
140 } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
141 | IllegalAccessException | InstantiationException e) {
142 throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
143 e);
144 }
145 }
146
christianw1caa33a2019-04-05 14:26:24 -0700147 private ShadowInfo pickShadow(
148 String instrumentedClassName, Class<?> clazz, ShadowInfo shadowInfo) {
149 return pickShadow(instrumentedClassName, clazz, shadowInfo.getShadowPickerClass().getName());
150 }
151
Christian Williamse71f9192018-01-08 13:17:59 -0800152 public static ShadowInfo obtainShadowInfo(Class<?> clazz) {
Christian Williams1375d8d2018-01-24 14:59:32 -0800153 return obtainShadowInfo(clazz, false);
154 }
155
156 static ShadowInfo obtainShadowInfo(Class<?> clazz, boolean mayBeNonShadow) {
Erich Douglass9a8b34e2015-06-16 23:05:12 -0700157 Implements annotation = clazz.getAnnotation(Implements.class);
158 if (annotation == null) {
Christian Williams1375d8d2018-01-24 14:59:32 -0800159 if (mayBeNonShadow) {
160 return null;
161 } else {
162 throw new IllegalArgumentException(clazz + " is not annotated with @Implements");
163 }
Erich Douglass9a8b34e2015-06-16 23:05:12 -0700164 }
165
166 String className = annotation.className();
167 if (className.isEmpty()) {
168 className = annotation.value().getName();
169 }
Christian Williamse71f9192018-01-08 13:17:59 -0800170 return new ShadowInfo(className, clazz.getName(), annotation);
Christian Williams29a83592013-05-13 15:05:09 -0700171 }
172
Christian Williams07e61e02017-10-06 16:56:08 -0700173 @SuppressWarnings("ReferenceEquality")
Alexander Blom10213152015-04-22 12:33:45 +0200174 public Set<String> getInvalidatedClasses(ShadowMap previous) {
christianwb15df5a2018-08-07 11:59:33 -0700175 if (this == previous && shadowPickers.isEmpty()) return Collections.emptySet();
Alexander Blom10213152015-04-22 12:33:45 +0200176
christianwb15df5a2018-08-07 11:59:33 -0700177 Map<String, ShadowInfo> invalidated = new HashMap<>(overriddenShadows);
Alexander Blom10213152015-04-22 12:33:45 +0200178
Christian Williamse71f9192018-01-08 13:17:59 -0800179 for (Map.Entry<String, ShadowInfo> entry : previous.overriddenShadows.entrySet()) {
Alexander Blom10213152015-04-22 12:33:45 +0200180 String className = entry.getKey();
Christian Williamse71f9192018-01-08 13:17:59 -0800181 ShadowInfo previousConfig = entry.getValue();
182 ShadowInfo currentConfig = invalidated.get(className);
Alexander Blom10213152015-04-22 12:33:45 +0200183 if (currentConfig == null) {
184 invalidated.put(className, previousConfig);
185 } else if (previousConfig.equals(currentConfig)) {
186 invalidated.remove(className);
187 }
188 }
189
Christian Williams6b08d4a2019-02-01 13:50:07 -0800190 return invalidated.keySet();
Alexander Blom10213152015-04-22 12:33:45 +0200191 }
192
Christian Williams3f024452018-01-05 16:57:16 -0800193 /**
194 * @deprecated do not use
195 */
196 @Deprecated
Alexander Blom10213152015-04-22 12:33:45 +0200197 public static String convertToShadowName(String className) {
198 String shadowClassName =
199 "org.robolectric.shadows.Shadow" + className.substring(className.lastIndexOf(".") + 1);
200 shadowClassName = shadowClassName.replaceAll("\\$", "\\$Shadow");
201 return shadowClassName;
202 }
203
Christian Williams29a83592013-05-13 15:05:09 -0700204 public Builder newBuilder() {
205 return new Builder(this);
206 }
207
208 @Override
209 public boolean equals(Object o) {
210 if (this == o) return true;
jongerrishfaf97f22018-08-21 15:07:20 -0700211 if (!(o instanceof ShadowMap)) return false;
Christian Williams29a83592013-05-13 15:05:09 -0700212
213 ShadowMap shadowMap = (ShadowMap) o;
214
Christian Williamse71f9192018-01-08 13:17:59 -0800215 if (!overriddenShadows.equals(shadowMap.overriddenShadows)) return false;
Christian Williams29a83592013-05-13 15:05:09 -0700216
217 return true;
218 }
219
220 @Override
221 public int hashCode() {
Christian Williamse71f9192018-01-08 13:17:59 -0800222 return overriddenShadows.hashCode();
Christian Williams29a83592013-05-13 15:05:09 -0700223 }
224
225 public static class Builder {
Michael Hoisie7e005cf2022-05-24 10:47:07 -0700226 private final ImmutableListMultimap<String, String> defaultShadows;
Christian Williamse71f9192018-01-08 13:17:59 -0800227 private final Map<String, ShadowInfo> overriddenShadows;
christianwb15df5a2018-08-07 11:59:33 -0700228 private final Map<String, String> shadowPickers;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -0800229
Christian Williamse71f9192018-01-08 13:17:59 -0800230 public Builder () {
Michael Hoisie7e005cf2022-05-24 10:47:07 -0700231 defaultShadows = ImmutableListMultimap.of();
Christian Williamse71f9192018-01-08 13:17:59 -0800232 overriddenShadows = new HashMap<>();
christianwb15df5a2018-08-07 11:59:33 -0700233 shadowPickers = new HashMap<>();
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -0800234 }
235
Christian Williams29a83592013-05-13 15:05:09 -0700236 public Builder(ShadowMap shadowMap) {
Christian Williamse71f9192018-01-08 13:17:59 -0800237 this.defaultShadows = shadowMap.defaultShadows;
238 this.overriddenShadows = new HashMap<>(shadowMap.overriddenShadows);
christianwb15df5a2018-08-07 11:59:33 -0700239 this.shadowPickers = new HashMap<>(shadowMap.shadowPickers);
Christian Williams + Kiran Ryali4306d0d2013-04-05 13:20:17 -0700240 }
241
Christian Williams29a83592013-05-13 15:05:09 -0700242 public Builder addShadowClasses(Class<?>... shadowClasses) {
243 for (Class<?> shadowClass : shadowClasses) {
244 addShadowClass(shadowClass);
245 }
246 return this;
Christian Williams + Kiran Ryali4306d0d2013-04-05 13:20:17 -0700247 }
248
Christian Williamsd214ef02018-10-04 17:16:49 -0700249 Builder addShadowClass(Class<?> shadowClass) {
Christian Williamse71f9192018-01-08 13:17:59 -0800250 addShadowInfo(obtainShadowInfo(shadowClass));
Christian Williams29a83592013-05-13 15:05:09 -0700251 return this;
Christian Williams36d87d72013-03-13 18:25:54 -0700252 }
253
Christian Williamsd214ef02018-10-04 17:16:49 -0700254 Builder addShadowClass(
jongerrishfaf97f22018-08-21 15:07:20 -0700255 String realClassName,
256 String shadowClassName,
257 boolean callThroughByDefault,
Christian Williamsf7e46ea2018-01-17 15:09:34 -0800258 boolean looseSignatures) {
jongerrishfaf97f22018-08-21 15:07:20 -0700259 addShadowInfo(
260 new ShadowInfo(
261 realClassName, shadowClassName, callThroughByDefault, looseSignatures, -1, -1, null));
Christian Williams29a83592013-05-13 15:05:09 -0700262 return this;
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -0800263 }
264
Christian Williamse71f9192018-01-08 13:17:59 -0800265 private void addShadowInfo(ShadowInfo shadowInfo) {
266 overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
christianwb15df5a2018-08-07 11:59:33 -0700267 if (shadowInfo.hasShadowPicker()) {
268 shadowPickers
269 .put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
270 }
Christian Williamsa1c48302013-03-18 14:44:04 -0700271 }
272
Christian Williams0d4f5f62018-08-06 14:18:45 -0700273 public ShadowMap build() {
christianwb15df5a2018-08-07 11:59:33 -0700274 return new ShadowMap(defaultShadows, overriddenShadows, shadowPickers);
Christian Williamsa1c48302013-03-18 14:44:04 -0700275 }
Christian Williams29a83592013-05-13 15:05:09 -0700276 }
Kiran Ryali + Christian Williams7be4d2b2013-03-01 18:10:16 -0800277}