package org.robolectric.annotation.processing;
import com.google.common.base.Equivalence;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.processing.validator.ImplementsValidator;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.AnnotationValueVisitor;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleAnnotationValueVisitor6;
import javax.lang.model.util.SimpleElementVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newTreeMap;
import static com.google.common.collect.Sets.newTreeSet;
/**
* Model describing the Robolectric source file.
*/
public class RobolectricModel {
private static FQComparator fqComparator = new FQComparator();
private static SimpleComparator comparator = new SimpleComparator();
/** TypeElement representing the Robolectric.Anything interface, or null if the element isn't found. */
final TypeElement ANYTHING;
/** TypeMirror representing the Robolectric.Anything interface, or null if the element isn't found. */
public final TypeMirror ANYTHING_MIRROR;
/** TypeMirror representing the Object class. */
final TypeMirror OBJECT_MIRROR;
/** TypeElement representing the @Implements annotation. */
final TypeElement IMPLEMENTS;
/** PackageElement representing the java.lang package. */
final PackageElement JAVA_LANG;
/** Convenience reference for the processing environment's elements utilities. */
private final Elements elements;
/** Convenience reference for the processing environment's types utilities. */
private final Types types;
private HashMap<TypeElement,String> referentMap = newHashMap();
private HashMultimap<String,TypeElement> typeMap = HashMultimap.create();
private HashMap<TypeElement,TypeElement> importMap = newHashMap();
private TreeMap<TypeElement,TypeElement> shadowTypes = newTreeMap(fqComparator);
private TreeMap<String, String> extraShadowTypes = newTreeMap();
private TreeSet<String> imports = newTreeSet();
private TreeMap<TypeElement,ExecutableElement> resetterMap = newTreeMap(comparator);
private final Map<String, DocumentedPackage> documentedPackages = new TreeMap<>();
public Collection<DocumentedPackage> getDocumentedPackages() {
return documentedPackages.values();
}
public void documentPackage(String name, String documentation) {
getDocumentedPackage(name).setDocumentation(documentation);
}
private DocumentedPackage getDocumentedPackage(String name) {
DocumentedPackage documentedPackage = documentedPackages.get(name);
if (documentedPackage == null) {
documentedPackage = new DocumentedPackage(name);
documentedPackages.put(name, documentedPackage);
}
return documentedPackage;
}
private DocumentedPackage getDocumentedPackage(TypeElement type) {
Element pkgElement = type.getEnclosingElement();
return getDocumentedPackage(pkgElement.toString());
}
public void documentType(TypeElement type, String documentation, List<String> imports) {
DocumentedType documentedType = getDocumentedType(type);
documentedType.setDocumentation(documentation);
documentedType.imports = imports;
}
private DocumentedType getDocumentedType(TypeElement type) {
DocumentedPackage documentedPackage = getDocumentedPackage(type);
return documentedPackage.getDocumentedType(type.getQualifiedName().toString());
}
public void documentMethod(TypeElement shadowClass, DocumentedMethod documentedMethod) {
DocumentedType documentedType = getDocumentedType(shadowClass);
documentedType.methods.put(documentedMethod.getName(), documentedMethod);
}
private static class FQComparator implements Comparator<TypeElement> {
@Override
public int compare(TypeElement o1, TypeElement o2) {
return o1.getQualifiedName().toString().compareTo(o2.getQualifiedName().toString());
}
}
private static class SimpleComparator implements Comparator<Element> {
@Override
public int compare(Element o1, Element o2) {
return o1.getSimpleName().toString().compareTo(o2.getSimpleName().toString());
}
}
public RobolectricModel(Elements elements, Types types) {
this.elements = elements;
this.types = types;
ANYTHING = elements.getTypeElement("org.robolectric.Robolectric.Anything");
ANYTHING_MIRROR = ANYTHING == null ? null : ANYTHING.asType();
// FIXME: check this type lookup for NPEs (and also the ones in the
// validators)
IMPLEMENTS = elements.getTypeElement(ImplementsValidator.IMPLEMENTS_CLASS);
JAVA_LANG = elements.getPackageElement("java.lang");
OBJECT_MIRROR = elements.getTypeElement(Object.class.getCanonicalName()).asType();
notObject = new Predicate<TypeMirror>() {
@Override
public boolean apply(TypeMirror t) {
return !RobolectricModel.this.types.isSameType(t, OBJECT_MIRROR);
}
};
}
public AnnotationMirror getAnnotationMirror(Element element, TypeElement annotation) {
TypeMirror expectedType = annotation.asType();
for (AnnotationMirror m : element.getAnnotationMirrors()) {
if (types.isSameType(expectedType, m.getAnnotationType())) {
return m;
}
}
return null;
}
public static ElementVisitor<TypeElement,Void> typeVisitor = new SimpleElementVisitor6<TypeElement,Void>() {
@Override
public TypeElement visitType(TypeElement e, Void p) {
return e;
}
};
public static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String key) {
for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet() ) {
if (entry.getKey().getSimpleName().toString().equals(key)) {
return entry.getValue();
}
}
return null;
}
public static AnnotationValueVisitor<TypeMirror,Void> valueVisitor = new SimpleAnnotationValueVisitor6<TypeMirror,Void>() {
@Override
public TypeMirror visitType(TypeMirror t, Void arg) {
return t;
}
};
public static AnnotationValueVisitor<String, Void> classNameVisitor = new SimpleAnnotationValueVisitor6<String, Void>() {
@Override
public String visitString(String s, Void arg) {
return s;
}
};
public static AnnotationValueVisitor<Integer, Void> intVisitor = new SimpleAnnotationValueVisitor6<Integer, Void>() {
@Override
public Integer visitInt(int i, Void aVoid) {
return i;
}
};
public AnnotationMirror getImplementsMirror(Element elem) {
return getAnnotationMirror(elem, IMPLEMENTS);
}
private TypeMirror getImplementedClassName(AnnotationMirror am) {
AnnotationValue className = getAnnotationValue(am, "className");
if (className == null) {
return null;
}
String classNameString = classNameVisitor.visit(className);
if (classNameString == null) {
return null;
}
TypeElement impElement = elements.getTypeElement(classNameString.replace('$', '.'));
if (impElement == null) {
return null;
}
return impElement.asType();
}
public TypeMirror getImplementedClass(AnnotationMirror am) {
if (am == null) {
return null;
}
// RobolectricWiringTest prefers className (if provided) to value, so we do the same here.
TypeMirror impType = getImplementedClassName(am);
if (impType != null) {
return impType;
}
AnnotationValue av = getAnnotationValue(am, "value");
if (av == null) {
return null;
}
TypeMirror type = valueVisitor.visit(av);
if (type == null) {
return null;
}
// If the class is Robolectric.Anything, treat as if it wasn't specified at all.
if (ANYTHING_MIRROR != null && types.isSameType(type, ANYTHING_MIRROR)) {
return null;
}
return type;
}
private static ElementVisitor<TypeElement,Void> typeElementVisitor = new SimpleElementVisitor6<TypeElement,Void>() {
@Override
public TypeElement visitType(TypeElement e, Void p) {
return e;
}
};
private void registerType(TypeElement type) {
if (!Objects.equal(ANYTHING, type) && !importMap.containsKey(type)) {
typeMap.put(type.getSimpleName().toString(), type);
importMap.put(type, type);
for (TypeParameterElement typeParam : type.getTypeParameters()) {
for (TypeMirror bound : typeParam.getBounds()) {
// FIXME: get rid of cast using a visitor
TypeElement boundElement = typeElementVisitor.visit(types.asElement(bound));
registerType(boundElement);
}
}
}
}
/**
* Prepares the various derived parts of the model based on the class mappings
* that have been registered to date.
*/
public void prepare() {
for (Map.Entry<TypeElement,TypeElement> entry : getVisibleShadowTypes().entrySet()) {
final TypeElement shadowType = entry.getKey();
registerType(shadowType);
final TypeElement solidType = entry.getValue();
registerType(solidType);
}
for (Map.Entry<TypeElement,TypeElement> entry : getResetterShadowTypes().entrySet()) {
final TypeElement shadowType = entry.getKey();
registerType(shadowType);
}
while (!typeMap.isEmpty()) {
final HashMultimap<String,TypeElement> nextRound = HashMultimap.create();
for (Map.Entry<String, Set<TypeElement>> referents : Multimaps.asMap(typeMap).entrySet()) {
final Set<TypeElement> c = referents.getValue();
// If there is only one type left with the given simple
// name, then
if (c.size() == 1) {
final TypeElement type = c.iterator().next();
referentMap.put(type, referents.getKey());
}
else {
for (TypeElement type : c) {
SimpleElementVisitor6<Void,TypeElement> visitor = new SimpleElementVisitor6<Void,TypeElement>() {
@Override
public Void visitType(TypeElement parent, TypeElement type) {
nextRound.put(parent.getSimpleName() + "." + type.getSimpleName(), type);
importMap.put(type, parent);
return null;
}
@Override
public Void visitPackage(PackageElement parent, TypeElement type) {
referentMap.put(type, type.getQualifiedName().toString());
importMap.remove(type);
return null;
}
};
visitor.visit(importMap.get(type).getEnclosingElement(), type);
}
}
}
typeMap = nextRound;
}
for (TypeElement imp: importMap.values()) {
if (imp.getModifiers().contains(Modifier.PUBLIC)
&& !JAVA_LANG.equals(imp.getEnclosingElement())) {
imports.add(imp.getQualifiedName().toString());
}
}
// Other imports that the generated class needs
imports.add("java.util.Map");
imports.add("java.util.HashMap");
imports.add("javax.annotation.Generated");
imports.add("org.robolectric.internal.ShadowExtractor");
imports.add("org.robolectric.internal.ShadowProvider");
}
public void addShadowType(TypeElement elem, TypeElement type) {
shadowTypes.put(elem, type);
}
public void addExtraShadow(String sdkClassName, String shadowClassName) {
extraShadowTypes.put(shadowClassName, sdkClassName);
}
public void addResetter(TypeElement parent, ExecutableElement elem) {
resetterMap.put(parent, elem);
}
public Map<TypeElement, ExecutableElement> getResetters() {
return resetterMap;
}
public Set<String> getImports() {
return imports;
}
public Map<TypeElement, TypeElement> getAllShadowTypes() {
return shadowTypes;
}
public Map<String, String> getExtraShadowTypes() {
return extraShadowTypes;
}
public Map<TypeElement, TypeElement> getResetterShadowTypes() {
return Maps.filterEntries(shadowTypes, new Predicate<Entry<TypeElement, TypeElement>>() {
@Override
public boolean apply(Entry<TypeElement, TypeElement> entry) {
return resetterMap.containsKey(entry.getKey());
}
});
}
public Map<TypeElement, TypeElement> getVisibleShadowTypes() {
return Maps.filterEntries(shadowTypes, new Predicate<Entry<TypeElement, TypeElement>>() {
@Override
public boolean apply(Entry<TypeElement, TypeElement> entry) {
return entry.getKey().getAnnotation(Implements.class).isInAndroidSdk();
}
});
}
public Map<TypeElement, TypeElement> getShadowOfMap() {
return Maps.filterEntries(getVisibleShadowTypes(), new Predicate<Entry<TypeElement,TypeElement>> () {
@Override
public boolean apply(Entry<TypeElement, TypeElement> entry) {
return !Objects.equal(ANYTHING, entry.getValue());
}
});
}
public Collection<String> getShadowedPackages() {
Set<String> packages = new TreeSet<>();
for (TypeElement element : shadowTypes.values()) {
String packageName = elements.getPackageOf(element).toString();
// org.robolectric.* should never be instrumented
if (packageName.matches("org.robolectric(\\..*)?")) {
continue;
}
packages.add("\"" + packageName + "\"");
}
return packages;
}
private Predicate<TypeMirror> notObject;
public List<TypeMirror> getExplicitBounds(TypeParameterElement typeParam) {
return newArrayList(Iterables.filter(typeParam.getBounds(), notObject));
}
/**
* Returns a plain string to be used in the generated source
* to identify the given type. The returned string will have
* sufficient level of qualification in order to make the referent
* unique for the source file.
* @param type
* @return
*/
public String getReferentFor(TypeElement type) {
return referentMap.get(type);
}
private TypeVisitor<String,Void> findReferent = new SimpleTypeVisitor6<String,Void>() {
@Override
public String visitDeclared(DeclaredType t, Void p) {
return referentMap.get(t.asElement());
}
};
public String getReferentFor(TypeMirror type) {
return findReferent.visit(type);
}
private Equivalence<TypeMirror> typeMirrorEq = new Equivalence<TypeMirror>() {
@Override
protected boolean doEquivalent(TypeMirror a, TypeMirror b) {
return types.isSameType(a, b);
}
@Override
protected int doHash(TypeMirror t) {
// We're not using the hash.
return 0;
}
};
private Equivalence<TypeParameterElement> typeEq = new Equivalence<TypeParameterElement>() {
@Override
@SuppressWarnings({"unchecked"})
protected boolean doEquivalent(TypeParameterElement arg0,
TypeParameterElement arg1) {
// Casts are necessary due to flaw in pairwise equivalence implementation.
return typeMirrorEq.pairwise().equivalent((List<TypeMirror>)arg0.getBounds(),
(List<TypeMirror>)arg1.getBounds());
}
@Override
protected int doHash(TypeParameterElement arg0) {
// We don't use the hash code.
return 0;
}
};
public void appendParameterList(StringBuilder message, List<? extends TypeParameterElement> tpeList) {
boolean first = true;
for (TypeParameterElement tpe : tpeList) {
if (first) {
first = false;
} else {
message.append(',');
}
message.append(tpe.toString());
boolean iFirst = true;
for (TypeMirror bound : getExplicitBounds(tpe)) {
if (iFirst) {
message.append(" extends ");
iFirst = false;
} else {
message.append(',');
}
message.append(bound);
}
}
}
@SuppressWarnings({"unchecked"})
public boolean isSameParameterList(List<? extends TypeParameterElement> l1, List<? extends TypeParameterElement> l2) {
// Cast is necessary because of a flaw in the API design of "PairwiseEquivalent",
// a flaw that is even acknowledged in the source.
// Our casts are safe because we're not trying to add elements to the list
// and therefore can't violate the constraint.
return typeEq.pairwise().equivalent((List<TypeParameterElement>)l1,
(List<TypeParameterElement>)l2);
}
}