blob: 5c7d8a3d860b8c00b78336ad1edab7efb983e087 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.
*/
package android.arch.persistence.room.writer
import android.arch.persistence.room.ext.L
import android.arch.persistence.room.ext.N
import android.arch.persistence.room.ext.RoomTypeNames
import android.arch.persistence.room.ext.SupportDbTypeNames
import android.arch.persistence.room.ext.T
import android.arch.persistence.room.ext.typeName
import android.arch.persistence.room.parser.QueryType
import android.arch.persistence.room.processor.OnConflictProcessor
import android.arch.persistence.room.solver.CodeGenScope
import android.arch.persistence.room.vo.Dao
import android.arch.persistence.room.vo.Entity
import android.arch.persistence.room.vo.InsertionMethod
import android.arch.persistence.room.vo.QueryMethod
import android.arch.persistence.room.vo.ShortcutMethod
import android.arch.persistence.room.vo.TransactionMethod
import com.google.auto.common.MoreTypes
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import stripNonJava
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.Modifier.PRIVATE
import javax.lang.model.element.Modifier.PUBLIC
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.TypeKind
/**
* Creates the implementation for a class annotated with Dao.
*/
class DaoWriter(val dao: Dao, val processingEnv: ProcessingEnvironment)
: ClassWriter(dao.typeName) {
val declaredDao = MoreTypes.asDeclared(dao.element.asType())
companion object {
// TODO nothing prevents this from conflicting, we should fix.
val dbField: FieldSpec = FieldSpec
.builder(RoomTypeNames.ROOM_DB, "__db", PRIVATE, FINAL)
.build()
private fun typeNameToFieldName(typeName: TypeName?): String {
if (typeName is ClassName) {
return typeName.simpleName()
} else {
return typeName.toString().replace('.', '_').stripNonJava()
}
}
}
override fun createTypeSpecBuilder(): TypeSpec.Builder {
val builder = TypeSpec.classBuilder(dao.implTypeName)
/**
* if delete / update query method wants to return modified rows, we need prepared query.
* in that case, if args are dynamic, we cannot re-use the query, if not, we should re-use
* it. this requires more work but creates good performance.
*/
val groupedDeleteUpdate = dao.queryMethods
.filter { it.query.type == QueryType.DELETE || it.query.type == QueryType.UPDATE }
.groupBy { it.parameters.any { it.queryParamAdapter?.isMultiple ?: true } }
// delete queries that can be prepared ahead of time
val preparedDeleteOrUpdateQueries = groupedDeleteUpdate[false] ?: emptyList()
// delete queries that must be rebuild every single time
val oneOffDeleteOrUpdateQueries = groupedDeleteUpdate[true] ?: emptyList()
val shortcutMethods = createInsertionMethods() +
createDeletionMethods() + createUpdateMethods() + createTransactionMethods() +
createPreparedDeleteOrUpdateQueries(preparedDeleteOrUpdateQueries)
builder.apply {
addModifiers(PUBLIC)
if (dao.element.kind == ElementKind.INTERFACE) {
addSuperinterface(dao.typeName)
} else {
superclass(dao.typeName)
}
addField(dbField)
val dbParam = ParameterSpec
.builder(dao.constructorParamType ?: dbField.type, dbField.name).build()
addMethod(createConstructor(dbParam, shortcutMethods, dao.constructorParamType != null))
shortcutMethods.forEach {
addMethod(it.methodImpl)
}
dao.queryMethods.filter { it.query.type == QueryType.SELECT }.forEach { method ->
addMethod(createSelectMethod(method))
}
oneOffDeleteOrUpdateQueries.forEach {
addMethod(createDeleteOrUpdateQueryMethod(it))
}
}
return builder
}
private fun createPreparedDeleteOrUpdateQueries(preparedDeleteQueries: List<QueryMethod>)
: List<PreparedStmtQuery> {
return preparedDeleteQueries.map { method ->
val fieldSpec = getOrCreateField(PreparedStatementField(method))
val queryWriter = QueryWriter(method)
val fieldImpl = PreparedStatementWriter(queryWriter)
.createAnonymous(this@DaoWriter, dbField)
val methodBody = createPreparedDeleteQueryMethodBody(method, fieldSpec, queryWriter)
PreparedStmtQuery(mapOf(PreparedStmtQuery.NO_PARAM_FIELD
to (fieldSpec to fieldImpl)), methodBody)
}
}
private fun createPreparedDeleteQueryMethodBody(method: QueryMethod,
preparedStmtField: FieldSpec,
queryWriter: QueryWriter): MethodSpec {
val scope = CodeGenScope(this)
val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
val stmtName = scope.getTmpVar("_stmt")
addStatement("final $T $L = $N.acquire()",
SupportDbTypeNames.SQLITE_STMT, stmtName, preparedStmtField)
addStatement("$N.beginTransaction()", dbField)
beginControlFlow("try").apply {
val bindScope = scope.fork()
queryWriter.bindArgs(stmtName, emptyList(), bindScope)
addCode(bindScope.builder().build())
if (method.returnsValue) {
val resultVar = scope.getTmpVar("_result")
addStatement("final $L $L = $L.executeUpdateDelete()",
method.returnType.typeName(), resultVar, stmtName)
addStatement("$N.setTransactionSuccessful()", dbField)
addStatement("return $L", resultVar)
} else {
addStatement("$L.executeUpdateDelete()", stmtName)
addStatement("$N.setTransactionSuccessful()", dbField)
}
}
nextControlFlow("finally").apply {
addStatement("$N.endTransaction()", dbField)
addStatement("$N.release($L)", preparedStmtField, stmtName)
}
endControlFlow()
}
return methodBuilder.build()
}
private fun createTransactionMethods(): List<PreparedStmtQuery> {
return dao.transactionMethods.map {
PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
}
}
private fun createTransactionMethodBody(method: TransactionMethod): MethodSpec {
val scope = CodeGenScope(this)
val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
addStatement("$N.beginTransaction()", dbField)
beginControlFlow("try").apply {
val returnsValue = method.element.returnType.kind != TypeKind.VOID
val resultVar = if (returnsValue) {
scope.getTmpVar("_result")
} else {
null
}
addDelegateToSuperStatement(method.element, resultVar)
addStatement("$N.setTransactionSuccessful()", dbField)
if (returnsValue) {
addStatement("return $N", resultVar)
}
}
nextControlFlow("finally").apply {
addStatement("$N.endTransaction()", dbField)
}
endControlFlow()
}
return methodBuilder.build()
}
private fun MethodSpec.Builder.addDelegateToSuperStatement(element: ExecutableElement,
result: String?) {
val params: MutableList<Any> = mutableListOf()
val format = buildString {
if (result != null) {
append("$T $L = ")
params.add(element.returnType)
params.add(result)
}
append("super.$N(")
params.add(element.simpleName)
var first = true
element.parameters.forEach {
if (first) {
first = false
} else {
append(", ")
}
append(L)
params.add(it.simpleName)
}
append(")")
}
addStatement(format, *params.toTypedArray())
}
private fun createConstructor(dbParam: ParameterSpec,
shortcutMethods: List<PreparedStmtQuery>,
callSuper: Boolean): MethodSpec {
return MethodSpec.constructorBuilder().apply {
addParameter(dbParam)
addModifiers(PUBLIC)
if (callSuper) {
addStatement("super($N)", dbParam)
}
addStatement("this.$N = $N", dbField, dbParam)
shortcutMethods.filterNot {
it.fields.isEmpty()
}.map {
it.fields.values
}.flatten().groupBy {
it.first.name
}.map {
it.value.first()
}.forEach {
addStatement("this.$N = $L", it.first, it.second)
}
}.build()
}
private fun createSelectMethod(method: QueryMethod): MethodSpec {
return overrideWithoutAnnotations(method.element, declaredDao).apply {
addCode(createQueryMethodBody(method))
}.build()
}
private fun createDeleteOrUpdateQueryMethod(method: QueryMethod): MethodSpec {
return overrideWithoutAnnotations(method.element, declaredDao).apply {
addCode(createDeleteOrUpdateQueryMethodBody(method))
}.build()
}
/**
* Groups all insertion methods based on the insert statement they will use then creates all
* field specs, EntityInsertionAdapterWriter and actual insert methods.
*/
private fun createInsertionMethods(): List<PreparedStmtQuery> {
return dao.insertionMethods
.map { insertionMethod ->
val onConflict = OnConflictProcessor.onConflictText(insertionMethod.onConflict)
val entities = insertionMethod.entities
val fields = entities.mapValues {
val spec = getOrCreateField(InsertionMethodField(it.value, onConflict))
val impl = EntityInsertionAdapterWriter(it.value, onConflict)
.createAnonymous(this@DaoWriter, dbField.name)
spec to impl
}
val methodImpl = overrideWithoutAnnotations(insertionMethod.element,
declaredDao).apply {
addCode(createInsertionMethodBody(insertionMethod, fields))
}.build()
PreparedStmtQuery(fields, methodImpl)
}.filterNotNull()
}
private fun createInsertionMethodBody(method: InsertionMethod,
insertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>)
: CodeBlock {
val insertionType = method.insertionType
if (insertionAdapters.isEmpty() || insertionType == null) {
return CodeBlock.builder().build()
}
val scope = CodeGenScope(this)
return scope.builder().apply {
// TODO assert thread
// TODO collect results
addStatement("$N.beginTransaction()", dbField)
val needsReturnType = insertionType != InsertionMethod.Type.INSERT_VOID
val resultVar = if (needsReturnType) {
scope.getTmpVar("_result")
} else {
null
}
beginControlFlow("try").apply {
method.parameters.forEach { param ->
val insertionAdapter = insertionAdapters[param.name]?.first
if (needsReturnType) {
// if it has more than 1 parameter, we would've already printed the error
// so we don't care about re-declaring the variable here
addStatement("$T $L = $N.$L($L)",
insertionType.returnTypeName, resultVar,
insertionAdapter, insertionType.methodName,
param.name)
} else {
addStatement("$N.$L($L)", insertionAdapter, insertionType.methodName,
param.name)
}
}
addStatement("$N.setTransactionSuccessful()", dbField)
if (needsReturnType) {
addStatement("return $L", resultVar)
}
}
nextControlFlow("finally").apply {
addStatement("$N.endTransaction()", dbField)
}
endControlFlow()
}.build()
}
/**
* Creates EntityUpdateAdapter for each deletion method.
*/
private fun createDeletionMethods(): List<PreparedStmtQuery> {
return createShortcutMethods(dao.deletionMethods, "deletion", { _, entity ->
EntityDeletionAdapterWriter(entity)
.createAnonymous(this@DaoWriter, dbField.name)
})
}
/**
* Creates EntityUpdateAdapter for each @Update method.
*/
private fun createUpdateMethods(): List<PreparedStmtQuery> {
return createShortcutMethods(dao.updateMethods, "update", { update, entity ->
val onConflict = OnConflictProcessor.onConflictText(update.onConflictStrategy)
EntityUpdateAdapterWriter(entity, onConflict)
.createAnonymous(this@DaoWriter, dbField.name)
})
}
private fun <T : ShortcutMethod> createShortcutMethods(methods: List<T>, methodPrefix: String,
implCallback: (T, Entity) -> TypeSpec)
: List<PreparedStmtQuery> {
return methods.map { method ->
val entities = method.entities
if (entities.isEmpty()) {
null
} else {
val fields = entities.mapValues {
val spec = getOrCreateField(DeleteOrUpdateAdapterField(it.value, methodPrefix))
val impl = implCallback(method, it.value)
spec to impl
}
val methodSpec = overrideWithoutAnnotations(method.element, declaredDao).apply {
addCode(createDeleteOrUpdateMethodBody(method, fields))
}.build()
PreparedStmtQuery(fields, methodSpec)
}
}.filterNotNull()
}
private fun createDeleteOrUpdateMethodBody(method: ShortcutMethod,
adapters: Map<String, Pair<FieldSpec, TypeSpec>>)
: CodeBlock {
if (adapters.isEmpty()) {
return CodeBlock.builder().build()
}
val scope = CodeGenScope(this)
val resultVar = if (method.returnCount) {
scope.getTmpVar("_total")
} else {
null
}
return scope.builder().apply {
if (resultVar != null) {
addStatement("$T $L = 0", TypeName.INT, resultVar)
}
addStatement("$N.beginTransaction()", dbField)
beginControlFlow("try").apply {
method.parameters.forEach { param ->
val adapter = adapters[param.name]?.first
addStatement("$L$N.$L($L)",
if (resultVar == null) "" else "$resultVar +=",
adapter, param.handleMethodName(), param.name)
}
addStatement("$N.setTransactionSuccessful()", dbField)
if (resultVar != null) {
addStatement("return $L", resultVar)
}
}
nextControlFlow("finally").apply {
addStatement("$N.endTransaction()", dbField)
}
endControlFlow()
}.build()
}
/**
* @Query with delete action
*/
private fun createDeleteOrUpdateQueryMethodBody(method: QueryMethod): CodeBlock {
val queryWriter = QueryWriter(method)
val scope = CodeGenScope(this)
val sqlVar = scope.getTmpVar("_sql")
val stmtVar = scope.getTmpVar("_stmt")
val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
scope.builder().apply {
addStatement("$T $L = $N.compileStatement($L)",
SupportDbTypeNames.SQLITE_STMT, stmtVar, dbField, sqlVar)
queryWriter.bindArgs(stmtVar, listSizeArgs, scope)
addStatement("$N.beginTransaction()", dbField)
beginControlFlow("try").apply {
if (method.returnsValue) {
val resultVar = scope.getTmpVar("_result")
addStatement("final $L $L = $L.executeUpdateDelete()",
method.returnType.typeName(), resultVar, stmtVar)
addStatement("$N.setTransactionSuccessful()", dbField)
addStatement("return $L", resultVar)
} else {
addStatement("$L.executeUpdateDelete()", stmtVar)
addStatement("$N.setTransactionSuccessful()", dbField)
}
}
nextControlFlow("finally").apply {
addStatement("$N.endTransaction()", dbField)
}
endControlFlow()
}
return scope.builder().build()
}
private fun createQueryMethodBody(method: QueryMethod): CodeBlock {
val queryWriter = QueryWriter(method)
val scope = CodeGenScope(this)
val sqlVar = scope.getTmpVar("_sql")
val roomSQLiteQueryVar = scope.getTmpVar("_statement")
queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
method.queryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField, scope)
return scope.builder().build()
}
private fun overrideWithoutAnnotations(elm: ExecutableElement,
owner : DeclaredType): MethodSpec.Builder {
val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils).build()
return MethodSpec.methodBuilder(baseSpec.name).apply {
addAnnotation(Override::class.java)
addModifiers(baseSpec.modifiers)
addParameters(baseSpec.parameters)
varargs(baseSpec.varargs)
returns(baseSpec.returnType)
}
}
/**
* Represents a query statement prepared in Dao implementation.
*
* @param fields This map holds all the member fields necessary for this query. The key is the
* corresponding parameter name in the defining query method. The value is a pair from the field
* declaration to definition.
* @param methodImpl The body of the query method implementation.
*/
data class PreparedStmtQuery(val fields: Map<String, Pair<FieldSpec, TypeSpec>>,
val methodImpl: MethodSpec) {
companion object {
// The key to be used in `fields` where the method requires a field that is not
// associated with any of its parameters
const val NO_PARAM_FIELD = "-"
}
}
private class InsertionMethodField(val entity: Entity, val onConflictText: String)
: SharedFieldSpec(
"insertionAdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
RoomTypeNames.INSERTION_ADAPTER) {
override fun getUniqueKey(): String {
return "${entity.typeName} $onConflictText"
}
override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
builder.addModifiers(FINAL, PRIVATE)
}
}
class DeleteOrUpdateAdapterField(val entity: Entity, val methodPrefix: String)
: SharedFieldSpec(
"${methodPrefix}AdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
RoomTypeNames.DELETE_OR_UPDATE_ADAPTER) {
override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
builder.addModifiers(PRIVATE, FINAL)
}
override fun getUniqueKey(): String {
return entity.typeName.toString() + methodPrefix
}
}
class PreparedStatementField(val method: QueryMethod) : SharedFieldSpec(
"preparedStmtOf${method.name.capitalize()}", RoomTypeNames.SHARED_SQLITE_STMT) {
override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
builder.addModifiers(PRIVATE, FINAL)
}
override fun getUniqueKey(): String {
return method.query.original
}
}
}