| /* |
| * 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() |
| } |
| } |