blob: f4ade0b5a9315327050aff43e5157480df91f04e [file] [log] [blame]
/*
* 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.
*/
@file:Suppress("HasPlatformType")
package androidx.room.solver
import androidx.room.DatabaseProcessingStep
import androidx.room.TypeConverter
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.VisibilityModifier
import androidx.room.compiler.codegen.XAnnotationSpec
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XFunSpec
import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.processing.util.CompilationResultSubject
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.RoomAnnotationTypeNames
import androidx.room.ext.RoomTypeNames.ROOM_DB
import androidx.room.processor.ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class CustomTypeConverterResolutionTest {
fun XTypeSpec.toSource(): Source {
return Source.java(
this.className.canonicalName,
"package foo.bar;\n" + toString()
)
}
companion object {
val ENTITY = XClassName.get("foo.bar", "MyEntity")
val DB = XClassName.get("foo.bar", "MyDb")
val DAO = XClassName.get("foo.bar", "MyDao")
val CUSTOM_TYPE = XClassName.get("foo.bar", "CustomType")
val CUSTOM_TYPE_JFO = Source.java(
CUSTOM_TYPE.canonicalName,
"""
package ${CUSTOM_TYPE.packageName};
public class ${CUSTOM_TYPE.simpleNames.first()} {
public int value;
}
"""
)
val CUSTOM_TYPE_CONVERTER = XClassName.get("foo.bar", "MyConverter")
val CUSTOM_TYPE_CONVERTER_JFO = Source.java(
CUSTOM_TYPE_CONVERTER.canonicalName,
"""
package ${CUSTOM_TYPE_CONVERTER.packageName};
public class ${CUSTOM_TYPE_CONVERTER.simpleNames.first()} {
@${TypeConverter::class.java.canonicalName}
public static ${CUSTOM_TYPE.canonicalName} toCustom(int value) {
return null;
}
@${TypeConverter::class.java.canonicalName}
public static int fromCustom(${CUSTOM_TYPE.canonicalName} input) {
return 0;
}
}
"""
)
val CUSTOM_TYPE_SET = CommonTypeNames.SET.parametrizedBy(CUSTOM_TYPE)
val CUSTOM_TYPE_SET_CONVERTER = XClassName.get("foo.bar", "MySetConverter")
val CUSTOM_TYPE_SET_CONVERTER_JFO = Source.java(
CUSTOM_TYPE_SET_CONVERTER.canonicalName,
"""
package ${CUSTOM_TYPE_SET_CONVERTER.packageName};
import java.util.HashSet;
import java.util.Set;
public class ${CUSTOM_TYPE_SET_CONVERTER.simpleNames.first()} {
@${TypeConverter::class.java.canonicalName}
public static ${CUSTOM_TYPE_SET.toString(CodeLanguage.JAVA)} toCustom(int value) {
return null;
}
@${TypeConverter::class.java.canonicalName}
public static int fromCustom(${CUSTOM_TYPE_SET.toString(CodeLanguage.JAVA)} input) {
return 0;
}
}
"""
)
}
@Test
fun useFromDatabase_forEntity() {
val entity = createEntity(hasCustomField = true)
val database = createDatabase(hasConverters = true, hasDao = true)
val dao = createDao(hasQueryReturningEntity = true, hasQueryWithCustomParam = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun collection_forEntity() {
val entity = createEntity(
hasCustomField = true,
useCollection = true
)
val database = createDatabase(
hasConverters = true,
hasDao = true,
useCollection = true
)
val dao = createDao(
hasQueryWithCustomParam = false,
useCollection = true
)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun collection_forDao() {
val entity = createEntity(
hasCustomField = true,
useCollection = true
)
val database = createDatabase(
hasConverters = true,
hasDao = true,
useCollection = true
)
val dao = createDao(
hasQueryWithCustomParam = true,
useCollection = true
)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromDatabase_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasConverters = true, hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromDatabase_forReturnValue() {
val entity = createEntity(hasCustomField = true)
val database = createDatabase(hasConverters = true, hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromDao_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasDao = true)
val dao = createDao(
hasConverters = true, hasQueryReturningEntity = true,
hasQueryWithCustomParam = true
)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromEntity_forReturnValue() {
val entity = createEntity(hasCustomField = true, hasConverters = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromEntityField_forReturnValue() {
val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromEntity_forQueryParameter() {
val entity = createEntity(hasCustomField = true, hasConverters = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
) {
it.hasErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
}
}
@Test
fun useFromEntityField_forQueryParameter() {
val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
) {
it.hasErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
}
}
@Test
fun useFromQueryMethod_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true, hasMethodConverters = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
@Test
fun useFromQueryParameter_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true, hasParameterConverters = true)
runTest(
sources = listOf(entity.toSource(), dao.toSource(), database.toSource())
)
}
private fun runTest(
sources: List<Source>,
onCompilationResult: (CompilationResultSubject) -> Unit = {
it.hasErrorCount(0)
}
) {
runProcessorTest(
sources = sources + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO +
CUSTOM_TYPE_SET_CONVERTER_JFO,
createProcessingSteps = {
listOf(DatabaseProcessingStep())
},
onCompilationResult = onCompilationResult
)
}
private fun createEntity(
hasCustomField: Boolean = false,
hasConverters: Boolean = false,
hasConverterOnField: Boolean = false,
useCollection: Boolean = false
): XTypeSpec {
if (hasConverterOnField && hasConverters) {
throw IllegalArgumentException("cannot have both converters")
}
val type = if (useCollection) {
CUSTOM_TYPE_SET
} else {
CUSTOM_TYPE
}
return XTypeSpec.classBuilder(CodeLanguage.JAVA, ENTITY).apply {
addAnnotation(
XAnnotationSpec.builder(CodeLanguage.JAVA, RoomAnnotationTypeNames.ENTITY).build()
)
setVisibility(VisibilityModifier.PUBLIC)
if (hasCustomField) {
addProperty(
XPropertySpec.builder(
CodeLanguage.JAVA,
"myCustomField",
type,
VisibilityModifier.PUBLIC,
isMutable = true
).apply {
if (hasConverterOnField) {
addAnnotation(createConvertersAnnotation())
}
}.build()
)
}
if (hasConverters) {
addAnnotation(createConvertersAnnotation())
}
addProperty(
XPropertySpec.builder(
language = CodeLanguage.JAVA,
name = "id",
typeName = XTypeName.PRIMITIVE_INT,
visibility = VisibilityModifier.PUBLIC,
isMutable = true
).addAnnotation(
XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.PRIMARY_KEY
).build()
).build()
)
}.build()
}
private fun createDatabase(
hasConverters: Boolean = false,
hasDao: Boolean = false,
useCollection: Boolean = false
): XTypeSpec {
return XTypeSpec.classBuilder(CodeLanguage.JAVA, DB, isOpen = true).apply {
addAbstractModifier()
setVisibility(VisibilityModifier.PUBLIC)
superclass(ROOM_DB)
if (hasConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
addProperty(
XPropertySpec.builder(
language = CodeLanguage.JAVA,
name = "id",
typeName = XTypeName.PRIMITIVE_INT,
visibility = VisibilityModifier.PUBLIC,
isMutable = true
).addAnnotation(
XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.PRIMARY_KEY
).build()
).build()
)
if (hasDao) {
addFunction(
XFunSpec.builder(
language = CodeLanguage.JAVA,
"getDao",
VisibilityModifier.PUBLIC
).apply {
addAbstractModifier()
returns(DAO)
}.build()
)
}
addAnnotation(
XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.DATABASE
).apply {
addMember(
"entities",
XCodeBlock.of(
language,
"{%T.class}",
ENTITY
)
)
addMember(
"version",
XCodeBlock.of(
language,
"42"
)
)
addMember(
"exportSchema",
XCodeBlock.of(
language,
"false"
)
)
}.build()
)
}.build()
}
private fun createDao(
hasConverters: Boolean = false,
hasQueryReturningEntity: Boolean = false,
hasQueryWithCustomParam: Boolean = false,
hasMethodConverters: Boolean = false,
hasParameterConverters: Boolean = false,
useCollection: Boolean = false
): XTypeSpec {
val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
.map { if (it) 1 else 0 }.sum()
if (annotationCount > 1) {
throw IllegalArgumentException("cannot set both of these")
}
if (hasParameterConverters && !hasQueryWithCustomParam) {
throw IllegalArgumentException("inconsistent")
}
return XTypeSpec.classBuilder(
CodeLanguage.JAVA,
DAO,
isOpen = true
).apply {
addAbstractModifier()
addAnnotation(XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.DAO
).build())
setVisibility(VisibilityModifier.PUBLIC)
if (hasConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
if (hasQueryReturningEntity) {
addFunction(
XFunSpec.builder(
CodeLanguage.JAVA,
"loadAll",
VisibilityModifier.PUBLIC
).apply {
addAbstractModifier()
addAnnotation(XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.QUERY
).addMember(
"value",
XCodeBlock.of(
CodeLanguage.JAVA,
"%S",
"SELECT * FROM ${ENTITY.simpleNames.first()} LIMIT 1"
)
).build())
returns(ENTITY)
}.build()
)
}
val customType = if (useCollection) {
CUSTOM_TYPE_SET
} else {
CUSTOM_TYPE
}
if (hasQueryWithCustomParam) {
addFunction(
XFunSpec.builder(
CodeLanguage.JAVA,
"queryWithCustom",
VisibilityModifier.PUBLIC
).apply {
addAbstractModifier()
addAnnotation(XAnnotationSpec.builder(
CodeLanguage.JAVA,
RoomAnnotationTypeNames.QUERY
).addMember(
"value",
XCodeBlock.of(
CodeLanguage.JAVA,
"%S",
"SELECT COUNT(*) FROM ${ENTITY.simpleNames.first()} where" +
" id = :custom"
)
).build())
if (hasMethodConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
addParameter(
customType,
"custom"
).apply {
if (hasParameterConverters) {
addAnnotation(
createConvertersAnnotation(useCollection = useCollection)
)
}
}.build()
returns(XTypeName.PRIMITIVE_INT)
}.build()
)
}
}.build()
}
private fun createConvertersAnnotation(useCollection: Boolean = false): XAnnotationSpec {
val converter = if (useCollection) {
CUSTOM_TYPE_SET_CONVERTER
} else {
CUSTOM_TYPE_CONVERTER
}
return XAnnotationSpec.builder(CodeLanguage.JAVA, RoomAnnotationTypeNames.TYPE_CONVERTERS)
.addMember("value", XCodeBlock.of(CodeLanguage.JAVA, "%T.class", converter)).build()
}
}