/*
* Copyright (C) 2017 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.solver.query.result
import androidx.room.compiler.processing.XType
import androidx.room.ext.L
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.S
import androidx.room.ext.T
import androidx.room.ext.capitalize
import androidx.room.ext.stripNonJava
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.processor.ProcessorErrors
import androidx.room.processor.ProcessorErrors.ISSUE_TRACKER_LINK
import androidx.room.solver.CodeGenScope
import androidx.room.verifier.QueryResultInfo
import androidx.room.vo.Field
import androidx.room.vo.FieldWithIndex
import androidx.room.vo.Pojo
import androidx.room.vo.RelationCollector
import androidx.room.vo.findFieldByColumnName
import androidx.room.writer.FieldReadWriteWriter
import com.squareup.javapoet.TypeName
import java.util.Locale
/**
* Creates the entity from the given info.
* <p>
* The info comes from the query processor so we know about the order of columns in the result etc.
*/
class PojoRowAdapter(
context: Context,
private val info: QueryResultInfo?,
private val query: ParsedQuery?,
val pojo: Pojo,
out: XType
) : RowAdapter(out), QueryMappedRowAdapter {
override val mapping: PojoMapping
val relationCollectors: List<RelationCollector>
// Set when cursor is ready.
lateinit var fieldsWithIndices: List<FieldWithIndex>
init {
// toMutableList documentation is not clear if it copies so lets be safe.
val remainingFields = pojo.fields.mapTo(mutableListOf(), { it })
val unusedColumns = arrayListOf<String>()
val matchedFields: List<Field>
if (info != null) {
matchedFields = info.columns.mapNotNull { column ->
// first check remaining, otherwise check any. maybe developer wants to map the same
// column into 2 fields. (if they want to post process etc)
val field = remainingFields.firstOrNull { it.columnName == column.name }
?: pojo.findFieldByColumnName(column.name)
if (field == null) {
unusedColumns.add(column.name)
null
} else {
remainingFields.remove(field)
field
}
}
val nonNulls = remainingFields.filter { it.nonNull }
if (nonNulls.isNotEmpty()) {
context.logger.e(
ProcessorErrors.pojoMissingNonNull(
pojoTypeName = pojo.typeName,
missingPojoFields = nonNulls.map { it.name },
allQueryColumns = info.columns.map { it.name }
)
)
}
if (matchedFields.isEmpty()) {
context.logger.e(ProcessorErrors.cannotFindQueryResultAdapter(out.typeName))
}
} else {
matchedFields = remainingFields.map { it }
remainingFields.clear()
}
relationCollectors = RelationCollector.createCollectors(context, pojo.relations)
mapping = PojoMapping(
pojo = pojo,
matchedFields = matchedFields,
unusedColumns = unusedColumns,
unusedFields = remainingFields
)
}
fun relationTableNames(): List<String> {
return relationCollectors.flatMap {
val queryTableNames = it.loadAllQuery.tables.map { it.name }
if (it.rowAdapter is PojoRowAdapter) {
it.rowAdapter.relationTableNames() + queryTableNames
} else {
queryTableNames
}
}.distinct()
}
override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
fieldsWithIndices = mapping.matchedFields.map {
val indexVar = scope.getTmpVar(
"_cursorIndexOf${it.name.stripNonJava().capitalize(Locale.US)}"
)
if (info != null && query != null && query.hasTopStarProjection == false) {
// When result info is available and query does not have a top-level star
// projection we can generate column to field index since the column result order
// is deterministic.
val infoIndex = info.columns.indexOfFirst { columnInfo ->
columnInfo.name == it.columnName
}
check(infoIndex != -1) {
"Result column index not found for field '$it' with column name " +
"'${it.columnName}'. Query: ${query.original}. Please file a bug at " +
ISSUE_TRACKER_LINK
}
scope.builder().addStatement(
"final $T $L = $L",
TypeName.INT, indexVar, infoIndex
)
} else {
val indexMethod = if (info == null) {
"getColumnIndex"
} else {
"getColumnIndexOrThrow"
}
scope.builder().addStatement(
"final $T $L = $T.$L($L, $S)",
TypeName.INT, indexVar, RoomTypeNames.CURSOR_UTIL, indexMethod, cursorVarName,
it.columnName
)
}
FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
}
if (relationCollectors.isNotEmpty()) {
relationCollectors.forEach { it.writeInitCode(scope) }
scope.builder().apply {
beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
relationCollectors.forEach {
it.writeReadParentKeyCode(cursorVarName, fieldsWithIndices, scope)
}
}
endControlFlow()
}
scope.builder().addStatement("$L.moveToPosition(-1)", cursorVarName)
relationCollectors.forEach { it.writeCollectionCode(scope) }
}
}
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder().apply {
FieldReadWriteWriter.readFromCursor(
outVar = outVarName,
outPojo = pojo,
cursorVar = cursorVarName,
fieldsWithIndices = fieldsWithIndices,
relationCollectors = relationCollectors,
scope = scope
)
}
}
data class PojoMapping(
val pojo: Pojo,
val matchedFields: List<Field>,
val unusedColumns: List<String>,
val unusedFields: List<Field>
) : QueryMappedRowAdapter.Mapping() {
override val usedColumns = matchedFields.map { it.columnName }
}
}