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