blob: f1649cc99e79313040c2602b97c65071886c4af7 [file] [log] [blame]
package org.robolectric.internal.bytecode;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.robolectric.annotation.Implements;
import org.robolectric.internal.ShadowProvider;
import org.robolectric.shadow.api.ShadowPicker;
/**
* Maps from instrumented class to shadow class.
*
* We deal with class names rather than actual classes here, since a ShadowMap is built outside of
* any sandboxes, but instrumented and shadowed classes must be loaded through a
* {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
*
* Once constructed, instances are immutable.
*/
@SuppressWarnings("NewApi")
public class ShadowMap {
static final ShadowMap EMPTY = new ShadowMap(ImmutableMap.of(), ImmutableMap.of());
private final ImmutableMap<String, String> defaultShadows;
private final ImmutableMap<String, ShadowInfo> overriddenShadows;
private final ImmutableMap<String, String> shadowPickers;
public static ShadowMap createFromShadowProviders(Iterable<ShadowProvider> shadowProviders) {
final Map<String, String> shadowMap = new HashMap<>();
final Map<String, String> shadowPickerMap = new HashMap<>();
for (ShadowProvider provider : shadowProviders) {
shadowMap.putAll(provider.getShadowMap());
shadowPickerMap.putAll(provider.getShadowPickerMap());
}
return new ShadowMap(ImmutableMap.copyOf(shadowMap), Collections.emptyMap(),
ImmutableMap.copyOf(shadowPickerMap));
}
ShadowMap(ImmutableMap<String, String> defaultShadows, Map<String, ShadowInfo> overriddenShadows) {
this(defaultShadows, overriddenShadows, Collections.emptyMap());
}
private ShadowMap(ImmutableMap<String, String> defaultShadows,
Map<String, ShadowInfo> overriddenShadows,
Map<String, String> shadowPickers) {
this.defaultShadows = defaultShadows;
this.overriddenShadows = ImmutableMap.copyOf(overriddenShadows);
this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
}
public ShadowInfo getShadowInfo(Class<?> clazz, int apiLevel) {
String instrumentedClassName = clazz.getName();
ShadowInfo shadowInfo = overriddenShadows.get(instrumentedClassName);
if (shadowInfo == null) {
shadowInfo = checkShadowPickers(instrumentedClassName, clazz);
}
if (shadowInfo == null && clazz.getClassLoader() != null) {
try {
final String shadowName = defaultShadows.get(clazz.getCanonicalName());
if (shadowName != null) {
Class<?> shadowClass = clazz.getClassLoader().loadClass(shadowName);
shadowInfo = obtainShadowInfo(shadowClass);
if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
// somehow we got the wrong shadow class?
shadowInfo = null;
}
}
} catch (ClassNotFoundException | IncompatibleClassChangeError e) {
return null;
}
}
if (shadowInfo != null && !shadowInfo.supportsSdk(apiLevel)) {
return null;
}
return shadowInfo;
}
// todo: some caching would probably be nice here...
private ShadowInfo checkShadowPickers(String instrumentedClassName, Class<?> clazz) {
String shadowPickerClassName = shadowPickers.get(instrumentedClassName);
if (shadowPickerClassName == null) {
return null;
}
ClassLoader classLoader = clazz.getClassLoader();
try {
Class<? extends ShadowPicker<?>> shadowPickerClass =
(Class<? extends ShadowPicker<?>>) classLoader.loadClass(shadowPickerClassName);
ShadowPicker<?> shadowPicker = shadowPickerClass.getDeclaredConstructor().newInstance();
Class<?> selectedShadowClass = shadowPicker.pickShadowClass();
if (selectedShadowClass == null) {
return obtainShadowInfo(Object.class, true);
}
ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);
if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
throw new IllegalArgumentException("Implemented class for "
+ selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
+ instrumentedClassName);
}
return shadowInfo;
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
| IllegalAccessException | InstantiationException e) {
throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
e);
}
}
public static ShadowInfo obtainShadowInfo(Class<?> clazz) {
return obtainShadowInfo(clazz, false);
}
static ShadowInfo obtainShadowInfo(Class<?> clazz, boolean mayBeNonShadow) {
Implements annotation = clazz.getAnnotation(Implements.class);
if (annotation == null) {
if (mayBeNonShadow) {
return null;
} else {
throw new IllegalArgumentException(clazz + " is not annotated with @Implements");
}
}
String className = annotation.className();
if (className.isEmpty()) {
className = annotation.value().getName();
}
return new ShadowInfo(className, clazz.getName(), annotation);
}
@SuppressWarnings("ReferenceEquality")
public Set<String> getInvalidatedClasses(ShadowMap previous) {
if (this == previous && shadowPickers.isEmpty()) return Collections.emptySet();
Map<String, ShadowInfo> invalidated = new HashMap<>(overriddenShadows);
for (Map.Entry<String, ShadowInfo> entry : previous.overriddenShadows.entrySet()) {
String className = entry.getKey();
ShadowInfo previousConfig = entry.getValue();
ShadowInfo currentConfig = invalidated.get(className);
if (currentConfig == null) {
invalidated.put(className, previousConfig);
} else if (previousConfig.equals(currentConfig)) {
invalidated.remove(className);
}
}
return invalidated.keySet();
}
/**
* @deprecated do not use
*/
@Deprecated
public static String convertToShadowName(String className) {
String shadowClassName =
"org.robolectric.shadows.Shadow" + className.substring(className.lastIndexOf(".") + 1);
shadowClassName = shadowClassName.replaceAll("\\$", "\\$Shadow");
return shadowClassName;
}
public Builder newBuilder() {
return new Builder(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ShadowMap)) return false;
ShadowMap shadowMap = (ShadowMap) o;
if (!overriddenShadows.equals(shadowMap.overriddenShadows)) return false;
return true;
}
@Override
public int hashCode() {
return overriddenShadows.hashCode();
}
public static class Builder {
private final ImmutableMap<String, String> defaultShadows;
private final Map<String, ShadowInfo> overriddenShadows;
private final Map<String, String> shadowPickers;
public Builder () {
defaultShadows = ImmutableMap.of();
overriddenShadows = new HashMap<>();
shadowPickers = new HashMap<>();
}
public Builder(ShadowMap shadowMap) {
this.defaultShadows = shadowMap.defaultShadows;
this.overriddenShadows = new HashMap<>(shadowMap.overriddenShadows);
this.shadowPickers = new HashMap<>(shadowMap.shadowPickers);
}
public Builder addShadowClasses(Class<?>... shadowClasses) {
for (Class<?> shadowClass : shadowClasses) {
addShadowClass(shadowClass);
}
return this;
}
Builder addShadowClass(Class<?> shadowClass) {
addShadowInfo(obtainShadowInfo(shadowClass));
return this;
}
Builder addShadowClass(
String realClassName,
String shadowClassName,
boolean callThroughByDefault,
boolean looseSignatures) {
addShadowInfo(
new ShadowInfo(
realClassName, shadowClassName, callThroughByDefault, looseSignatures, -1, -1, null));
return this;
}
private void addShadowInfo(ShadowInfo shadowInfo) {
overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
if (shadowInfo.hasShadowPicker()) {
shadowPickers
.put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
}
}
public ShadowMap build() {
return new ShadowMap(defaultShadows, overriddenShadows, shadowPickers);
}
}
}