package com.ryanharter.auto.value.parcel;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
@AutoService(AutoValueExtension.class)
public final class AutoValueParcelExtension extends AutoValueExtension {
static final class Property {
final String methodName;
final String humanName;
final ExecutableElement element;
final TypeName type;
final ImmutableSet<String> annotations;
TypeMirror typeAdapter;
public Property(String humanName, ExecutableElement element) {
this.methodName = element.getSimpleName().toString();
this.humanName = humanName;
this.element = element;
type = TypeName.get(element.getReturnType());
annotations = buildAnnotations(element);
ParcelAdapter parcelAdapter = element.getAnnotation(ParcelAdapter.class);
if (parcelAdapter != null) {
try {
parcelAdapter.value();
} catch (MirroredTypeException e) {
typeAdapter = e.getTypeMirror();
}
}
}
public boolean nullable() {
return annotations.contains("Nullable");
}
private ImmutableSet<String> buildAnnotations(ExecutableElement element) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
builder.add(annotation.getAnnotationType().asElement().getSimpleName().toString());
}
return builder.build();
}
}
@Override
public boolean applicable(Context context) {
TypeMirror autoValueClass = context.autoValueClass().asType();
// Disallow manual implementation of the CREATOR instance
VariableElement creator = findCreator(context);
if (creator != null) {
context.processingEnvironment().getMessager().printMessage(Diagnostic.Kind.ERROR,
"Manual implementation of a static Parcelable.Creator<T> CREATOR field found when processing "
+ autoValueClass.toString() + ". Remove this so auto-value-parcel can automatically generate the "
+ "implementation for you.", creator);
}
// Disallow manual implementation of writeToParcel
ExecutableElement writeToParcel = findWriteToParcel(context);
if (writeToParcel != null) {
context.processingEnvironment().getMessager().printMessage(Diagnostic.Kind.ERROR,
"Manual implementation of Parcelable#writeToParcel(Parcel,int) found when processing "
+ autoValueClass.toString() + ". Remove this so auto-value-parcel can automatically generate the "
+ "implementation for you.", writeToParcel);
}
TypeMirror parcelable = context.processingEnvironment().getElementUtils()
.getTypeElement("android.os.Parcelable").asType();
return TypeSimplifier.isClassOfType(context.processingEnvironment().getTypeUtils(), parcelable,
autoValueClass);
}
@Override
public boolean mustBeFinal(Context context) {
return true;
}
@Override
public Set<String> consumeProperties(Context context) {
ImmutableSet.Builder<String> properties = new ImmutableSet.Builder<>();
for (String property : context.properties().keySet()) {
switch (property) {
case "describeContents":
properties.add(property);
break;
}
}
return properties.build();
}
@Override public Set<ExecutableElement> consumeMethods(Context context) {
ImmutableSet.Builder<ExecutableElement> methods = new ImmutableSet.Builder<>();
for (ExecutableElement element : context.abstractMethods()) {
switch (element.getSimpleName().toString()) {
case "writeToParcel":
methods.add(element);
break;
}
}
return methods.build();
}
@Override
public String generateClass(Context context, String className, String classToExtend,
boolean isFinal) {
ProcessingEnvironment env = context.processingEnvironment();
ImmutableList<Property> properties = readProperties(context.properties());
validateProperties(env, properties);
ImmutableMap<TypeMirror, FieldSpec> typeAdapters = getTypeAdapters(properties);
TypeName type = ClassName.get(context.packageName(), className);
TypeSpec.Builder subclass = TypeSpec.classBuilder(className)
.addModifiers(FINAL)
.addMethod(generateConstructor(properties))
.addMethod(generateWriteToParcel(env, properties, typeAdapters));
if (!typeAdapters.isEmpty()) {
for (FieldSpec field : typeAdapters.values()) {
subclass.addField(field);
}
}
subclass.addField(generateCreator(env, properties, type, typeAdapters));
ClassName superClass = ClassName.get(context.packageName(), classToExtend);
List<? extends TypeParameterElement> tpes = context.autoValueClass().getTypeParameters();
if (tpes.isEmpty()) {
subclass.superclass(superClass);
} else {
TypeName[] superTypeVariables = new TypeName[tpes.size()];
for (int i = 0, tpesSize = tpes.size(); i < tpesSize; i++) {
TypeParameterElement tpe = tpes.get(i);
subclass.addTypeVariable(TypeVariableName.get(tpe));
superTypeVariables[i] = TypeVariableName.get(tpe.getSimpleName().toString());
}
subclass.superclass(ParameterizedTypeName.get(superClass, superTypeVariables));
}
if (needsContentDescriptor(context)) {
subclass.addMethod(generateDescribeContents());
}
JavaFile javaFile = JavaFile.builder(context.packageName(), subclass.build()).build();
return javaFile.toString();
}
private static boolean needsContentDescriptor(Context context) {
ProcessingEnvironment env = context.processingEnvironment();
for (ExecutableElement element : MoreElements.getLocalAndInheritedMethods(
context.autoValueClass(), env.getElementUtils())) {
if (element.getSimpleName().contentEquals("describeContents")
&& MoreTypes.isTypeOf(int.class, element.getReturnType())
&& element.getParameters().isEmpty()
&& !element.getModifiers().contains(ABSTRACT)) {
return false;
}
}
return true;
}
private static ExecutableElement findWriteToParcel(Context context) {
ProcessingEnvironment env = context.processingEnvironment();
TypeMirror parcel = env.getElementUtils().getTypeElement("android.os.Parcel").asType();
for (ExecutableElement element : MoreElements.getLocalAndInheritedMethods(
context.autoValueClass(), env.getElementUtils())) {
if (element.getSimpleName().contentEquals("writeToParcel")
&& MoreTypes.isTypeOf(void.class, element.getReturnType())
&& !element.getModifiers().contains(ABSTRACT)) {
List<? extends VariableElement> parameters = element.getParameters();
if (parameters.size() == 2
&& env.getTypeUtils().isSameType(parcel, parameters.get(0).asType())
&& MoreTypes.isTypeOf(int.class, parameters.get(1).asType())) {
return element;
}
}
}
return null;
}
private static VariableElement findCreator(Context context) {
ProcessingEnvironment env = context.processingEnvironment();
Types typeUtils = env.getTypeUtils();
Elements elementUtils = env.getElementUtils();
TypeMirror creatorType = typeUtils.erasure(elementUtils.getTypeElement("android.os.Parcelable.Creator").asType());
List<? extends Element> members = env.getElementUtils().getAllMembers(context.autoValueClass());
for (VariableElement field : ElementFilter.fieldsIn(members)) {
if (field.getSimpleName().contentEquals("CREATOR")
&& typeUtils.isSameType(creatorType, typeUtils.erasure(field.asType()))
&& field.getModifiers().contains(STATIC)) {
return field;
}
}
return null;
}
private ImmutableList<Property> readProperties(Map<String, ExecutableElement> properties) {
ImmutableList.Builder<Property> values = ImmutableList.builder();
for (Map.Entry<String, ExecutableElement> entry : properties.entrySet()) {
values.add(new Property(entry.getKey(), entry.getValue()));
}
return values.build();
}
private void validateProperties(ProcessingEnvironment env, List<Property> properties) {
Types typeUtils = env.getTypeUtils();
for (Property property : properties) {
if (property.typeAdapter != null) {
continue;
}
TypeMirror type = property.element.getReturnType();
if (type.getKind() == TypeKind.ARRAY) {
ArrayType aType = (ArrayType) type;
type = aType.getComponentType();
}
if (type.getKind() == TypeKind.TYPEVAR) {
TypeVariable vType = (TypeVariable) type;
type = vType.getUpperBound();
}
TypeElement element = (TypeElement) typeUtils.asElement(type);
if ((element == null || !Parcelables.isValidType(typeUtils, type))
&& !Parcelables.isValidType(TypeName.get(type))) {
if (element != null && Parcelables.MAP.equals(Parcelables.getParcelableType(typeUtils, element))) {
env.getMessager().printMessage(Diagnostic.Kind.ERROR, "Maps can only have String objects "
+ "for keys and valid Parcelable types for values.", property.element);
} else {
env.getMessager().printMessage(Diagnostic.Kind.ERROR, "AutoValue property " +
property.methodName + " is not a supported Parcelable type.", property.element);
}
throw new AutoValueParcelException();
}
}
}
MethodSpec generateConstructor(List<Property> properties) {
List<ParameterSpec> params = Lists.newArrayListWithCapacity(properties.size());
for (Property property : properties) {
params.add(ParameterSpec.builder(property.type, property.humanName).build());
}
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addParameters(params);
StringBuilder superFormat = new StringBuilder("super(");
List<ParameterSpec> args = new ArrayList<>();
for (int i = 0, n = params.size(); i < n; i++) {
args.add(params.get(i));
superFormat.append("$N");
if (i < n - 1) superFormat.append(", ");
}
superFormat.append(")");
builder.addStatement(superFormat.toString(), args.toArray());
return builder.build();
}
FieldSpec generateCreator(ProcessingEnvironment env, List<Property> properties, TypeName type,
Map<TypeMirror, FieldSpec> typeAdapters) {
ClassName creator = ClassName.bestGuess("android.os.Parcelable.Creator");
TypeName creatorOfClass = ParameterizedTypeName.get(creator, type);
Types typeUtils = env.getTypeUtils();
CodeBlock.Builder ctorCall = CodeBlock.builder();
ctorCall.add("return new $T(\n", type);
ctorCall.indent().indent();
boolean requiresSuppressWarnings = false;
for (int i = 0, n = properties.size(); i < n; i++) {
Property property = properties.get(i);
if (property.typeAdapter != null && typeAdapters.containsKey(property.typeAdapter)) {
Parcelables.readValueWithTypeAdapter(ctorCall, property,
typeAdapters.get(property.typeAdapter));
} else {
final TypeName typeName = Parcelables.getTypeNameFromProperty(property, typeUtils);
requiresSuppressWarnings |= Parcelables.isTypeRequiresSuppressWarnings(typeName);
Parcelables.readValue(ctorCall, property, typeName, env.getTypeUtils());
}
if (i < n - 1) ctorCall.add(",");
ctorCall.add("\n");
}
ctorCall.unindent().unindent();
ctorCall.add(");\n");
MethodSpec.Builder createFromParcel = MethodSpec.methodBuilder("createFromParcel")
.addAnnotation(Override.class);
if (requiresSuppressWarnings) {
createFromParcel.addAnnotation(createSuppressUncheckedWarningAnnotation());
}
createFromParcel
.addModifiers(PUBLIC)
.returns(type)
.addParameter(ClassName.bestGuess("android.os.Parcel"), "in");
createFromParcel.addCode(ctorCall.build());
TypeSpec creatorImpl = TypeSpec.anonymousClassBuilder("")
.superclass(creatorOfClass)
.addMethod(createFromParcel
.build())
.addMethod(MethodSpec.methodBuilder("newArray")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(ArrayTypeName.of(type))
.addParameter(int.class, "size")
.addStatement("return new $T[size]", type)
.build())
.build();
return FieldSpec
.builder(creatorOfClass, "CREATOR", PUBLIC, FINAL, STATIC)
.initializer("$L", creatorImpl)
.build();
}
MethodSpec generateWriteToParcel(ProcessingEnvironment env, List<Property> properties,
Map<TypeMirror, FieldSpec> typeAdapters) {
ParameterSpec dest = ParameterSpec
.builder(ClassName.get("android.os", "Parcel"), "dest")
.build();
ParameterSpec flags = ParameterSpec.builder(int.class, "flags").build();
MethodSpec.Builder builder = MethodSpec.methodBuilder("writeToParcel")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(dest)
.addParameter(flags);
Types typeUtils = env.getTypeUtils();
for (Property p : properties) {
if (p.typeAdapter != null && typeAdapters.containsKey(p.typeAdapter)) {
FieldSpec typeAdapter = typeAdapters.get(p.typeAdapter);
builder.addCode(Parcelables.writeValueWithTypeAdapter(typeAdapter, p, dest));
} else {
builder.addCode(Parcelables.writeValue(typeUtils, p, dest, flags));
}
}
return builder.build();
}
private static AnnotationSpec createSuppressUncheckedWarningAnnotation() {
return AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "\"unchecked\"")
.build();
}
private ImmutableMap<TypeMirror, FieldSpec> getTypeAdapters(List<Property> properties) {
Map<TypeMirror, FieldSpec> typeAdapters = new LinkedHashMap<>();
NameAllocator nameAllocator = new NameAllocator();
nameAllocator.newName("CREATOR");
for (Property property : properties) {
if (property.typeAdapter != null && !typeAdapters.containsKey(property.typeAdapter)) {
ClassName typeName = (ClassName) TypeName.get(property.typeAdapter);
String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, typeName.simpleName());
name = nameAllocator.newName(name, typeName);
typeAdapters.put(property.typeAdapter, FieldSpec.builder(
typeName, NameAllocator.toJavaIdentifier(name), PRIVATE, STATIC, FINAL)
.initializer("new $T()", typeName).build());
}
}
return ImmutableMap.copyOf(typeAdapters);
}
MethodSpec generateDescribeContents() {
return MethodSpec.methodBuilder("describeContents")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(int.class)
.addStatement("return 0")
.build();
}
}