1 package org.robolectric.annotation.processing; 2 3 import static com.google.common.collect.Maps.newHashMap; 4 import static com.google.common.collect.Maps.newTreeMap; 5 import static com.google.common.collect.Sets.newTreeSet; 6 7 import com.google.common.collect.HashMultimap; 8 import com.google.common.collect.Iterables; 9 import com.google.common.collect.Multimaps; 10 import java.util.Collection; 11 import java.util.List; 12 import java.util.Map; 13 import java.util.Set; 14 import java.util.TreeMap; 15 import java.util.TreeSet; 16 import javax.annotation.processing.ProcessingEnvironment; 17 import javax.lang.model.element.Element; 18 import javax.lang.model.element.ElementVisitor; 19 import javax.lang.model.element.ExecutableElement; 20 import javax.lang.model.element.Modifier; 21 import javax.lang.model.element.PackageElement; 22 import javax.lang.model.element.TypeElement; 23 import javax.lang.model.element.TypeParameterElement; 24 import javax.lang.model.type.DeclaredType; 25 import javax.lang.model.type.TypeMirror; 26 import javax.lang.model.type.TypeVisitor; 27 import javax.lang.model.util.SimpleElementVisitor6; 28 import javax.lang.model.util.SimpleTypeVisitor6; 29 import org.robolectric.annotation.Implements; 30 import org.robolectric.shadow.api.ShadowPicker; 31 32 /** 33 * Model describing the Robolectric source file. 34 */ 35 public class RobolectricModel { 36 37 private final TreeSet<String> imports; 38 /** 39 * Key: name of shadow class 40 */ 41 private final TreeMap<String, ShadowInfo> shadowTypes; 42 private final TreeMap<String, String> extraShadowTypes; 43 /** 44 * Key: name of shadow class 45 */ 46 private final TreeMap<String, ResetterInfo> resetterMap; 47 48 private final TreeMap<String, DocumentedPackage> documentedPackages; 49 getDocumentedPackages()50 public Collection<DocumentedPackage> getDocumentedPackages() { 51 return documentedPackages.values(); 52 } 53 RobolectricModel(TreeSet<String> imports, TreeMap<String, ShadowInfo> shadowTypes, TreeMap<String, String> extraShadowTypes, TreeMap<String, ResetterInfo> resetterMap, Map<String, DocumentedPackage> documentedPackages)54 public RobolectricModel(TreeSet<String> imports, 55 TreeMap<String, ShadowInfo> shadowTypes, 56 TreeMap<String, String> extraShadowTypes, 57 TreeMap<String, ResetterInfo> resetterMap, 58 Map<String, DocumentedPackage> documentedPackages) { 59 this.imports = new TreeSet<>(imports); 60 this.shadowTypes = new TreeMap<>(shadowTypes); 61 this.extraShadowTypes = new TreeMap<>(extraShadowTypes); 62 this.resetterMap = new TreeMap<>(resetterMap); 63 this.documentedPackages = new TreeMap<>(documentedPackages); 64 } 65 66 private final static ElementVisitor<TypeElement, Void> TYPE_ELEMENT_VISITOR = 67 new SimpleElementVisitor6<TypeElement, Void>() { 68 @Override 69 public TypeElement visitType(TypeElement e, Void p) { 70 return e; 71 } 72 }; 73 74 public static class Builder { 75 76 private final Helpers helpers; 77 78 private final TreeSet<String> imports = newTreeSet(); 79 private final TreeMap<String, ShadowInfo> shadowTypes = newTreeMap(); 80 private final TreeMap<String, String> extraShadowTypes = newTreeMap(); 81 private final TreeMap<String, ResetterInfo> resetterMap = newTreeMap(); 82 private final Map<String, DocumentedPackage> documentedPackages = new TreeMap<>(); 83 84 private final Map<TypeElement, TypeElement> importMap = newHashMap(); 85 private final Map<TypeElement, String> referentMap = newHashMap(); 86 private HashMultimap<String, TypeElement> typeMap = HashMultimap.create(); 87 Builder(ProcessingEnvironment environment)88 Builder(ProcessingEnvironment environment) { 89 this.helpers = new Helpers(environment); 90 } 91 addShadowType(TypeElement shadowType, TypeElement actualType, TypeElement shadowPickerType)92 public void addShadowType(TypeElement shadowType, TypeElement actualType, 93 TypeElement shadowPickerType) { 94 TypeElement shadowBaseType = null; 95 if (shadowPickerType != null) { 96 TypeMirror iface = helpers.findInterface(shadowPickerType, ShadowPicker.class); 97 if (iface != null) { 98 com.sun.tools.javac.code.Type type = ((com.sun.tools.javac.code.Type.ClassType) iface) 99 .allparams().get(0); 100 String baseClassName = type.asElement().getQualifiedName().toString(); 101 shadowBaseType = helpers.getTypeElement(baseClassName); 102 } 103 } 104 ShadowInfo shadowInfo = 105 new ShadowInfo(shadowType, actualType, shadowPickerType, shadowBaseType); 106 107 if (shadowInfo.isInAndroidSdk()) { 108 registerType(shadowInfo.shadowType); 109 registerType(shadowInfo.actualType); 110 registerType(shadowInfo.shadowBaseClass); 111 } 112 113 shadowTypes.put(shadowType.getQualifiedName().toString(), shadowInfo); 114 } 115 addExtraShadow(String sdkClassName, String shadowClassName)116 public void addExtraShadow(String sdkClassName, String shadowClassName) { 117 extraShadowTypes.put(shadowClassName, sdkClassName); 118 } 119 addResetter(TypeElement shadowTypeElement, ExecutableElement elem)120 public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) { 121 registerType(shadowTypeElement); 122 123 resetterMap.put(shadowTypeElement.getQualifiedName().toString(), 124 new ResetterInfo(shadowTypeElement, elem)); 125 } 126 documentPackage(String name, String documentation)127 public void documentPackage(String name, String documentation) { 128 getDocumentedPackage(name).setDocumentation(documentation); 129 } 130 documentType(TypeElement type, String documentation, List<String> imports)131 public void documentType(TypeElement type, String documentation, List<String> imports) { 132 DocumentedType documentedType = getDocumentedType(type); 133 documentedType.setDocumentation(documentation); 134 documentedType.imports = imports; 135 } 136 documentMethod(TypeElement shadowClass, DocumentedMethod documentedMethod)137 public void documentMethod(TypeElement shadowClass, DocumentedMethod documentedMethod) { 138 DocumentedType documentedType = getDocumentedType(shadowClass); 139 documentedType.methods.put(documentedMethod.getName(), documentedMethod); 140 } 141 getDocumentedPackage(String name)142 private DocumentedPackage getDocumentedPackage(String name) { 143 DocumentedPackage documentedPackage = documentedPackages.get(name); 144 if (documentedPackage == null) { 145 documentedPackage = new DocumentedPackage(name); 146 documentedPackages.put(name, documentedPackage); 147 } 148 return documentedPackage; 149 } 150 getDocumentedPackage(TypeElement type)151 private DocumentedPackage getDocumentedPackage(TypeElement type) { 152 Element pkgElement = type.getEnclosingElement(); 153 return getDocumentedPackage(pkgElement.toString()); 154 } 155 getDocumentedType(TypeElement type)156 private DocumentedType getDocumentedType(TypeElement type) { 157 DocumentedPackage documentedPackage = getDocumentedPackage(type); 158 return documentedPackage.getDocumentedType(type.getQualifiedName().toString()); 159 } 160 build()161 RobolectricModel build() { 162 prepare(); 163 164 return new RobolectricModel(imports, shadowTypes, extraShadowTypes, resetterMap, 165 documentedPackages); 166 } 167 168 /** 169 * Prepares the various derived parts of the model based on the class mappings that have been 170 * registered to date. 171 */ prepare()172 void prepare() { 173 while (!typeMap.isEmpty()) { 174 final HashMultimap<String, TypeElement> nextRound = HashMultimap.create(); 175 for (Map.Entry<String, Set<TypeElement>> referents : Multimaps.asMap(typeMap).entrySet()) { 176 final Set<TypeElement> c = referents.getValue(); 177 // If there is only one type left with the given simple 178 // name, then 179 if (c.size() == 1) { 180 final TypeElement type = c.iterator().next(); 181 referentMap.put(type, referents.getKey()); 182 } else { 183 for (TypeElement type : c) { 184 SimpleElementVisitor6<Void, TypeElement> visitor = new SimpleElementVisitor6<Void, TypeElement>() { 185 @Override 186 public Void visitType(TypeElement parent, TypeElement type) { 187 nextRound.put(parent.getSimpleName() + "." + type.getSimpleName(), type); 188 importMap.put(type, parent); 189 return null; 190 } 191 192 @Override 193 public Void visitPackage(PackageElement parent, TypeElement type) { 194 referentMap.put(type, type.getQualifiedName().toString()); 195 importMap.remove(type); 196 return null; 197 } 198 }; 199 visitor.visit(importMap.get(type).getEnclosingElement(), type); 200 } 201 } 202 } 203 typeMap = nextRound; 204 } 205 206 // FIXME: check this type lookup for NPEs (and also the ones in the validators) 207 Element javaLang = helpers.getPackageElement("java.lang"); 208 209 for (TypeElement imp : importMap.values()) { 210 if (imp.getModifiers().contains(Modifier.PUBLIC) 211 && !javaLang.equals(imp.getEnclosingElement())) { 212 imports.add(imp.getQualifiedName().toString()); 213 } 214 } 215 216 // Other imports that the generated class needs 217 imports.add("java.util.Map"); 218 imports.add("java.util.HashMap"); 219 imports.add("javax.annotation.Generated"); 220 imports.add("org.robolectric.internal.ShadowProvider"); 221 imports.add("org.robolectric.shadow.api.Shadow"); 222 223 ReferentResolver referentResolver = new ReferentResolver() { 224 @Override 225 public String getReferentFor(TypeMirror typeMirror) { 226 return findReferent.visit(typeMirror); 227 } 228 229 @Override 230 public String getReferentFor(TypeElement type) { 231 return referentMap.get(type); 232 } 233 }; 234 shadowTypes.values().forEach(shadowInfo -> shadowInfo.prepare(referentResolver, helpers)); 235 resetterMap.values().forEach(resetterInfo -> resetterInfo.prepare(referentResolver)); 236 } 237 registerType(TypeElement type)238 private void registerType(TypeElement type) { 239 if (type != null && !importMap.containsKey(type)) { 240 typeMap.put(type.getSimpleName().toString(), type); 241 importMap.put(type, type); 242 for (TypeParameterElement typeParam : type.getTypeParameters()) { 243 for (TypeMirror bound : typeParam.getBounds()) { 244 // FIXME: get rid of cast using a visitor 245 TypeElement boundElement = TYPE_ELEMENT_VISITOR.visit(helpers.asElement(bound)); 246 registerType(boundElement); 247 } 248 } 249 } 250 } 251 252 private final TypeVisitor<String, Void> findReferent = new SimpleTypeVisitor6<String, Void>() { 253 @Override 254 public String visitDeclared(DeclaredType t, Void p) { 255 return referentMap.get(t.asElement()); 256 } 257 }; 258 } 259 getResetters()260 public Collection<ResetterInfo> getResetters() { 261 return resetterMap.values(); 262 } 263 getImports()264 public Set<String> getImports() { 265 return imports; 266 } 267 getAllShadowTypes()268 public Collection<ShadowInfo> getAllShadowTypes() { 269 return shadowTypes.values(); 270 } 271 getExtraShadowTypes()272 public Map<String, String> getExtraShadowTypes() { 273 return extraShadowTypes; 274 } 275 getVisibleShadowTypes()276 public Iterable<ShadowInfo> getVisibleShadowTypes() { 277 return Iterables.filter(shadowTypes.values(), 278 ShadowInfo::isInAndroidSdk); 279 } 280 getShadowPickers()281 public TreeMap<String, ShadowInfo> getShadowPickers() { 282 TreeMap<String, ShadowInfo> map = new TreeMap<>(); 283 Iterables.filter(shadowTypes.values(), ShadowInfo::hasShadowPicker) 284 .forEach(shadowInfo -> { 285 String actualName = shadowInfo.getActualName(); 286 String shadowPickerClassName = shadowInfo.getShadowPickerBinaryName(); 287 ShadowInfo otherShadowInfo = map.get(actualName); 288 String otherPicker = 289 otherShadowInfo == null ? null : otherShadowInfo.getShadowPickerBinaryName(); 290 if (otherPicker != null && !otherPicker.equals(shadowPickerClassName)) { 291 throw new IllegalArgumentException( 292 actualName + " has conflicting pickers: " + shadowPickerClassName + " != " 293 + otherPicker); 294 } else { 295 map.put(actualName, shadowInfo); 296 } 297 }); 298 return map; 299 } 300 getShadowedPackages()301 public Collection<String> getShadowedPackages() { 302 Set<String> packages = new TreeSet<>(); 303 for (ShadowInfo shadowInfo : shadowTypes.values()) { 304 String packageName = shadowInfo.getActualPackage(); 305 306 // org.robolectric.* should never be instrumented 307 if (packageName.matches("org.robolectric(\\..*)?")) { 308 continue; 309 } 310 311 packages.add("\"" + packageName + "\""); 312 } 313 return packages; 314 } 315 316 interface ReferentResolver { 317 getReferentFor(TypeMirror typeMirror)318 String getReferentFor(TypeMirror typeMirror); 319 320 /** 321 * Returns a plain string to be used in the generated source to identify the given type. The 322 * returned string will have sufficient level of qualification in order to make the referent 323 * unique for the source file. 324 */ getReferentFor(TypeElement type)325 String getReferentFor(TypeElement type); 326 } 327 328 public static class ShadowInfo { 329 330 private final TypeElement shadowType; 331 private final TypeElement actualType; 332 private final TypeElement shadowPickerType; 333 private final TypeElement shadowBaseClass; 334 335 private String paramDefStr; 336 private String paramUseStr; 337 private String actualBinaryName; 338 private String actualTypeReferent; 339 private String shadowTypeReferent; 340 private String actualTypePackage; 341 private String shadowBinaryName; 342 private String shadowPickerBinaryName; 343 private String shadowBaseName; 344 ShadowInfo(TypeElement shadowType, TypeElement actualType, TypeElement shadowPickerType, TypeElement shadowBaseClass)345 ShadowInfo(TypeElement shadowType, TypeElement actualType, TypeElement shadowPickerType, 346 TypeElement shadowBaseClass) { 347 this.shadowType = shadowType; 348 this.actualType = actualType; 349 this.shadowPickerType = shadowPickerType; 350 this.shadowBaseClass = shadowBaseClass; 351 } 352 prepare(ReferentResolver referentResolver, Helpers helpers)353 void prepare(ReferentResolver referentResolver, Helpers helpers) { 354 int paramCount = 0; 355 StringBuilder paramDef = new StringBuilder("<"); 356 StringBuilder paramUse = new StringBuilder("<"); 357 for (TypeParameterElement typeParam : actualType.getTypeParameters()) { 358 if (paramCount > 0) { 359 paramDef.append(','); 360 paramUse.append(','); 361 } 362 boolean first = true; 363 paramDef.append(typeParam); 364 paramUse.append(typeParam); 365 for (TypeMirror bound : helpers.getExplicitBounds(typeParam)) { 366 if (first) { 367 paramDef.append(" extends "); 368 first = false; 369 } else { 370 paramDef.append(" & "); 371 } 372 paramDef.append(referentResolver.getReferentFor(bound)); 373 } 374 paramCount++; 375 } 376 377 this.paramDefStr = ""; 378 this.paramUseStr = ""; 379 if (paramCount > 0) { 380 paramDefStr = paramDef.append('>').toString(); 381 paramUseStr = paramUse.append('>').toString(); 382 } 383 384 actualTypeReferent = referentResolver.getReferentFor(actualType); 385 actualTypePackage = helpers.getPackageOf(actualType); 386 actualBinaryName = helpers.getBinaryName(actualType); 387 shadowTypeReferent = referentResolver.getReferentFor(shadowType); 388 shadowBinaryName = helpers.getBinaryName(shadowType); 389 shadowPickerBinaryName = helpers.getBinaryName(shadowPickerType); 390 shadowBaseName = referentResolver.getReferentFor(shadowBaseClass); 391 } 392 getActualBinaryName()393 public String getActualBinaryName() { 394 return actualBinaryName; 395 } 396 getActualName()397 public String getActualName() { 398 return actualType.getQualifiedName().toString(); 399 } 400 isInAndroidSdk()401 public boolean isInAndroidSdk() { 402 return shadowType.getAnnotation(Implements.class).isInAndroidSdk(); 403 } 404 getParamDefStr()405 public String getParamDefStr() { 406 return paramDefStr; 407 } 408 shadowIsDeprecated()409 public boolean shadowIsDeprecated() { 410 return shadowType.getAnnotation(Deprecated.class) != null; 411 } 412 actualIsPublic()413 public boolean actualIsPublic() { 414 return actualType.getModifiers().contains(Modifier.PUBLIC); 415 } 416 getActualTypeWithParams()417 public String getActualTypeWithParams() { 418 return actualTypeReferent + paramUseStr; 419 } 420 getShadowName()421 public String getShadowName() { 422 return shadowType.getQualifiedName().toString(); 423 } 424 getShadowBinaryName()425 public String getShadowBinaryName() { 426 return shadowBinaryName; 427 } 428 getShadowTypeWithParams()429 public String getShadowTypeWithParams() { 430 return shadowTypeReferent + paramUseStr; 431 } 432 getActualPackage()433 String getActualPackage() { 434 return actualTypePackage; 435 } 436 hasShadowPicker()437 boolean hasShadowPicker() { 438 return shadowPickerType != null; 439 } 440 getShadowPickerBinaryName()441 public String getShadowPickerBinaryName() { 442 return shadowPickerBinaryName; 443 } 444 getShadowBaseName()445 public String getShadowBaseName() { 446 return shadowBaseName; 447 } 448 } 449 450 public static class ResetterInfo { 451 452 private final TypeElement shadowType; 453 private final ExecutableElement executableElement; 454 private String shadowTypeReferent; 455 ResetterInfo(TypeElement shadowType, ExecutableElement executableElement)456 ResetterInfo(TypeElement shadowType, ExecutableElement executableElement) { 457 this.shadowType = shadowType; 458 this.executableElement = executableElement; 459 } 460 prepare(ReferentResolver referentResolver)461 void prepare(ReferentResolver referentResolver) { 462 shadowTypeReferent = referentResolver.getReferentFor(shadowType); 463 } 464 getImplementsAnnotation()465 private Implements getImplementsAnnotation() { 466 return shadowType.getAnnotation(Implements.class); 467 } 468 getMethodCall()469 public String getMethodCall() { 470 return shadowTypeReferent + "." + executableElement.getSimpleName() + "();"; 471 } 472 getMinSdk()473 public int getMinSdk() { 474 return getImplementsAnnotation().minSdk(); 475 } 476 getMaxSdk()477 public int getMaxSdk() { 478 return getImplementsAnnotation().maxSdk(); 479 } 480 } 481 482 } 483