/*
* 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 androidx.room.writer
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.VisibilityModifier
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
import androidx.room.compiler.codegen.XFunSpec
import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.apply
import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.RoomMemberNames
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
import androidx.room.ext.RoomTypeNames.INSERTION_ADAPTER
import androidx.room.ext.RoomTypeNames.ROOM_DB
import androidx.room.ext.RoomTypeNames.SHARED_SQLITE_STMT
import androidx.room.ext.RoomTypeNames.UPSERTION_ADAPTER
import androidx.room.ext.SupportDbTypeNames
import androidx.room.ext.capitalize
import androidx.room.processor.OnConflictProcessor
import androidx.room.solver.CodeGenScope
import androidx.room.solver.KotlinBoxedPrimitiveMethodDelegateBinder
import androidx.room.solver.KotlinDefaultMethodDelegateBinder
import androidx.room.solver.types.getRequiredTypeConverters
import androidx.room.vo.Dao
import androidx.room.vo.DeleteOrUpdateShortcutMethod
import androidx.room.vo.InsertionMethod
import androidx.room.vo.KotlinBoxedPrimitiveMethodDelegate
import androidx.room.vo.KotlinDefaultMethodDelegate
import androidx.room.vo.QueryMethod
import androidx.room.vo.RawQueryMethod
import androidx.room.vo.ReadQueryMethod
import androidx.room.vo.ShortcutEntity
import androidx.room.vo.TransactionMethod
import androidx.room.vo.UpdateMethod
import androidx.room.vo.UpsertionMethod
import androidx.room.vo.WriteQueryMethod
import java.util.Locale
/**
* Creates the implementation for a class annotated with Dao.
*/
class DaoWriter(
val dao: Dao,
private val dbElement: XElement,
codeLanguage: CodeLanguage
) : TypeWriter(codeLanguage) {
private val declaredDao = dao.element.type
// TODO nothing prevents this from conflicting, we should fix.
private val dbProperty: XPropertySpec = XPropertySpec
.builder(codeLanguage, DB_PROPERTY_NAME, ROOM_DB, VisibilityModifier.PRIVATE)
.build()
private val companionTypeBuilder = lazy {
XTypeSpec.companionObjectBuilder(codeLanguage)
}
companion object {
const val GET_LIST_OF_TYPE_CONVERTERS_METHOD = "getRequiredConverters"
const val DB_PROPERTY_NAME = "__db"
private fun shortcutEntityFieldNamePart(shortcutEntity: ShortcutEntity): String {
fun typeNameToFieldName(typeName: XClassName): String {
return typeName.simpleNames.last()
}
return if (shortcutEntity.isPartialEntity) {
typeNameToFieldName(shortcutEntity.pojo.className) + "As" +
typeNameToFieldName(shortcutEntity.entityClassName)
} else {
typeNameToFieldName(shortcutEntity.entityClassName)
}
}
}
override fun createTypeSpecBuilder(): XTypeSpec.Builder {
val builder = XTypeSpec.classBuilder(codeLanguage, dao.implTypeName)
/**
* For prepared statements that perform insert/update/delete/upsert,
* we check if there are any arguments of variable length (e.g. "IN (:var)").
* If not, we should re-use the statement.
* This requires more work but creates good performance.
*/
val groupedPreparedQueries = dao.queryMethods
.filterIsInstance<WriteQueryMethod>()
.groupBy { it.parameters.any { it.queryParamAdapter?.isMultiple ?: true } }
// queries that can be prepared ahead of time
val preparedQueries = groupedPreparedQueries[false] ?: emptyList()
// queries that must be rebuilt every single time
val oneOffPreparedQueries = groupedPreparedQueries[true] ?: emptyList()
val shortcutMethods = buildList {
addAll(createInsertionMethods())
addAll(createDeletionMethods())
addAll(createUpdateMethods())
addAll(createTransactionMethods())
addAll(createPreparedQueries(preparedQueries))
addAll(createUpsertMethods())
}
builder.apply {
addOriginatingElement(dbElement)
setVisibility(
if (dao.element.isInternal()) {
VisibilityModifier.INTERNAL
} else {
VisibilityModifier.PUBLIC
}
)
if (dao.element.isInterface()) {
addSuperinterface(dao.typeName)
} else {
superclass(dao.typeName)
}
addProperty(dbProperty)
setPrimaryConstructor(
createConstructor(
shortcutMethods,
dao.constructorParamType != null
)
)
shortcutMethods.forEach {
addFunction(it.functionImpl)
}
dao.queryMethods.filterIsInstance<ReadQueryMethod>().forEach { method ->
addFunction(createSelectMethod(method))
}
oneOffPreparedQueries.forEach {
addFunction(createPreparedQueryMethod(it))
}
dao.rawQueryMethods.forEach {
addFunction(createRawQueryMethod(it))
}
if (codeLanguage == CodeLanguage.JAVA) {
dao.kotlinDefaultMethodDelegates.forEach {
addFunction(createDefaultImplMethodDelegate(it))
}
dao.kotlinBoxedPrimitiveMethodDelegates.forEach {
addFunction(createBoxedPrimitiveBridgeMethodDelegate(it))
}
}
// Keep this the last one to be generated because used custom converters will
// register fields with a payload which we collect in dao to report used
// Type Converters.
addConverterListMethod(this)
if (companionTypeBuilder.isInitialized()) {
addType(companionTypeBuilder.value.build())
}
}
return builder
}
private fun addConverterListMethod(typeSpecBuilder: XTypeSpec.Builder) {
when (codeLanguage) {
// For Java a static method is created
CodeLanguage.JAVA -> typeSpecBuilder.addFunction(createConverterListMethod())
// For Kotlin a function in the companion object is created
CodeLanguage.KOTLIN -> companionTypeBuilder.value
.addFunction(createConverterListMethod())
.build()
}
}
private fun createConverterListMethod(): XFunSpec {
val body = XCodeBlock.builder(codeLanguage).apply {
val requiredTypeConverters = getRequiredTypeConverters()
if (requiredTypeConverters.isEmpty()) {
when (language) {
CodeLanguage.JAVA ->
addStatement("return %T.emptyList()", CommonTypeNames.COLLECTIONS)
CodeLanguage.KOTLIN ->
addStatement("return emptyList()")
}
} else {
val placeholders = requiredTypeConverters.joinToString(",") { "%L" }
val requiredTypeConvertersLiterals = requiredTypeConverters.map {
XCodeBlock.ofJavaClassLiteral(language, it)
}.toTypedArray()
when (language) {
CodeLanguage.JAVA ->
addStatement("return %T.asList($placeholders)",
CommonTypeNames.ARRAYS,
*requiredTypeConvertersLiterals
)
CodeLanguage.KOTLIN ->
addStatement(
"return listOf($placeholders)",
*requiredTypeConvertersLiterals
)
}
}
}.build()
return XFunSpec.builder(
codeLanguage,
GET_LIST_OF_TYPE_CONVERTERS_METHOD,
VisibilityModifier.PUBLIC
).apply(
javaMethodBuilder = {
addModifiers(javax.lang.model.element.Modifier.STATIC)
},
kotlinFunctionBuilder = {
addAnnotation(kotlin.jvm.JvmStatic::class)
},
).apply {
returns(
CommonTypeNames.LIST.parametrizedBy(
CommonTypeNames.JAVA_CLASS.parametrizedBy(XTypeName.ANY_WILDCARD)
)
)
addCode(body)
}.build()
}
private fun createPreparedQueries(
preparedQueries: List<WriteQueryMethod>
): List<PreparedStmtQuery> {
return preparedQueries.map { method ->
val fieldSpec = getOrCreateProperty(PreparedStatementProperty(method))
val queryWriter = QueryWriter(method)
val fieldImpl = PreparedStatementWriter(queryWriter)
.createAnonymous(this@DaoWriter, dbProperty)
val methodBody =
createPreparedQueryMethodBody(method, fieldSpec, queryWriter)
PreparedStmtQuery(
mapOf(PreparedStmtQuery.NO_PARAM_FIELD to (fieldSpec to fieldImpl)),
methodBody
)
}
}
private fun createPreparedQueryMethodBody(
method: WriteQueryMethod,
preparedStmtField: XPropertySpec,
queryWriter: QueryWriter
): XFunSpec {
val scope = CodeGenScope(this)
method.preparedQueryResultBinder.executeAndReturn(
prepareQueryStmtBlock = {
val stmtName = getTmpVar("_stmt")
builder.addLocalVal(
stmtName,
SupportDbTypeNames.SQLITE_STMT,
"%N.acquire()",
preparedStmtField
)
queryWriter.bindArgs(stmtName, emptyList(), this)
stmtName
},
preparedStmtProperty = preparedStmtField,
dbProperty = dbProperty,
scope = scope
)
return overrideWithoutAnnotations(method.element, declaredDao)
.addCode(scope.generate())
.build()
}
private fun createTransactionMethods(): List<PreparedStmtQuery> {
return dao.transactionMethods.map {
PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
}
}
private fun createTransactionMethodBody(method: TransactionMethod): XFunSpec {
val scope = CodeGenScope(this)
method.methodBinder.executeAndReturn(
returnType = method.returnType,
parameterNames = method.parameterNames,
daoName = dao.typeName,
daoImplName = dao.implTypeName,
dbProperty = dbProperty,
scope = scope
)
return overrideWithoutAnnotations(method.element, declaredDao)
.addCode(scope.generate())
.build()
}
private fun createConstructor(
shortcutMethods: List<PreparedStmtQuery>,
callSuper: Boolean
): XFunSpec {
val body = XCodeBlock.builder(codeLanguage).apply {
addStatement("this.%N = %L", dbProperty, dbProperty.name)
shortcutMethods.asSequence().filterNot {
it.fields.isEmpty()
}.map {
it.fields.values
}.flatten().groupBy {
it.first.name
}.map {
it.value.first()
}.forEach { (propertySpec, initExpression) ->
addStatement("this.%L = %L", propertySpec.name, initExpression)
}
}.build()
return XFunSpec.constructorBuilder(codeLanguage, VisibilityModifier.PUBLIC).apply {
addParameter(
typeName = dao.constructorParamType ?: ROOM_DB,
name = dbProperty.name
)
if (callSuper) {
callSuperConstructor(XCodeBlock.of(language, "%L", dbProperty.name))
}
addCode(body)
}.build()
}
private fun createSelectMethod(method: ReadQueryMethod): XFunSpec {
return overrideWithoutAnnotations(method.element, declaredDao)
.addCode(createQueryMethodBody(method))
.build()
}
private fun createRawQueryMethod(method: RawQueryMethod): XFunSpec {
val body = XCodeBlock.builder(codeLanguage).apply {
val scope = CodeGenScope(this@DaoWriter)
val roomSQLiteQueryVar: String
val queryParam = method.runtimeQueryParam
val shouldReleaseQuery: Boolean
if (queryParam?.isSupportQuery() == true) {
roomSQLiteQueryVar = queryParam.paramName
shouldReleaseQuery = false
} else if (queryParam?.isString() == true) {
roomSQLiteQueryVar = scope.getTmpVar("_statement")
shouldReleaseQuery = true
addLocalVariable(
name = roomSQLiteQueryVar,
typeName = RoomTypeNames.ROOM_SQL_QUERY,
assignExpr = XCodeBlock.of(
codeLanguage,
"%M(%L, 0)",
RoomMemberNames.ROOM_SQL_QUERY_ACQUIRE,
queryParam.paramName
),
)
} else {
// try to generate compiling code. we would've already reported this error
roomSQLiteQueryVar = scope.getTmpVar("_statement")
shouldReleaseQuery = false
addLocalVariable(
name = roomSQLiteQueryVar,
typeName = RoomTypeNames.ROOM_SQL_QUERY,
assignExpr = XCodeBlock.of(
codeLanguage,
"%M(%S, 0)",
RoomMemberNames.ROOM_SQL_QUERY_ACQUIRE,
"missing query parameter"
),
)
}
if (method.returnsValue) {
// don't generate code because it will create 1 more error. The original error is
// already reported by the processor.
method.queryResultBinder.convertAndReturn(
roomSQLiteQueryVar = roomSQLiteQueryVar,
canReleaseQuery = shouldReleaseQuery,
dbProperty = dbProperty,
inTransaction = method.inTransaction,
scope = scope
)
}
add(scope.generate())
}.build()
return overrideWithoutAnnotations(method.element, declaredDao)
.addCode(body)
.build()
}
private fun createPreparedQueryMethod(method: WriteQueryMethod): XFunSpec {
return overrideWithoutAnnotations(method.element, declaredDao)
.addCode(createPreparedQueryMethodBody(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 = getOrCreateProperty(InsertionMethodProperty(it.value, onConflict))
val impl = EntityInsertionAdapterWriter.create(it.value, onConflict)
.createAnonymous(this@DaoWriter, dbProperty)
spec to impl
}
val methodImpl = overrideWithoutAnnotations(
insertionMethod.element,
declaredDao
).apply {
addCode(createInsertionMethodBody(insertionMethod, fields))
}.build()
PreparedStmtQuery(fields, methodImpl)
}
}
private fun createInsertionMethodBody(
method: InsertionMethod,
insertionAdapters: Map<String, Pair<XPropertySpec, XTypeSpec>>
): XCodeBlock {
if (insertionAdapters.isEmpty() || method.methodBinder == null) {
return XCodeBlock.builder(codeLanguage).build()
}
val scope = CodeGenScope(this)
method.methodBinder.convertAndReturn(
parameters = method.parameters,
adapters = insertionAdapters,
dbProperty = dbProperty,
scope = scope
)
return scope.generate()
}
/**
* Creates EntityUpdateAdapter for each deletion method.
*/
private fun createDeletionMethods(): List<PreparedStmtQuery> {
return createShortcutMethods(dao.deletionMethods, "deletion") { _, entity ->
EntityDeletionAdapterWriter.create(entity)
.createAnonymous(this@DaoWriter, dbProperty.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.create(entity, onConflict)
.createAnonymous(this@DaoWriter, dbProperty.name)
}
}
private fun <T : DeleteOrUpdateShortcutMethod> createShortcutMethods(
methods: List<T>,
methodPrefix: String,
implCallback: (T, ShortcutEntity) -> XTypeSpec
): List<PreparedStmtQuery> {
return methods.mapNotNull { method ->
val entities = method.entities
if (entities.isEmpty()) {
null
} else {
val onConflict = if (method is UpdateMethod) {
OnConflictProcessor.onConflictText(method.onConflictStrategy)
} else {
""
}
val fields = entities.mapValues {
val spec = getOrCreateProperty(
DeleteOrUpdateAdapterProperty(it.value, methodPrefix, onConflict)
)
val impl = implCallback(method, it.value)
spec to impl
}
val methodSpec = overrideWithoutAnnotations(method.element, declaredDao).apply {
addCode(createDeleteOrUpdateMethodBody(method, fields))
}.build()
PreparedStmtQuery(fields, methodSpec)
}
}
}
private fun createDeleteOrUpdateMethodBody(
method: DeleteOrUpdateShortcutMethod,
adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>
): XCodeBlock {
if (adapters.isEmpty() || method.methodBinder == null) {
return XCodeBlock.builder(codeLanguage).build()
}
val scope = CodeGenScope(this)
method.methodBinder.convertAndReturn(
parameters = method.parameters,
adapters = adapters,
dbProperty = dbProperty,
scope = scope
)
return scope.generate()
}
/**
* Groups all upsertion methods based on the upsert statement they will use then creates all
* field specs, EntityIUpsertionAdapterWriter and actual upsert methods.
*/
private fun createUpsertMethods(): List<PreparedStmtQuery> {
return dao.upsertionMethods
.map { upsertionMethod ->
val entities = upsertionMethod.entities
val fields = entities.mapValues {
val spec = getOrCreateProperty(UpsertionAdapterProperty(it.value))
val impl = EntityUpsertionAdapterWriter.create(it.value)
.createConcrete(it.value, this@DaoWriter, dbProperty)
spec to impl
}
val methodImpl = overrideWithoutAnnotations(
upsertionMethod.element,
declaredDao
).apply {
addCode(createUpsertionMethodBody(upsertionMethod, fields))
}.build()
PreparedStmtQuery(fields, methodImpl)
}
}
private fun createUpsertionMethodBody(
method: UpsertionMethod,
upsertionAdapters: Map<String, Pair<XPropertySpec, XCodeBlock>>
): XCodeBlock {
if (upsertionAdapters.isEmpty() || method.methodBinder == null) {
return XCodeBlock.builder(codeLanguage).build()
}
val scope = CodeGenScope(this)
method.methodBinder.convertAndReturn(
parameters = method.parameters,
adapters = upsertionAdapters,
dbProperty = dbProperty,
scope = scope
)
return scope.generate()
}
private fun createPreparedQueryMethodBody(method: WriteQueryMethod): XCodeBlock {
val scope = CodeGenScope(this)
method.preparedQueryResultBinder.executeAndReturn(
prepareQueryStmtBlock = {
val queryWriter = QueryWriter(method)
val sqlVar = getTmpVar("_sql")
val stmtVar = getTmpVar("_stmt")
val listSizeArgs = queryWriter.prepareQuery(sqlVar, this)
builder.addLocalVal(
stmtVar,
SupportDbTypeNames.SQLITE_STMT,
"%N.compileStatement(%L)",
dbProperty,
sqlVar
)
queryWriter.bindArgs(stmtVar, listSizeArgs, this)
stmtVar
},
preparedStmtProperty = null,
dbProperty = dbProperty,
scope = scope
)
return scope.builder.build()
}
private fun createQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
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 = roomSQLiteQueryVar,
canReleaseQuery = true,
dbProperty = dbProperty,
inTransaction = method.inTransaction,
scope = scope
)
return scope.generate()
}
// TODO(b/251459654): Handle @JvmOverloads in delegating functions with Kotlin codegen.
private fun createDefaultImplMethodDelegate(method: KotlinDefaultMethodDelegate): XFunSpec {
val scope = CodeGenScope(this)
return overrideWithoutAnnotations(method.element, declaredDao).apply {
KotlinDefaultMethodDelegateBinder.executeAndReturn(
daoName = dao.typeName,
daoImplName = dao.implTypeName,
methodName = method.element.jvmName,
returnType = method.element.returnType,
parameterNames = method.element.parameters.map { it.name },
scope = scope
)
addCode(scope.generate())
}.build()
}
private fun createBoxedPrimitiveBridgeMethodDelegate(
method: KotlinBoxedPrimitiveMethodDelegate
): XFunSpec {
val scope = CodeGenScope(this)
return overrideWithoutAnnotations(method.element, declaredDao).apply {
KotlinBoxedPrimitiveMethodDelegateBinder.execute(
methodName = method.element.jvmName,
returnType = method.element.returnType,
parameters = method.concreteMethod.parameters.map {
it.type.asTypeName() to it.name
},
scope = scope
)
addCode(scope.generate())
}.build()
}
private fun overrideWithoutAnnotations(
elm: XMethodElement,
owner: XType
): XFunSpec.Builder {
return XFunSpec.overridingBuilder(codeLanguage, elm, owner)
}
/**
* Represents a query statement prepared in Dao implementation.
*
* @param fields This map holds all the member properties necessary for this query. The key is
* the corresponding parameter name in the defining query method. The value is a pair from the
* property declaration to definition.
* @param functionImpl The body of the query method implementation.
*/
data class PreparedStmtQuery(
val fields: Map<String, Pair<XPropertySpec, Any>>,
val functionImpl: XFunSpec
) {
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 InsertionMethodProperty(
val shortcutEntity: ShortcutEntity,
val onConflictText: String
) : SharedPropertySpec(
baseName = "insertionAdapterOf${shortcutEntityFieldNamePart(shortcutEntity)}",
type = INSERTION_ADAPTER.parametrizedBy(shortcutEntity.pojo.typeName)
) {
override fun getUniqueKey(): String {
return "${shortcutEntity.pojo.typeName}-${shortcutEntity.entityTypeName}$onConflictText"
}
override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
}
}
class DeleteOrUpdateAdapterProperty(
val shortcutEntity: ShortcutEntity,
val methodPrefix: String,
val onConflictText: String
) : SharedPropertySpec(
baseName = "${methodPrefix}AdapterOf${shortcutEntityFieldNamePart(shortcutEntity)}",
type = DELETE_OR_UPDATE_ADAPTER.parametrizedBy(shortcutEntity.pojo.typeName)
) {
override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
}
override fun getUniqueKey(): String {
return "${shortcutEntity.pojo.typeName}-${shortcutEntity.entityTypeName}" +
"$methodPrefix$onConflictText"
}
}
class UpsertionAdapterProperty(
val shortcutEntity: ShortcutEntity
) : SharedPropertySpec(
baseName = "upsertionAdapterOf${shortcutEntityFieldNamePart(shortcutEntity)}",
type = UPSERTION_ADAPTER.parametrizedBy(shortcutEntity.pojo.typeName)
) {
override fun getUniqueKey(): String {
return "${shortcutEntity.pojo.typeName}-${shortcutEntity.entityTypeName}"
}
override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
}
}
class PreparedStatementProperty(val method: QueryMethod) : SharedPropertySpec(
baseName = "preparedStmtOf${method.element.name.capitalize(Locale.US)}",
type = SHARED_SQLITE_STMT
) {
override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
}
override fun getUniqueKey(): String {
return method.query.original
}
}
}