package com.aitorvs.autoparcel.internal.codegen;
/*
* Copyright (C) 13/07/16 aitorvs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.aitorvs.autoparcel.AutoParcel;
import com.aitorvs.autoparcel.ParcelAdapter;
import com.aitorvs.autoparcel.ParcelVersion;
import com.aitorvs.autoparcel.internal.common.MoreElements;
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;
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 java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
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;
@SupportedAnnotationTypes("com.aitorvs.autoparcel.AutoParcel")
public final class AutoParcelProcessor extends AbstractProcessor {
private ErrorReporter mErrorReporter;
private Types mTypeUtils;
static final class Property {
final String fieldName;
final VariableElement element;
final TypeName typeName;
final ImmutableSet<String> annotations;
final int version;
TypeMirror typeAdapter;
Property(String fieldName, VariableElement element) {
this.fieldName = fieldName;
this.element = element;
this.typeName = TypeName.get(element.asType());
this.annotations = getAnnotations(element);
// get the parcel adapter if any
ParcelAdapter parcelAdapter = element.getAnnotation(ParcelAdapter.class);
if (parcelAdapter != null) {
try {
parcelAdapter.value();
} catch (MirroredTypeException e) {
this.typeAdapter = e.getTypeMirror();
}
}
// get the element version, default 0
ParcelVersion parcelVersion = element.getAnnotation(ParcelVersion.class);
this.version = parcelVersion == null ? 0 : parcelVersion.from();
}
public boolean isNullable() {
return this.annotations.contains("Nullable");
}
public int version() {
return this.version;
}
private ImmutableSet<String> getAnnotations(VariableElement element) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
builder.add(annotation.getAnnotationType().asElement().getSimpleName().toString());
}
return builder.build();
}
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mErrorReporter = new ErrorReporter(processingEnv);
mTypeUtils = processingEnv.getTypeUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Collection<? extends Element> annotatedElements =
env.getElementsAnnotatedWith(AutoParcel.class);
List<TypeElement> types = new ImmutableList.Builder<TypeElement>()
.addAll(ElementFilter.typesIn(annotatedElements))
.build();
for (TypeElement type : types) {
processType(type);
}
// We are the only ones handling AutoParcel annotations
return true;
}
private void processType(TypeElement type) {
AutoParcel autoParcel = type.getAnnotation(AutoParcel.class);
if (autoParcel == null) {
mErrorReporter.abortWithError("annotation processor for @AutoParcel was invoked with a" +
"type annotated differently; compiler bug? O_o", type);
}
if (type.getKind() != ElementKind.CLASS) {
mErrorReporter.abortWithError("@" + AutoParcel.class.getName() + " only applies to classes", type);
}
if (ancestorIsAutoParcel(type)) {
mErrorReporter.abortWithError("One @AutoParcel class shall not extend another", type);
}
checkModifiersIfNested(type);
// get the fully-qualified class name
String fqClassName = generatedSubclassName(type, 0);
// class name
String className = TypeUtil.simpleNameOf(fqClassName);
String source = generateClass(type, className, type.getSimpleName().toString(), false);
source = Reformatter.fixup(source);
writeSourceFile(fqClassName, source, type);
}
private void writeSourceFile(String className, String text, TypeElement originatingType) {
try {
JavaFileObject sourceFile =
processingEnv.getFiler().createSourceFile(className, originatingType);
Writer writer = sourceFile.openWriter();
try {
writer.write(text);
} finally {
writer.close();
}
} catch (IOException e) {
// This should really be an error, but we make it a warning in the hope of resisting Eclipse
// bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=367599. If that bug manifests, we may get
// invoked more than once for the same file, so ignoring the ability to overwrite it is the
// right thing to do. If we are unable to write for some other reason, we should get a compile
// error later because user code will have a reference to the code we were supposed to
// generate (new AutoValue_Foo() or whatever) and that reference will be undefined.
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"Could not write generated class " + className + ": " + e);
}
}
private String generateClass(TypeElement type, String className, String classToExtend, boolean isFinal) {
if (type == null) {
mErrorReporter.abortWithError("generateClass was invoked with null type", type);
}
if (className == null) {
mErrorReporter.abortWithError("generateClass was invoked with null class name", type);
}
if (classToExtend == null) {
mErrorReporter.abortWithError("generateClass was invoked with null parent class", type);
}
List<VariableElement> nonPrivateFields = getParcelableFieldsOrError(type);
if (nonPrivateFields.isEmpty()) {
mErrorReporter.abortWithError("generateClass error, all fields are declared PRIVATE", type);
}
// get the properties
ImmutableList<Property> properties = buildProperties(nonPrivateFields);
// get the type adapters
ImmutableMap<TypeMirror, FieldSpec> typeAdapters = getTypeAdapters(properties);
// get the parcel version
//noinspection ConstantConditions
int version = type.getAnnotation(AutoParcel.class).version();
// Generate the AutoParcel_??? class
String pkg = TypeUtil.packageNameOf(type);
TypeName classTypeName = ClassName.get(pkg, className);
TypeSpec.Builder subClass = TypeSpec.classBuilder(className)
// Add the version
.addField(TypeName.INT, "version", PRIVATE)
// Class must be always final
.addModifiers(FINAL)
// extends from original abstract class
.superclass(ClassName.get(pkg, classToExtend))
// Add the DEFAULT constructor
.addMethod(generateConstructor(properties))
// Add the private constructor
.addMethod(generateConstructorFromParcel(processingEnv, properties, typeAdapters))
// overrides describeContents()
.addMethod(generateDescribeContents())
// static final CREATOR
.addField(generateCreator(processingEnv, properties, classTypeName, typeAdapters))
// overrides writeToParcel()
.addMethod(generateWriteToParcel(version, processingEnv, properties, typeAdapters)); // generate writeToParcel()
if (!ancestoIsParcelable(processingEnv, type)) {
// Implement android.os.Parcelable if the ancestor does not do it.
subClass.addSuperinterface(ClassName.get("android.os", "Parcelable"));
}
if (!typeAdapters.isEmpty()) {
typeAdapters.values().forEach(subClass::addField);
}
JavaFile javaFile = JavaFile.builder(pkg, subClass.build()).build();
return javaFile.toString();
}
private ImmutableMap<TypeMirror, FieldSpec> getTypeAdapters(ImmutableList<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);
}
private ImmutableList<Property> buildProperties(List<VariableElement> elements) {
ImmutableList.Builder<Property> builder = ImmutableList.builder();
for (VariableElement element : elements) {
builder.add(new Property(element.getSimpleName().toString(), element));
}
return builder.build();
}
/**
* This method returns a list of all non private fields. If any <code>private</code> fields is
* found, the method errors out
*
* @param type element
* @return list of all non-<code>private</code> fields
*/
private List<VariableElement> getParcelableFieldsOrError(TypeElement type) {
List<VariableElement> allFields = ElementFilter.fieldsIn(type.getEnclosedElements());
List<VariableElement> nonPrivateFields = new ArrayList<>();
for (VariableElement field : allFields) {
if (!field.getModifiers().contains(PRIVATE)) {
nonPrivateFields.add(field);
} else {
// return error, PRIVATE fields are not allowed
mErrorReporter.abortWithError("getFieldsError error, PRIVATE fields not allowed", type);
}
}
return nonPrivateFields;
}
private MethodSpec generateConstructor(ImmutableList<Property> properties) {
List<ParameterSpec> params = Lists.newArrayListWithCapacity(properties.size());
for (Property property : properties) {
params.add(ParameterSpec.builder(property.typeName, property.fieldName).build());
}
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addParameters(params);
for (ParameterSpec param : params) {
builder.addStatement("this.$N = $N", param.name, param.name);
}
return builder.build();
}
private MethodSpec generateConstructorFromParcel(
ProcessingEnvironment env,
ImmutableList<Property> properties,
ImmutableMap<TypeMirror, FieldSpec> typeAdapters) {
// Create the PRIVATE constructor from Parcel
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(PRIVATE) // private
.addParameter(ClassName.bestGuess("android.os.Parcel"), "in"); // input param
// get a code block builder
CodeBlock.Builder block = CodeBlock.builder();
// First thing is reading the Parcelable object version
block.add("this.version = in.readInt();\n");
// FIXME: 31/07/16 remove if not used
boolean requiresSuppressWarnings = false;
// Now, iterate all properties, check the version initialize them
for (Property p : properties) {
// get the property version
int pVersion = p.version();
if (pVersion > 0) {
block.beginControlFlow("if (this.version >= $L)", pVersion);
}
block.add("this.$N = ", p.fieldName);
if (p.typeAdapter != null && typeAdapters.containsKey(p.typeAdapter)) {
Parcelables.readValueWithTypeAdapter(block, p, typeAdapters.get(p.typeAdapter));
} else {
requiresSuppressWarnings |= Parcelables.isTypeRequiresSuppressWarnings(p.typeName);
TypeName parcelableType = Parcelables.getTypeNameFromProperty(p, env.getTypeUtils());
Parcelables.readValue(block, p, parcelableType);
}
block.add(";\n");
if (pVersion > 0) {
block.endControlFlow();
}
}
builder.addCode(block.build());
return builder.build();
}
private String generatedSubclassName(TypeElement type, int depth) {
return generatedClassName(type, Strings.repeat("$", depth) + "AutoParcel_");
}
private String generatedClassName(TypeElement type, String prefix) {
String name = type.getSimpleName().toString();
while (type.getEnclosingElement() instanceof TypeElement) {
type = (TypeElement) type.getEnclosingElement();
name = type.getSimpleName() + "_" + name;
}
String pkg = TypeUtil.packageNameOf(type);
String dot = pkg.isEmpty() ? "" : ".";
return pkg + dot + prefix + name;
}
private MethodSpec generateWriteToParcel(
int version,
ProcessingEnvironment env,
ImmutableList<Property> properties,
ImmutableMap<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(PUBLIC)
.addParameter(dest)
.addParameter(flags);
// write first the parcelable object version...
builder.addCode(Parcelables.writeVersion(version, dest));
// ...then write all the properties
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(p, dest, flags, env.getTypeUtils()));
}
}
return builder.build();
}
private MethodSpec generateDescribeContents() {
return MethodSpec.methodBuilder("describeContents")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(int.class)
.addStatement("return 0")
.build();
}
private FieldSpec generateCreator(
ProcessingEnvironment env,
ImmutableList<Property> properties,
TypeName type,
ImmutableMap<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();
boolean requiresSuppressWarnings = false;
ctorCall.add("return new $T(in);\n", type);
// Method createFromParcel()
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();
}
private void checkModifiersIfNested(TypeElement type) {
ElementKind enclosingKind = type.getEnclosingElement().getKind();
if (enclosingKind.isClass() || enclosingKind.isInterface()) {
if (type.getModifiers().contains(PRIVATE)) {
mErrorReporter.abortWithError("@AutoParcel class must not be private", type);
}
if (!type.getModifiers().contains(STATIC)) {
mErrorReporter.abortWithError("Nested @AutoParcel class must be static", type);
}
}
// In principle type.getEnclosingElement() could be an ExecutableElement (for a class
// declared inside a method), but since RoundEnvironment.getElementsAnnotatedWith doesn't
// return such classes we won't see them here.
}
private boolean ancestorIsAutoParcel(TypeElement type) {
while (true) {
TypeMirror parentMirror = type.getSuperclass();
if (parentMirror.getKind() == TypeKind.NONE) {
return false;
}
TypeElement parentElement = (TypeElement) mTypeUtils.asElement(parentMirror);
if (MoreElements.isAnnotationPresent(parentElement, AutoParcel.class)) {
return true;
}
type = parentElement;
}
}
private boolean ancestoIsParcelable(ProcessingEnvironment env, TypeElement type) {
// TODO: 15/07/16 check recursively
TypeMirror classType = type.asType();
TypeMirror parcelable = env.getElementUtils().getTypeElement("android.os.Parcelable").asType();
return TypeUtil.isClassOfType(env.getTypeUtils(), parcelable, classType);
}
private static AnnotationSpec createSuppressUncheckedWarningAnnotation() {
return AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "\"unchecked\"")
.build();
}
}