blob: 69fe5b6df4b82ddf735b1059875213011bc784ad [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.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomProcessor
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.ext.RoomTypeNames
import androidx.room.ext.S
import androidx.room.ext.T
import androidx.room.processor.ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
import com.google.common.truth.Truth
import com.google.testing.compile.CompileTester
import com.google.testing.compile.JavaFileObjects
import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import javax.lang.model.element.Modifier
import javax.tools.JavaFileObject
@RunWith(JUnit4::class)
class CustomTypeConverterResolutionTest {
fun TypeSpec.toJFO(): JavaFileObject {
return JavaFileObjects.forSourceString("foo.bar.${this.name}",
"package foo.bar;\n" + toString())
}
companion object {
val ENTITY = ClassName.get("foo.bar", "MyEntity")
val DB = ClassName.get("foo.bar", "MyDb")
val DAO = ClassName.get("foo.bar", "MyDao")
val CUSTOM_TYPE = ClassName.get("foo.bar", "CustomType")
val CUSTOM_TYPE_JFO = JavaFileObjects.forSourceLines(CUSTOM_TYPE.toString(),
"""
package ${CUSTOM_TYPE.packageName()};
public class ${CUSTOM_TYPE.simpleName()} {
public int value;
}
""")
val CUSTOM_TYPE_CONVERTER = ClassName.get("foo.bar", "MyConverter")
val CUSTOM_TYPE_CONVERTER_JFO = JavaFileObjects.forSourceLines(
CUSTOM_TYPE_CONVERTER.toString(),
"""
package ${CUSTOM_TYPE_CONVERTER.packageName()};
public class ${CUSTOM_TYPE_CONVERTER.simpleName()} {
@${TypeConverter::class.java.canonicalName}
public static $CUSTOM_TYPE toCustom(int value) {
return null;
}
@${TypeConverter::class.java.canonicalName}
public static int fromCustom($CUSTOM_TYPE input) {
return 0;
}
}
""")
val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
ClassName.get(Set::class.java), CUSTOM_TYPE)
val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
val CUSTOM_TYPE_SET_CONVERTER_JFO = JavaFileObjects.forSourceLines(
CUSTOM_TYPE_SET_CONVERTER.toString(),
"""
package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
import java.util.HashSet;
import java.util.Set;
public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
@${TypeConverter::class.java.canonicalName}
public static $CUSTOM_TYPE_SET toCustom(int value) {
return null;
}
@${TypeConverter::class.java.canonicalName}
public static int fromCustom($CUSTOM_TYPE_SET 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)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@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)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@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)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromDatabase_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasConverters = true, hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromDatabase_forReturnValue() {
val entity = createEntity(hasCustomField = true)
val database = createDatabase(hasConverters = true, hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromDao_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasDao = true)
val dao = createDao(hasConverters = true, hasQueryReturningEntity = true,
hasQueryWithCustomParam = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromEntity_forReturnValue() {
val entity = createEntity(hasCustomField = true, hasConverters = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromEntityField_forReturnValue() {
val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryReturningEntity = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromEntity_forQueryParameter() {
val entity = createEntity(hasCustomField = true, hasConverters = true)
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO())
.failsToCompile().withErrorContaining(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)
run(entity.toJFO(), dao.toJFO(), database.toJFO())
.failsToCompile().withErrorContaining(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)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
@Test
fun useFromQueryParameter_forQueryParameter() {
val entity = createEntity()
val database = createDatabase(hasDao = true)
val dao = createDao(hasQueryWithCustomParam = true, hasParameterConverters = true)
run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
}
fun run(vararg jfos: JavaFileObject): CompileTester {
return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
.that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO +
CUSTOM_TYPE_SET_CONVERTER_JFO)
.processedWith(RoomProcessor())
}
private fun createEntity(
hasCustomField: Boolean = false,
hasConverters: Boolean = false,
hasConverterOnField: Boolean = false,
useCollection: Boolean = false
): TypeSpec {
if (hasConverterOnField && hasConverters) {
throw IllegalArgumentException("cannot have both converters")
}
val type = if (useCollection) {
CUSTOM_TYPE_SET
} else {
CUSTOM_TYPE
}
return TypeSpec.classBuilder(ENTITY).apply {
addAnnotation(Entity::class.java)
addModifiers(Modifier.PUBLIC)
if (hasCustomField) {
addField(FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
if (hasConverterOnField) {
addAnnotation(createConvertersAnnotation())
}
}.build())
}
if (hasConverters) {
addAnnotation(createConvertersAnnotation())
}
addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
addAnnotation(PrimaryKey::class.java)
}.build())
}.build()
}
private fun createDatabase(
hasConverters: Boolean = false,
hasDao: Boolean = false,
useCollection: Boolean = false
): TypeSpec {
return TypeSpec.classBuilder(DB).apply {
addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
superclass(RoomTypeNames.ROOM_DB)
if (hasConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
addAnnotation(PrimaryKey::class.java)
}.build())
if (hasDao) {
addMethod(MethodSpec.methodBuilder("getDao").apply {
addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
returns(DAO)
}.build())
}
addAnnotation(
AnnotationSpec.builder(Database::class.java).apply {
addMember("entities", "{$T.class}", ENTITY)
addMember("version", "42")
}.build()
)
}.build()
}
private fun createDao(
hasConverters: Boolean = false,
hasQueryReturningEntity: Boolean = false,
hasQueryWithCustomParam: Boolean = false,
hasMethodConverters: Boolean = false,
hasParameterConverters: Boolean = false,
useCollection: Boolean = false
): TypeSpec {
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 TypeSpec.classBuilder(DAO).apply {
addAnnotation(Dao::class.java)
addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
if (hasConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
if (hasQueryReturningEntity) {
addMethod(MethodSpec.methodBuilder("loadAll").apply {
addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
addMember("value", S, "SELECT * FROM ${ENTITY.simpleName()} LIMIT 1")
}.build())
addModifiers(Modifier.ABSTRACT)
returns(ENTITY)
}.build())
}
val customType = if (useCollection) {
CUSTOM_TYPE_SET
} else {
CUSTOM_TYPE
}
if (hasQueryWithCustomParam) {
addMethod(MethodSpec.methodBuilder("queryWithCustom").apply {
addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
addMember("value", S, "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
" id = :custom")
}.build())
if (hasMethodConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
addParameter(ParameterSpec.builder(customType, "custom").apply {
if (hasParameterConverters) {
addAnnotation(createConvertersAnnotation(useCollection = useCollection))
}
}.build())
addModifiers(Modifier.ABSTRACT)
returns(TypeName.INT)
}.build())
}
}.build()
}
private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
val converter = if (useCollection) {
CUSTOM_TYPE_SET_CONVERTER
} else {
CUSTOM_TYPE_CONVERTER
}
return AnnotationSpec.builder(TypeConverters::class.java)
.addMember("value", "$T.class", converter).build()
}
}