package com.gabrielittner.auto.value.cursor; import com.gabrielittner.auto.value.ColumnProperty; import com.gabrielittner.auto.value.util.ElementUtil; import com.google.auto.service.AutoService; import com.google.auto.value.extension.AutoValueExtension; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; 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.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.TypeElement; import static com.gabrielittner.auto.value.util.AutoValueUtil.error; import static com.gabrielittner.auto.value.util.AutoValueUtil.getAutoValueClassTypeName; import static com.gabrielittner.auto.value.util.AutoValueUtil.getFinalClassClassName; import static com.gabrielittner.auto.value.util.AutoValueUtil.newFinalClassConstructorCall; import static com.gabrielittner.auto.value.util.AutoValueUtil.newTypeSpecBuilder; import static com.gabrielittner.auto.value.util.ElementUtil.getMatchingStaticField; import static com.gabrielittner.auto.value.util.ElementUtil.getMatchingStaticMethod; import static com.google.common.base.Preconditions.checkNotNull; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; @AutoService(AutoValueExtension.class) public class AutoValueCursorExtension extends AutoValueExtension { private static final ClassName CURSOR = ClassName.get("android.database", "Cursor"); private static final ClassName FUNC1 = ClassName.get("rx.functions", "Func1"); private static final ClassName FUNCTION = ClassName.get("io.reactivex.functions", "Function"); private static final String METHOD_NAME = "createFromCursor"; private static final String FUNC1_FIELD_NAME = "MAPPER"; private static final String FUNC1_METHOD_NAME = "call"; private static final String FUNCTION_FIELD_NAME = "MAPPER_FUNCTION"; private static final String FUNCTION_METHOD_NAME = "apply"; @Override public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) { return IncrementalExtensionType.ISOLATING; } @Override public boolean applicable(Context context) { TypeElement valueClass = context.autoValueClass(); return getMatchingStaticMethod(valueClass, ClassName.get(valueClass), CURSOR).isPresent() || getMatchingStaticField(valueClass, getFunc1TypeName(context)).isPresent() || getMatchingStaticField(valueClass, getFunctionTypeName(context)).isPresent(); } @Override public String generateClass( Context context, String className, String classToExtend, boolean isFinal) { ImmutableList<ColumnProperty> properties = ColumnProperty.from(context); TypeSpec.Builder subclass = newTypeSpecBuilder(context, className, classToExtend, isFinal) .addMethod(createReadMethod(context, properties)); TypeName func1TypeName = getFunc1TypeName(context); if (getMatchingStaticField(context.autoValueClass(), func1TypeName).isPresent()) { subclass.addField(createRxJava1Mapper(context, func1TypeName)); } TypeName functionTypeName = getFunctionTypeName(context); if (getMatchingStaticField(context.autoValueClass(), functionTypeName).isPresent()) { subclass.addField(createRxJava2Mapper(context, functionTypeName)); } return JavaFile.builder(context.packageName(), subclass.build()).build().toString(); } private MethodSpec createReadMethod(Context context, ImmutableList<ColumnProperty> properties) { MethodSpec.Builder readMethod = MethodSpec.methodBuilder(METHOD_NAME) .addModifiers(STATIC) .returns(getFinalClassClassName(context)) .addParameter(CURSOR, "cursor"); ImmutableMap<ClassName, String> columnAdapters = addColumnAdaptersToMethod(readMethod, properties); String[] names = new String[properties.size()]; for (int i = 0; i < properties.size(); i++) { ColumnProperty property = properties.get(i); names[i] = property.humanName(); if (property.columnAdapter() != null) { readMethod.addStatement( "$T $N = $L.fromCursor(cursor, $S)", property.type(), property.humanName(), columnAdapters.get(property.columnAdapter()), property.columnName()); } else if (property.supportedType()) { if (property.nullable()) { readMethod.addCode(readNullableProperty(property)); } else { readMethod.addCode(readProperty(property)); } } else if (property.nullable()) { readMethod.addCode( "$T $N = null; // can't be read from cursor\n", property.type(), property.humanName()); } else { error(context, property, "Property has type that can't be read from Cursor."); } } return readMethod .addCode("return ") .addCode(newFinalClassConstructorCall(context, names)) .build(); } private CodeBlock readProperty(ColumnProperty property) { CodeBlock getValue = CodeBlock.of(checkNotNull(property.cursorMethod()), getColumnIndexOrThrow(property)); return CodeBlock.builder() .addStatement("$T $N = $L", property.type(), property.humanName(), getValue) .build(); } private CodeBlock readNullableProperty(ColumnProperty property) { String columnIndexVar = property.humanName() + "ColumnIndex"; String cursorMethod = checkNotNull(property.cursorMethod()); CodeBlock getValue = CodeBlock.builder() .add("($L == -1 || cursor.isNull($L)) ? null : ", columnIndexVar, columnIndexVar) .add(cursorMethod, columnIndexVar) .build(); return CodeBlock.builder() .addStatement("int $L = $L", columnIndexVar, getColumnIndex(property)) .addStatement("$T $N = $L", property.type(), property.humanName(), getValue) .build(); } private CodeBlock getColumnIndexOrThrow(ColumnProperty property) { return CodeBlock.of("cursor.getColumnIndexOrThrow($S)", property.columnName()); } private CodeBlock getColumnIndex(ColumnProperty property) { return CodeBlock.of("cursor.getColumnIndex($S)", property.columnName()); } private FieldSpec createRxJava1Mapper(Context context, TypeName func1Name) { MethodSpec func1Method = MethodSpec.methodBuilder(FUNC1_METHOD_NAME) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(CURSOR, "c") .returns(getFinalClassClassName(context)) .addStatement("return $L($N)", METHOD_NAME, "c") .build(); TypeSpec func1 = TypeSpec.anonymousClassBuilder("") .addSuperinterface(func1Name) .addMethod(func1Method) .build(); return FieldSpec.builder(func1Name, FUNC1_FIELD_NAME, STATIC, FINAL) .initializer("$L", func1) .build(); } private FieldSpec createRxJava2Mapper(Context context, TypeName functionName) { MethodSpec functionMethod = MethodSpec.methodBuilder(FUNCTION_METHOD_NAME) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(CURSOR, "c") .returns(getFinalClassClassName(context)) .addStatement("return $L($N)", METHOD_NAME, "c") .build(); TypeSpec function = TypeSpec.anonymousClassBuilder("") .addSuperinterface(functionName) .addMethod(functionMethod) .build(); return FieldSpec.builder(functionName, FUNCTION_FIELD_NAME, STATIC, FINAL) .initializer("$L", function) .build(); } private TypeName getFunc1TypeName(Context context) { return ParameterizedTypeName.get(FUNC1, CURSOR, getAutoValueClassTypeName(context)); } private TypeName getFunctionTypeName(Context context) { return ParameterizedTypeName.get(FUNCTION, CURSOR, getAutoValueClassTypeName(context)); } public static ImmutableMap<ClassName, String> addColumnAdaptersToMethod( MethodSpec.Builder method, List<ColumnProperty> properties) { Map<ClassName, String> columnAdapters = new LinkedHashMap<>(); NameAllocator nameAllocator = new NameAllocator(); for (ColumnProperty property : properties) { ClassName adapter = property.columnAdapter(); if (adapter != null && !columnAdapters.containsKey(adapter)) { String name = nameAllocator.newName(toLowerCase(adapter.simpleName())); method.addStatement("$1T $2L = new $1T()", adapter, name); columnAdapters.put(adapter, name); } } return ImmutableMap.copyOf(columnAdapters); } private static String toLowerCase(String s) { return Character.toLowerCase(s.charAt(0)) + s.substring(1); } }