MultimapQueryResultAdapter.kt
/*
* Copyright 2021 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.codegen.toJavaPoet
import androidx.room.compiler.processing.XType
import androidx.room.ext.L
import androidx.room.ext.W
import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.log.RLog
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.processor.ProcessorErrors
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.ENTITY
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.MAP_INFO
import androidx.room.processor.ProcessorErrors.AmbiguousColumnLocation.POJO
import androidx.room.solver.types.CursorValueReader
import androidx.room.vo.ColumnIndexVar
import androidx.room.vo.MapInfo
import androidx.room.vo.Warning
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
/**
* Abstract class for Map and Multimap result adapters.
*/
abstract class MultimapQueryResultAdapter(
context: Context,
parsedQuery: ParsedQuery,
rowAdapters: List<RowAdapter>,
) : QueryResultAdapter(rowAdapters) {
abstract val keyTypeArg: XType
abstract val valueTypeArg: XType
// List of duplicate columns in the query result. Note that if the query result info is not
// available then we use the adapter mappings to determine if there are duplicate columns.
// The latter approach might yield false positive (i.e. two POJOs that want the same column)
// but the resolver will still produce correct results based on the result columns at runtime.
val duplicateColumns: Set<String>
init {
val resultColumns =
parsedQuery.resultInfo?.columns?.map { it.name } ?: mappings.flatMap { it.usedColumns }
duplicateColumns = buildSet {
val visitedColumns = mutableSetOf<String>()
resultColumns.forEach {
// When Set.add() returns false the column is already visited and therefore a dupe.
if (!visitedColumns.add(it)) {
add(it)
}
}
}
if (parsedQuery.resultInfo != null && duplicateColumns.isNotEmpty()) {
// If there are duplicate columns and one of the result object is for a single column
// then we should warn the user to disambiguate in the query projections since the
// current AmbiguousColumnResolver will choose the first matching column. Only show
// this warning if the query has been analyzed or else we risk false positives.
mappings.filter {
it.usedColumns.size == 1 && duplicateColumns.contains(it.usedColumns.first())
}.forEach {
val ambiguousColumnName = it.usedColumns.first()
val (location, objectTypeName) = when (it) {
is SingleNamedColumnRowAdapter.SingleNamedColumnRowMapping ->
MAP_INFO to null
is PojoRowAdapter.PojoMapping ->
POJO to it.pojo.typeName.toJavaPoet()
is EntityRowAdapter.EntityMapping ->
ENTITY to it.entity.typeName.toJavaPoet()
else -> error("Unknown mapping type: $it")
}
context.logger.w(
Warning.AMBIGUOUS_COLUMN_IN_RESULT,
ProcessorErrors.ambiguousColumn(ambiguousColumnName, location, objectTypeName)
)
}
}
}
companion object {
val declaredToImplCollection = mapOf<ClassName, ClassName>(
ClassName.get(List::class.java) to ClassName.get(ArrayList::class.java),
ClassName.get(Set::class.java) to ClassName.get(HashSet::class.java)
)
/**
* Checks if the @MapInfo annotation is needed for clarification regarding the return type
* of a Dao method.
*/
fun validateMapTypeArgs(
keyTypeArg: XType,
valueTypeArg: XType,
keyReader: CursorValueReader?,
valueReader: CursorValueReader?,
mapInfo: MapInfo?,
logger: RLog
) {
if (!keyTypeArg.implementsEqualsAndHashcode()) {
logger.w(
Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
ProcessorErrors.classMustImplementEqualsAndHashCode(
keyTypeArg.typeName.toString()
)
)
}
val hasKeyColumnName = mapInfo?.keyColumnName?.isNotEmpty() ?: false
if (!hasKeyColumnName && keyReader != null) {
logger.e(
ProcessorErrors.keyMayNeedMapInfo(
keyTypeArg.typeName
)
)
}
val hasValueColumnName = mapInfo?.valueColumnName?.isNotEmpty() ?: false
if (!hasValueColumnName && valueReader != null) {
logger.e(
ProcessorErrors.valueMayNeedMapInfo(
valueTypeArg.typeName
)
)
}
}
}
/**
* Generates a code expression that verifies if all matched fields are null.
*/
fun getColumnNullCheckCode(
cursorVarName: String,
indexVars: List<ColumnIndexVar>
): CodeBlock {
val conditions = indexVars.map {
CodeBlock.of(
"$L.isNull($L)",
cursorVarName,
it.indexVar
)
}
return CodeBlock.join(conditions, "$W&&$W")
}
}