Add support for arg constructors
This CL adds constructor support to Pojo and Entity processors.
We try to map each constructor parameter to a field based on
its name. If there are conflicts, we fail the compilation.
If there are multiple matching constructors, we fail the compilation
and developer can annotate unwanted ones with @Ignore to workaround
the issue.
This CL also prioritizes PojoRowAdapter over EntityRowAdapter since
it is faster and proguard does a good job on shrinking the generated
code.
Bug: 34611719
Bug: 36473441
Bug: 34754519
Test: ConstructorTest, PojoProcessorTest
Change-Id: I70592b129c7dcb9cacfb072b2c63169630eadf9a
diff --git a/room/common/src/main/java/com/android/support/room/Entity.java b/room/common/src/main/java/com/android/support/room/Entity.java
index 06d57f7..db9f566 100644
--- a/room/common/src/main/java/com/android/support/room/Entity.java
+++ b/room/common/src/main/java/com/android/support/room/Entity.java
@@ -24,9 +24,14 @@
/**
* Marks a class as an entity. This class will have a mapping SQLite table in the database.
* <p>
- * Each entity must have at least 1 field annotated with {@link PrimaryKey} and it must have a
- * no-arg constructor. You can also use {@link #primaryKeys()} attribute to define the primary
- * key.
+ * Each entity must have at least 1 field annotated with {@link PrimaryKey}.
+ * You can also use {@link #primaryKeys()} attribute to define the primary key.
+ * <p>
+ * Each entity must either have a no-arg constructor or a constructor whose parameters match
+ * fields (based on type and name). Constructor does not have to receive all fields as parameters
+ * but if a field is not passed into the constructor, it should either be public or have a public
+ * setter. If a matching constructor is available, Room will always use it. If you don't want it
+ * to use a constructor, you can annotate it with {@link Ignore}.
* <p>
* When a class is marked as an Entity, all of its fields are persisted. If you would like to
* exclude some of its fields, you can mark them with {@link Ignore}.
@@ -36,12 +41,20 @@
* {@literal @}Entity
* public class User {
* {@literal @}PrimaryKey
- * private int uid;
+ * private final int uid;
* private String name;
* {@literal @}ColumnInfo(name = "last_name")
* private String lastName;
- * // getters and setters are ignored for brevity but they are required for Room to work or the
- * // fields should be public.
+ *
+ * public User(int uid) {
+ * this.uid = uid;
+ * }
+ * public String getLastName() {
+ * return lastName;
+ * }
+ * public void setLastName(String lastName) {
+ * this.lastName = lastName;
+ * }
* }
* </pre>
*
diff --git a/room/common/src/main/java/com/android/support/room/Ignore.java b/room/common/src/main/java/com/android/support/room/Ignore.java
index c8f8f9d..36888a0 100644
--- a/room/common/src/main/java/com/android/support/room/Ignore.java
+++ b/room/common/src/main/java/com/android/support/room/Ignore.java
@@ -27,7 +27,7 @@
* This annotation can be used in multiple places where Room processor runs. For instance, you can
* add it to a field of an {@link Entity} and Room will not persist that field.
*/
-@Target({ElementType.METHOD, ElementType.FIELD})
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Ignore {
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
index a7f8cfa..a1c86e9 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
@@ -105,3 +105,13 @@
object RoomRxJava2TypeNames {
val RX_ROOM = ClassName.get("com.android.support.room", "RxRoom")
}
+
+fun TypeName.defaultValue() : String {
+ return if (!isPrimitive) {
+ "null"
+ } else if (this == TypeName.BOOLEAN) {
+ "false"
+ } else {
+ "0"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
index 279fb62..2c7ab96 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
@@ -130,7 +130,8 @@
decomposedFields = pojo.decomposedFields,
indices = indices,
primaryKey = primaryKey,
- foreignKeys = entityForeignKeys)
+ foreignKeys = entityForeignKeys,
+ constructor = pojo.constructor)
return entity
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt
index 96520d0..85e3432 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/PojoProcessor.kt
@@ -34,6 +34,7 @@
import com.android.support.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
import com.android.support.room.processor.cache.Cache
import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Constructor
import com.android.support.room.vo.Field
import com.android.support.room.vo.FieldGetter
import com.android.support.room.vo.DecomposedField
@@ -56,6 +57,7 @@
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.TypeKind
import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.ElementFilter
/**
* Processes any class as if it is a Pojo.
@@ -148,28 +150,124 @@
val setterCandidates = methods.filter {
it.parameters.size == 1 && it.returnType.kind == TypeKind.VOID
}
+ // don't try to find a constructor for binding to statement.
+ val constructor = if (bindingScope == FieldProcessor.BindingScope.BIND_TO_STMT) {
+ // we don't need to construct this POJO.
+ null
+ } else {
+ chooseConstructor(myFields, decomposedFields)
+ }
assignGetters(myFields, getterCandidates)
- assignSetters(myFields, setterCandidates)
+ assignSetters(myFields, setterCandidates, constructor)
decomposedFields.forEach {
assignGetter(it.field, getterCandidates)
- assignSetter(it.field, setterCandidates)
+ assignSetter(it.field, setterCandidates, constructor)
}
myRelationsList.forEach {
assignGetter(it.field, getterCandidates)
- assignSetter(it.field, setterCandidates)
+ assignSetter(it.field, setterCandidates, constructor)
}
val pojo = Pojo(element = element,
type = declaredType,
fields = fields,
decomposedFields = decomposedFields,
- relations = relations)
+ relations = relations,
+ constructor = constructor)
return pojo
}
+ private fun chooseConstructor(myFields: List<Field>, decomposed : List<DecomposedField>)
+ : Constructor? {
+ val constructors = ElementFilter.constructorsIn(element.enclosedElements)
+ .filterNot { it.hasAnnotation(Ignore::class) || it.hasAnyOf(PRIVATE) }
+ val fieldMap = myFields.associateBy { it.name }
+ val decomposedMap = decomposed.associateBy { it.field.name }
+ val typeUtils = context.processingEnv.typeUtils
+ val failedConstructors = mutableMapOf<ExecutableElement, List<Constructor.Param?>>()
+ val goodConstructors = constructors.map { constructor ->
+ val params = constructor.parameters.map param@ { param ->
+ val paramName = param.simpleName.toString()
+ val paramType = param.asType()
+
+ val matches = fun(field: Field?): Boolean {
+ return if (field == null) {
+ false
+ } else if (!field.nameWithVariations.contains(paramName)) {
+ false
+ } else {
+ typeUtils.isAssignable(paramType, field.type)
+ }
+ }
+
+ val exactFieldMatch = fieldMap[paramName]
+
+ if (matches(exactFieldMatch)) {
+ return@param Constructor.FieldParam(exactFieldMatch!!)
+ }
+ val exactDecomposedMatch = decomposedMap[paramName]
+ if (matches(exactDecomposedMatch?.field)) {
+ return@param Constructor.DecomposedParam(exactDecomposedMatch!!)
+ }
+
+ val matchingFields = myFields.filter {
+ matches(it)
+ }
+ val decomposedMatches = decomposed.filter {
+ matches(it.field)
+ }
+ if (matchingFields.isEmpty() && decomposedMatches.isEmpty()) {
+ null
+ } else if (matchingFields.size + decomposedMatches.size == 1) {
+ if (matchingFields.isNotEmpty()) {
+ Constructor.FieldParam(matchingFields.first())
+ } else {
+ Constructor.DecomposedParam(decomposedMatches.first())
+ }
+ } else {
+ context.logger.e(param, ProcessorErrors.ambigiousConstructor(
+ pojo = element.qualifiedName.toString(),
+ paramName = param.simpleName.toString(),
+ matchingFields = matchingFields.map { it.getPath() }
+ + decomposed.map { it.field.getPath() }
+ ))
+ null
+ }
+ }
+ if (params.any { it == null }) {
+ failedConstructors.put(constructor, params)
+ null
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ Constructor(constructor, params as List<Constructor.Param>)
+ }
+ }.filterNotNull()
+ if (goodConstructors.isEmpty()) {
+ if (failedConstructors.isNotEmpty()) {
+ val failureMsg = failedConstructors.entries.joinToString("\n") { entry ->
+ val paramsMatching = entry.key.parameters.withIndex().joinToString(", ") {
+ "${it.value.simpleName} : ${entry.value[it.index]?.log()}"
+ }
+ "${entry.key} : [$paramsMatching]"
+ }
+ context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR +
+ "\nTried the following constructors but they failed to match:\n$failureMsg")
+ }
+ context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ return null
+ }
+ if (goodConstructors.size > 1) {
+ goodConstructors.forEach {
+ context.logger.e(it.element, ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
+ }
+ return null
+ }
+ return goodConstructors.first()
+ }
+
private fun processDecomposedField(declaredType: DeclaredType?, it: Element): DecomposedField {
val fieldPrefix = it.getAnnotationValue(Decompose::class.java, "prefix")
?.toString() ?: ""
@@ -323,13 +421,19 @@
context.checker.check(success, field.element, CANNOT_FIND_GETTER_FOR_FIELD)
}
- private fun assignSetters(fields: List<Field>, setterCandidates: List<ExecutableElement>) {
+ private fun assignSetters(fields: List<Field>, setterCandidates: List<ExecutableElement>,
+ constructor : Constructor?) {
fields.forEach { field ->
- assignSetter(field, setterCandidates)
+ assignSetter(field, setterCandidates, constructor)
}
}
- private fun assignSetter(field: Field, setterCandidates: List<ExecutableElement>) {
+ private fun assignSetter(field: Field, setterCandidates: List<ExecutableElement>,
+ constructor: Constructor?) {
+ if (constructor != null && constructor.hasField(field)) {
+ field.setter = FieldSetter(field.name, field.type, CallType.CONSTRUCTOR)
+ return
+ }
val success = chooseAssignment(field = field,
candidates = setterCandidates,
nameVariations = field.setterNameWithVariations,
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 6f8bec6..7917e68 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -424,4 +424,24 @@
}
val MISSING_ROOM_RXJAVA2_ARTIFACT = "To use RxJava2 features, you must add `rxjava2`" +
" artifact from Room as a dependency. com.android.support.room:rxjava2:<version>"
+
+ fun ambigiousConstructor(pojo : String, paramName:String, matchingFields : List<String>)
+ : String {
+ return """
+ Ambiguous constructor. The parameter ($paramName) in $pojo matches multiple fields:
+ [${matchingFields.joinToString(",")}]. If you don't want to use this constructor,
+ you can annotate it with @Ignore. If you want Room to use this constructor, you can
+ rename the parameters to exactly match the field name to fix the ambiguity.
+ """.trim()
+ }
+
+ val MISSING_POJO_CONSTRUCTOR = """
+ Entities and Pojos must have a usable public constructor. You can have an empty
+ constructor or a constructor whose parameters match the fields (by name and type).
+ """.trim()
+
+ val TOO_MANY_POJO_CONSTRUCTORS = """
+ Room cannot pick a constructor since multiple constructors are suitable. Try to annotate
+ unwanted constructors with @Ignore.
+ """.trim()
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
index 9183923..1f7b4ba 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
@@ -354,14 +354,6 @@
// TODO one day support this
return null
}
- val asElement = MoreTypes.asElement(typeMirror)
- if (asElement.hasAnnotation(Entity::class)) {
- // TODO we might parse this too much, would be nice to scope these parsers
- // at least for entities.
- return EntityRowAdapter(EntityProcessor(context,
- MoreElements.asType(asElement)).process())
- }
-
val resultInfo = query.resultInfo
val (rowAdapter, rowAdapterLogs) = if (resultInfo != null && query.errors.isEmpty()
@@ -384,6 +376,15 @@
Pair(null, null)
}
+ if (rowAdapter == null && query.resultInfo == null) {
+ // we don't know what query returns. Check for entity.
+ val asElement = MoreTypes.asElement(typeMirror)
+ if (asElement.hasAnnotation(Entity::class)) {
+ return EntityRowAdapter(EntityProcessor(context,
+ MoreElements.asType(asElement)).process())
+ }
+ }
+
if (rowAdapter != null && !(rowAdapterLogs?.hasErrors() ?: false)) {
rowAdapterLogs?.writeTo(context.processingEnv)
return rowAdapter
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt
index 254fc49..424d8b2 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/PojoRowAdapter.kt
@@ -101,15 +101,18 @@
val indexVar = scope.getTmpVar("_cursorIndexOf${it.name.stripNonJava().capitalize()}")
scope.builder().addStatement("final $T $L = $L.getColumnIndexOrThrow($S)",
TypeName.INT, indexVar, cursorVarName, it.columnName)
- FieldWithIndex(it, indexVar)
+ FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = true)
}
}
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder().apply {
- addStatement("$L = new $T()", outVarName, out.typeName())
- FieldReadWriteWriter.readFromCursor(outVarName, cursorVarName,
- mapping.fieldsWithIndices, scope)
+ FieldReadWriteWriter.readFromCursor(
+ outVar = outVarName,
+ outPojo = pojo,
+ cursorVar = cursorVarName,
+ fieldsWithIndices = mapping.fieldsWithIndices,
+ scope = scope)
relationCollectors.forEach {
it.writeReadParentKeyCode(
cursorVarName = cursorVarName,
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/CallType.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/CallType.kt
index aa30cb1..4e1f473 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/CallType.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/CallType.kt
@@ -18,5 +18,6 @@
enum class CallType {
FIELD,
- METHOD
+ METHOD,
+ CONSTRUCTOR
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Constructor.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Constructor.kt
new file mode 100644
index 0000000..b356837
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Constructor.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.support.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * For each Entity / Pojo we process has a constructor. It might be the empty constructor or a
+ * constructor with fields.
+ */
+data class Constructor(val element : ExecutableElement, val params : List<Param>) {
+
+ fun hasField(field : Field) : Boolean {
+ return params.any {
+ when (it) {
+ is FieldParam -> it.field === field
+ is DecomposedParam -> it.decomposed.field === field
+ else -> false
+ }
+ }
+ }
+
+ class FieldParam(val field : Field) : Param(ParamType.FIELD) {
+ override fun log(): String = field.getPath()
+
+ }
+
+ class DecomposedParam(val decomposed: DecomposedField) : Param(ParamType.DECOMPOSED) {
+ override fun log(): String = decomposed.field.getPath()
+
+ }
+
+ abstract class Param(val type : ParamType) {
+ abstract fun log() : String;
+ }
+
+ enum class ParamType {
+ FIELD,
+ DECOMPOSED
+ }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
index a9fa995..db70dcb 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
@@ -25,8 +25,9 @@
class Entity(element: TypeElement, val tableName: String, type: DeclaredType,
fields: List<Field>, decomposedFields: List<DecomposedField>,
val primaryKey: PrimaryKey, val indices: List<Index>,
- val foreignKeys: List<ForeignKey>)
- : Pojo(element, type, fields, decomposedFields, emptyList()) {
+ val foreignKeys: List<ForeignKey>,
+ constructor: Constructor?)
+ : Pojo(element, type, fields, decomposedFields, emptyList(), constructor) {
val createTableQuery by lazy {
createTableQuery(tableName)
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldGetter.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldGetter.kt
index 9b9f7a3..d6fe529 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldGetter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldGetter.kt
@@ -27,7 +27,10 @@
val stmt = when (callType) {
CallType.FIELD -> "final $T $L = $L.$L"
CallType.METHOD -> "final $T $L = $L.$L()"
+ CallType.CONSTRUCTOR -> null
}
- builder.addStatement(stmt, TypeName.get(type), outVar, ownerVar, name)
+ stmt?.let {
+ builder.addStatement(stmt, TypeName.get(type), outVar, ownerVar, name)
+ }
}
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldSetter.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldSetter.kt
index eb5d736..51ac9d5 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldSetter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldSetter.kt
@@ -25,7 +25,10 @@
val stmt = when (callType) {
CallType.FIELD -> "$L.$L = $L"
CallType.METHOD -> "$L.$L($L)"
+ CallType.CONSTRUCTOR -> null
}
- builder.addStatement(stmt, ownerVar, name, inVar)
+ stmt?.let {
+ builder.addStatement(stmt, ownerVar, name, inVar)
+ }
}
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldWithIndex.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldWithIndex.kt
index 13a643e..4cca0dd 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldWithIndex.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/FieldWithIndex.kt
@@ -19,5 +19,18 @@
/**
* A common value object when we need to associate a Field with an Index
* variable.
+ * <p>
+ * If we are sure that the field will be there at compile time, we set it to always Exists so that
+ * the generated code does not check for -1 column indices.
*/
-data class FieldWithIndex(val field : Field, val indexVar : String)
+data class FieldWithIndex(val field : Field, val indexVar : String, val alwaysExists : Boolean) {
+ companion object {
+ fun byOrder(fields : List<Field>) : List<FieldWithIndex> {
+ return fields.mapIndexed { index, field ->
+ FieldWithIndex(field = field,
+ indexVar = "${index + 1}",
+ alwaysExists = true)
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt
index 4516457..d79e141 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Pojo.kt
@@ -27,6 +27,7 @@
*/
// TODO make data class when move to kotlin 1.1
open class Pojo(val element : TypeElement, val type: DeclaredType, val fields : List<Field>,
- val decomposedFields : List<DecomposedField>, val relations: List<Relation>) {
+ val decomposedFields : List<DecomposedField>, val relations: List<Relation>,
+ val constructor : Constructor? = null) {
val typeName: TypeName by lazy { type.typeName() }
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityCursorConverterWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityCursorConverterWriter.kt
index a1b8c68..c9f95af 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityCursorConverterWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityCursorConverterWriter.kt
@@ -60,109 +60,27 @@
private fun buildConvertMethodBody(writer: ClassWriter, cursorParam: ParameterSpec)
: CodeBlock {
- // TODO support arg constructor
val scope = CodeGenScope(writer)
val entityVar = scope.getTmpVar("_entity")
scope.builder().apply {
- addStatement("$T $L = new $T()", entity.typeName, entityVar, entity.typeName)
- val allParents = FieldReadWriteWriter.getAllParents(entity.fields)
- val sortedParents = allParents
- .sortedBy {
- depth(it)
- }
- .associate {
- Pair(it, scope.getTmpVar("_tmp${it.field.name}"))
- }
- // for each field parent, create a not null var so that we can set it at the end
- val parentNotNullVars = declareParents(sortedParents, scope)
-
- val colNameVar = scope.getTmpVar("_columnName")
- val colIndexVar = scope.getTmpVar("_columnIndex")
- addStatement("$T $L = 0", TypeName.INT, colIndexVar)
- beginControlFlow("for ($T $L : $N.getColumnNames())",
- TypeName.get(String::class.java), colNameVar, cursorParam).apply {
- beginControlFlow("switch($L.hashCode())", colNameVar).apply {
- entity.fields.groupBy { it.columnName.hashCode() }.forEach {
- val hash = it.key
- beginControlFlow("case $L:", hash).apply {
- val fields = it.value
- fields.forEach { field ->
- val subOwner = field.parent?.let {
- sortedParents[it]
- } ?: entityVar
- beginControlFlow("if ($S.equals($L))", field.columnName, colNameVar)
- val notNullVar = field.parent?.let {
- parentNotNullVars[it]
- }
- if (notNullVar != null) {
- beginControlFlow("if (!cursor.isNull($L))", colIndexVar).apply {
- addStatement("$L = true", notNullVar)
- readField(field, cursorParam, colIndexVar, subOwner, scope)
- }
- endControlFlow()
- } else {
- readField(field, cursorParam, colIndexVar, subOwner, scope)
- }
- endControlFlow()
- }
- }
- endControlFlow()
- }
- }
- endControlFlow()
- addStatement("$L ++", colIndexVar)
+ scope.builder().addStatement("final $T $L", entity.typeName, entityVar)
+ val fieldsWithIndices = entity.fields.map {
+ val indexVar = scope.getTmpVar(
+ "_cursorIndexOf${it.name.stripNonJava().capitalize()}")
+ scope.builder().addStatement("final $T $L = $N.getColumnIndex($S)",
+ TypeName.INT, indexVar, cursorParam, it.columnName)
+ FieldWithIndex(field = it,
+ indexVar = indexVar,
+ alwaysExists = false)
}
- endControlFlow()
- // assign parents
- assignParents(entityVar, parentNotNullVars, sortedParents, scope)
+ FieldReadWriteWriter.readFromCursor(
+ outVar = entityVar,
+ outPojo = entity,
+ cursorVar = cursorParam.name,
+ fieldsWithIndices = fieldsWithIndices,
+ scope = scope)
addStatement("return $L", entityVar)
}
return scope.builder().build()
}
-
- private fun declareParents(parentVars: Map<DecomposedField, String>,
- scope: CodeGenScope): Map<DecomposedField, String> {
- val parentNotNullVars = hashMapOf<DecomposedField, String>()
- scope.builder().apply {
- parentVars.forEach {
- addStatement("final $T $L = new $T()", it.key.pojo.typeName,
- it.value, it.key.pojo.typeName)
- val notNullVar = scope.getTmpVar("_notNull${it.key.field.name}")
- parentNotNullVars[it.key] = notNullVar
- addStatement("$T $L = false", TypeName.BOOLEAN, notNullVar)
- }
- }
- return parentNotNullVars
- }
-
- private fun assignParents(entityVar: String, parentNotNullVars: Map<DecomposedField, String>,
- sortedParents: Map<DecomposedField, String>, scope: CodeGenScope) {
- scope.builder().apply {
- sortedParents.forEach {
- val parent = it.key
- val varName = it.value
- val allNotNullVars = parent.pojo.fields
- .map { parentNotNullVars[it.parent] }.distinct()
- val ifCheck = allNotNullVars.joinToString(" || ")
- beginControlFlow("if ($L)", ifCheck).apply {
- val grandParentVar = parent.parent?.let {
- sortedParents[it]
- } ?: entityVar
- parent.field.setter.writeSet(grandParentVar, varName, this)
- }
- endControlFlow()
- }
- }
- }
-
- private fun readField(field: Field, cursorParam: ParameterSpec,
- indexVar: String, ownerVar: String, scope: CodeGenScope) {
- scope.builder().apply {
- FieldReadWriteWriter(FieldWithIndex(field, indexVar)).readFromCursor(
- ownerVar = ownerVar,
- cursorVar = cursorParam.name,
- scope = scope
- )
- }
- }
}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityDeletionAdapterWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityDeletionAdapterWriter.kt
index 5b323ac..c7f8930 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityDeletionAdapterWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityDeletionAdapterWriter.kt
@@ -58,9 +58,7 @@
addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
returns(TypeName.VOID)
addModifiers(PUBLIC)
- val mapped = entity.primaryKey.fields.mapIndexed { index, field ->
- FieldWithIndex(field, "${index + 1}")
- }
+ val mapped = FieldWithIndex.byOrder(entity.primaryKey.fields)
FieldReadWriteWriter.bindToStatement(ownerVar = valueParam,
stmtParamVar = stmtParam,
fieldsWithIndices = mapped,
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityInsertionAdapterWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityInsertionAdapterWriter.kt
index bd031a4..ab2803f 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityInsertionAdapterWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityInsertionAdapterWriter.kt
@@ -82,9 +82,7 @@
addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
returns(TypeName.VOID)
addModifiers(PUBLIC)
- val mapped = entity.fields.mapIndexed { index, field ->
- FieldWithIndex(field, "${index + 1}")
- }
+ val mapped = FieldWithIndex.byOrder(entity.fields)
FieldReadWriteWriter.bindToStatement(
ownerVar = valueParam,
stmtParamVar = stmtParam,
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityUpdateAdapterWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityUpdateAdapterWriter.kt
index 957f2d3..f0b2e58 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityUpdateAdapterWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/EntityUpdateAdapterWriter.kt
@@ -60,9 +60,7 @@
addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
returns(TypeName.VOID)
addModifiers(PUBLIC)
- val mappedField = entity.fields.mapIndexed { index, field ->
- FieldWithIndex(field, "${index + 1}")
- }
+ val mappedField = FieldWithIndex.byOrder(entity.fields)
FieldReadWriteWriter.bindToStatement(
ownerVar = valueParam,
stmtParamVar = stmtParam,
@@ -71,7 +69,9 @@
)
val pkeyStart = entity.fields.size
val mappedPrimaryKeys = entity.primaryKey.fields.mapIndexed { index, field ->
- FieldWithIndex(field, "${pkeyStart + index + 1}")
+ FieldWithIndex(field = field,
+ indexVar = "${pkeyStart + index + 1}",
+ alwaysExists = true)
}
FieldReadWriteWriter.bindToStatement(
ownerVar = valueParam,
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/FieldReadWriteWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/FieldReadWriteWriter.kt
index e041e9f..2ae9d5f 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/FieldReadWriteWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/FieldReadWriteWriter.kt
@@ -18,12 +18,16 @@
import com.android.support.room.ext.L
import com.android.support.room.ext.T
+import com.android.support.room.ext.defaultValue
import com.android.support.room.ext.typeName
import com.android.support.room.solver.CodeGenScope
import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Constructor
import com.android.support.room.vo.Field
import com.android.support.room.vo.DecomposedField
import com.android.support.room.vo.FieldWithIndex
+import com.android.support.room.vo.Pojo
+import com.squareup.javapoet.TypeName
/**
* Handles writing a field into statement or reading it form statement.
@@ -31,6 +35,7 @@
class FieldReadWriteWriter(fieldWithIndex: FieldWithIndex) {
val field = fieldWithIndex.field
val indexVar = fieldWithIndex.indexVar
+ val alwaysExists = fieldWithIndex.alwaysExists
companion object {
/*
@@ -122,50 +127,132 @@
visitNode(createNodeTree(ownerVar, fieldsWithIndices, scope))
}
- fun readFromCursor(ownerVar: String, cursorVar: String,
+ /**
+ * Just constructs the given item, does NOT DECLARE. Declaration happens outside the
+ * reading statement since we may never read if the cursor does not have necessary
+ * columns.
+ */
+ private fun construct(outVar : String, constructor : Constructor?, typeName : TypeName,
+ localVariableNames : Map<String, FieldWithIndex>,
+ localDecomposeds : List<Node>, scope: CodeGenScope) {
+ if (constructor == null) {
+ // best hope code generation
+ scope.builder().apply {
+ addStatement("$L = new $T()", outVar, typeName)
+ }
+ return
+ }
+ val variableNames = constructor.params.map { param ->
+ when(param) {
+ is Constructor.FieldParam -> localVariableNames.entries.firstOrNull {
+ it.value.field === param.field
+ }?.key
+ is Constructor.DecomposedParam -> localDecomposeds.firstOrNull {
+ it.fieldParent === param.decomposed
+ }?.varName
+ else -> null
+ }
+ }
+ val args = variableNames.joinToString(",") { it ?: "null"}
+ scope.builder().apply {
+ addStatement("$L = new $T($L)", outVar, typeName, args)
+ }
+ }
+
+ /**
+ * Reads the row into the given variable. It does not declare it but constructs it.
+ */
+ fun readFromCursor(outVar: String,
+ outPojo : Pojo,
+ cursorVar: String,
fieldsWithIndices: List<FieldWithIndex>,
scope: CodeGenScope) {
fun visitNode(node: Node) {
val fieldParent = node.fieldParent
fun readNode() {
- if (fieldParent != null) {
- scope.builder()
- .addStatement("final $T $L = new $T()", fieldParent.pojo.typeName,
- node.varName, fieldParent.pojo.typeName)
+ // read constructor parameters into local fields
+ val constructorFields = node.directFields.filter {
+ it.field.setter.callType == CallType.CONSTRUCTOR
+ }.associateBy { fwi ->
+ FieldReadWriteWriter(fwi).readIntoTmpVar(cursorVar, scope)
}
- node.directFields.forEach {
- FieldReadWriteWriter(it).readFromCursor(node.varName, cursorVar, scope)
- }
+ // read decomposed fields
node.subNodes.forEach(::visitNode)
+ // construct the object
if (fieldParent != null) {
- fieldParent.setter.writeSet(
- ownerVar = node.parentNode!!.varName,
- inVar = node.varName,
- builder = scope.builder()
- )
+ construct(outVar = node.varName,
+ constructor = fieldParent.pojo.constructor,
+ typeName = fieldParent.field.typeName,
+ localDecomposeds = node.subNodes,
+ localVariableNames = constructorFields,
+ scope = scope)
+ } else {
+ construct(outVar = node.varName,
+ constructor = outPojo.constructor,
+ typeName = outPojo.typeName,
+ localDecomposeds = node.subNodes,
+ localVariableNames = constructorFields,
+ scope = scope)
+ }
+ // ready any field that was not part of the constructor
+ node.directFields.filterNot {
+ it.field.setter.callType == CallType.CONSTRUCTOR
+ }.forEach { fwi ->
+ FieldReadWriteWriter(fwi).readFromCursor(
+ ownerVar = node.varName,
+ cursorVar = cursorVar,
+ scope = scope)
+ }
+ // assign sub modes to fields if they were not part of the constructor.
+ node.subNodes.map {
+ val setter = it.fieldParent?.setter
+ if (setter != null && setter.callType != CallType.CONSTRUCTOR) {
+ Pair(it.varName, setter)
+ } else {
+ null
+ }
+ }.filterNotNull().forEach { pair ->
+ val varName = pair.first
+ val setter = pair.second
+ setter.writeSet(
+ ownerVar = node.varName,
+ inVar = varName,
+ builder = scope.builder())
}
}
if (fieldParent == null) {
// root element
+ // always declared by the caller so we don't declare this
readNode()
} else {
+ // always declare, we'll set below
+ scope.builder().addStatement("final $T $L", fieldParent.pojo.typeName,
+ node.varName)
if (fieldParent.nonNull) {
readNode()
} else {
val myDescendants = node.allFields()
val allNullCheck = myDescendants.joinToString(" && ") {
- "$cursorVar.isNull(${it.indexVar})"
+ if (it.alwaysExists) {
+ "$cursorVar.isNull(${it.indexVar})"
+ } else {
+ "( ${it.indexVar} == -1 || $cursorVar.isNull(${it.indexVar}))"
+ }
+
}
scope.builder().apply {
beginControlFlow("if (! ($L))", allNullCheck).apply {
readNode()
}
+ nextControlFlow(" else ").apply {
+ addStatement("$L = null", node.varName)
+ }
endControlFlow()
}
}
}
}
- visitNode(createNodeTree(ownerVar, fieldsWithIndices, scope))
+ visitNode(createNodeTree(outVar, fieldsWithIndices, scope))
}
}
@@ -192,23 +279,61 @@
* @param cursorVar The cursor variable
* @param scope The code generation scope
*/
- fun readFromCursor(ownerVar: String, cursorVar: String, scope: CodeGenScope) {
- field.cursorValueReader?.let { reader ->
- scope.builder().apply {
- when (field.setter.callType) {
- CallType.FIELD -> {
- reader.readFromCursor("$ownerVar.${field.getter.name}", cursorVar,
- indexVar, scope)
- }
- CallType.METHOD -> {
- val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
- addStatement("final $T $L", field.getter.type.typeName(), tmpField)
- reader.readFromCursor(tmpField, cursorVar, indexVar, scope)
- addStatement("$L.$L($L)", ownerVar, field.setter.name, tmpField)
+ private fun readFromCursor(ownerVar: String, cursorVar: String, scope: CodeGenScope) {
+ fun toRead() {
+ field.cursorValueReader?.let { reader ->
+ scope.builder().apply {
+ when (field.setter.callType) {
+ CallType.FIELD -> {
+ reader.readFromCursor("$ownerVar.${field.getter.name}", cursorVar,
+ indexVar, scope)
+ }
+ CallType.METHOD -> {
+ val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
+ addStatement("final $T $L", field.getter.type.typeName(), tmpField)
+ reader.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ addStatement("$L.$L($L)", ownerVar, field.setter.name, tmpField)
+ }
+ CallType.CONSTRUCTOR -> {
+ // no-op
+ }
}
}
}
}
+ if (alwaysExists) {
+ toRead()
+ } else {
+ scope.builder().apply {
+ beginControlFlow("if ($L != -1)", indexVar).apply {
+ toRead()
+ }
+ endControlFlow()
+ }
+ }
+ }
+
+ /**
+ * Reads the value into a temporary local variable.
+ */
+ fun readIntoTmpVar(cursorVar: String, scope: CodeGenScope) : String {
+ val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
+ val typeName = field.getter.type.typeName()
+ scope.builder().apply {
+ addStatement("final $T $L", typeName, tmpField)
+ if (alwaysExists) {
+ field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ } else {
+ beginControlFlow("if ($L == -1)", indexVar).apply {
+ addStatement("$L = $L", tmpField, typeName.defaultValue())
+ }
+ nextControlFlow("else").apply {
+ field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ }
+ endControlFlow()
+ }
+ }
+ return tmpField
}
/**
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 1b955cd..bbf05dd 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -54,9 +54,19 @@
_statement.bindLong(_argIndex, id);
final Cursor _cursor = __db.query(_statement);
try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
final User _result;
if(_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
} else {
_result = null;
}
@@ -85,9 +95,19 @@
}
final Cursor _cursor = __db.query(_statement);
try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
final User _result;
if(_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
} else {
_result = null;
}
@@ -115,10 +135,20 @@
}
final Cursor _cursor = __db.query(_statement);
try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
final List<User> _result = new ArrayList<User>(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item_1;
- _item_1 = __entityCursorConverter_fooBarUser(_cursor);
+ _item_1 = new User();
+ _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
+ _item_1.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _item_1.setLastName(_tmpLastName);
+ _item_1.age = _cursor.getInt(_cursorIndexOfAge);
_result.add(_item_1);
}
return _result;
@@ -241,9 +271,19 @@
}
final Cursor _cursor = __db.query(_statement);
try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
final User _result;
if(_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
} else {
_result = null;
}
@@ -291,10 +331,20 @@
}
final Cursor _cursor = __db.query(_statement);
try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
final List<User> _result = new ArrayList<User>(_cursor.getCount());
while(_cursor.moveToNext()) {
final User _item_1;
- _item_1 = __entityCursorConverter_fooBarUser(_cursor);
+ _item_1 = new User();
+ _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
+ _item_1.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _item_1.setLastName(_tmpLastName);
+ _item_1.age = _cursor.getInt(_cursorIndexOfAge);
_result.add(_item_1);
}
return _result;
@@ -309,37 +359,4 @@
}
}.getLiveData();
}
-
- private User __entityCursorConverter_fooBarUser(Cursor cursor) {
- User _entity = new User();
- int _columnIndex = 0;
- for (String _columnName : cursor.getColumnNames()) {
- switch(_columnName.hashCode()) {
- case 115792: {
- if ("uid".equals(_columnName)) {
- _entity.uid = cursor.getInt(_columnIndex);
- }
- }
- case 3373707: {
- if ("name".equals(_columnName)) {
- _entity.name = cursor.getString(_columnIndex);
- }
- }
- case -1459599807: {
- if ("lastName".equals(_columnName)) {
- final String _tmpLastName;
- _tmpLastName = cursor.getString(_columnIndex);
- _entity.setLastName(_tmpLastName);
- }
- }
- case 1358970165: {
- if ("ageColumn".equals(_columnName)) {
- _entity.age = cursor.getInt(_columnIndex);
- }
- }
- }
- _columnIndex ++;
- }
- return _entity;
- }
}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
index f1af084..3a3e952 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
@@ -539,8 +539,25 @@
fun cache_entity() {
singleDb("""
@Database(entities = {User.class}, version = 42)
+ @SkipQueryVerification
public abstract class MyDb extends RoomDatabase {
- public abstract UserDao userDao();
+ public abstract MyUserDao userDao();
+ @Dao
+ interface MyUserDao {
+ @Insert
+ public void insert(User... users);
+
+ @Query("SELECT * FROM user where uid = ?")
+ public User loadOne(int uid);
+
+ @TypeConverters(Converter.class)
+ @Query("SELECT * FROM user where uid = ?")
+ public User loadWithConverter(int uid);
+ }
+ public static class Converter {
+ @TypeConverter
+ public static java.util.Date foo(Long input) {return null;}
+ }
}
""", USER, USER_DAO){ db, invocation ->
val userDao = db.daoMethods.first().dao
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/PojoProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/PojoProcessorTest.kt
index dbde69a..cec2d36 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/PojoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/PojoProcessorTest.kt
@@ -25,6 +25,8 @@
import com.android.support.room.processor.ProcessorErrors.relationCannotFindEntityField
import com.android.support.room.processor.ProcessorErrors.relationCannotFindParentEntityField
import com.android.support.room.testing.TestInvocation
+import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Constructor
import com.android.support.room.vo.DecomposedField
import com.android.support.room.vo.Field
import com.android.support.room.vo.Pojo
@@ -34,6 +36,7 @@
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
@@ -452,6 +455,159 @@
}.compilesWithoutError()
}
+ @Test
+ fun constructor_empty() {
+ val pojo = """
+ public String mName;
+ """
+ singleRun(pojo) { pojo ->
+ assertThat(pojo.constructor, notNullValue())
+ assertThat(pojo.constructor?.params, `is`(emptyList<Constructor.Param>()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_twoFieldsExcatMatch() {
+ val pojo = """
+ public String mName;
+ public String _name;
+ public MyPojo(String mName) {
+ }
+ """
+ singleRun(pojo) { pojo ->
+ val param = pojo.constructor?.params?.first()
+ assertThat(param, instanceOf(Constructor.FieldParam::class.java))
+ assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_oneTypeMatches() {
+ val pojo = """
+ public String mName;
+ public int _name;
+ public MyPojo(String name) {
+ }
+ """
+ singleRun(pojo) { pojo ->
+ val param = pojo.constructor?.params?.first()
+ assertThat(param, instanceOf(Constructor.FieldParam::class.java))
+ assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_twoFields() {
+ val pojo = """
+ String mName;
+ String _name;
+ public MyPojo(String name) {
+ }
+ """
+ singleRun(pojo) { pojo ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.ambigiousConstructor(MY_POJO.toString(),
+ "name", listOf("mName", "_name"))
+ )
+ }
+
+ @Test
+ fun constructor_noMatchBadType() {
+ singleRun("""
+ int foo;
+ public MyPojo(String foo) {
+ }
+ """) { pojo ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_noMatch() {
+ singleRun("""
+ String mName;
+ String _name;
+ public MyPojo(String foo) {
+ }
+ """) { pojo ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_noMatchMultiArg() {
+ singleRun("""
+ String mName;
+ int bar;
+ public MyPojo(String foo, String name) {
+ }
+ """) { pojo ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_multipleMatching() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ public MyPojo(String name) {
+ }
+ public MyPojo(String name, String lastName) {
+ }
+ """) { pojo ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
+ }
+
+ @Test
+ fun constructor_multipleMatchingWithIgnored() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ @Ignore
+ public MyPojo(String name) {
+ }
+ public MyPojo(String name, String lastName) {
+ }
+ """) { pojo ->
+ assertThat(pojo.constructor, notNullValue())
+ assertThat(pojo.constructor?.params?.size, `is`(2))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ assertThat(pojo.fields.find { it.name == "mLastName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_dontTryForBindToScope() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ """) { pojo, invocation ->
+ val process2 = PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.BIND_TO_STMT,
+ parent = null).process()
+ assertThat(process2.constructor, nullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_bindForTwoWay() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ """) { pojo, invocation ->
+ val process2 = PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.TWO_WAY,
+ parent = null).process()
+ assertThat(process2.constructor, notNullValue())
+ }.compilesWithoutError()
+ }
+
fun singleRun(code: String, vararg jfos:JavaFileObject, handler: (Pojo) -> Unit)
: CompileTester {
return singleRun(code, *jfos) { pojo, invocation ->
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
index 8d08887..54ae061 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/verifier/DatabaseVerifierTest.kt
@@ -22,6 +22,7 @@
import com.android.support.room.processor.Context
import com.android.support.room.testing.TestInvocation
import com.android.support.room.vo.CallType
+import com.android.support.room.vo.Constructor
import com.android.support.room.vo.Database
import com.android.support.room.vo.Entity
import com.android.support.room.vo.Field
@@ -39,6 +40,7 @@
import simpleRun
import java.sql.Connection
import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.PrimitiveType
@@ -195,7 +197,8 @@
decomposedFields = emptyList(),
indices = emptyList(),
primaryKey = PrimaryKey(null, fields.take(1), false),
- foreignKeys = emptyList()
+ foreignKeys = emptyList(),
+ constructor = Constructor(mock(ExecutableElement::class.java), emptyList())
)
}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/EntityCursorConverterWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/EntityCursorConverterWriterTest.kt
index 393613f..3766328 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/EntityCursorConverterWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/EntityCursorConverterWriterTest.kt
@@ -32,7 +32,6 @@
val OUT_PREFIX = """
package foo.bar;
import android.database.Cursor;
- import java.lang.String;
public class MyContainerClass {
""".trimIndent()
const val OUT_SUFFIX = "}"
@@ -52,34 +51,25 @@
""",
"""
private MyEntity __entityCursorConverter_fooBarMyEntity(Cursor cursor) {
- MyEntity _entity = new MyEntity();
- int _columnIndex = 0;
- for (String _columnName : cursor.getColumnNames()) {
- switch(_columnName.hashCode()) {
- case ${"id".hashCode()}: {
- if ("id".equals(_columnName)) {
- final int _tmpId;
- _tmpId = cursor.getInt(_columnIndex);
- _entity.setId(_tmpId);
- }
- }
- case ${"name".hashCode()}: {
- if ("name".equals(_columnName)) {
- _entity.name = cursor.getString(_columnIndex);
- }
- }
- case ${"lastName".hashCode()}: {
- if ("lastName".equals(_columnName)) {
- _entity.lastName = cursor.getString(_columnIndex);
- }
- }
- case ${"age".hashCode()}: {
- if ("age".equals(_columnName)) {
- _entity.age = cursor.getInt(_columnIndex);
- }
- }
- }
- _columnIndex ++;
+ final MyEntity _entity;
+ final int _cursorIndexOfId = cursor.getColumnIndex("id");
+ final int _cursorIndexOfName = cursor.getColumnIndex("name");
+ final int _cursorIndexOfLastName = cursor.getColumnIndex("lastName");
+ final int _cursorIndexOfAge = cursor.getColumnIndex("age");
+ _entity = new MyEntity();
+ if (_cursorIndexOfId != -1) {
+ final int _tmpId;
+ _tmpId = cursor.getInt(_cursorIndexOfId);
+ _entity.setId(_tmpId);
+ }
+ if (_cursorIndexOfName != -1) {
+ _entity.name = cursor.getString(_cursorIndexOfName);
+ }
+ if (_cursorIndexOfLastName != -1) {
+ _entity.lastName = cursor.getString(_cursorIndexOfLastName);
+ }
+ if (_cursorIndexOfAge != -1) {
+ _entity.age = cursor.getInt(_cursorIndexOfAge);
}
return _entity;
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
index 29b97ce..f3cb500 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/TestDatabase.java
@@ -61,5 +61,4 @@
}
}
}
-
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ConstructorTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ConstructorTest.java
new file mode 100644
index 0000000..52b0184
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ConstructorTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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 com.android.support.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.support.room.Dao;
+import com.android.support.room.Database;
+import com.android.support.room.Decompose;
+import com.android.support.room.Entity;
+import com.android.support.room.Insert;
+import com.android.support.room.PrimaryKey;
+import com.android.support.room.Query;
+import com.android.support.room.Room;
+import com.android.support.room.RoomDatabase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SuppressWarnings("SqlNoDataSourceInspection")
+@SmallTest
+public class ConstructorTest {
+ @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class},
+ exportSchema = false)
+ abstract static class MyDb extends RoomDatabase {
+ abstract MyDao dao();
+ }
+
+ @Dao
+ interface MyDao {
+ @Insert
+ void insertFull(FullConstructor... full);
+
+ @Query("SELECT * FROM fc WHERE a = :a")
+ FullConstructor loadFull(int a);
+
+ @Insert
+ void insertPartial(PartialConstructor... partial);
+
+ @Query("SELECT * FROM pc WHERE a = :a")
+ PartialConstructor loadPartial(int a);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(tableName = "fc")
+ static class FullConstructor {
+ @PrimaryKey
+ public final int a;
+ public final int b;
+ @Decompose
+ public final MyDecomposed decomposed;
+
+ FullConstructor(int a, int b, MyDecomposed decomposed) {
+ this.a = a;
+ this.b = b;
+ this.decomposed = decomposed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FullConstructor that = (FullConstructor) o;
+
+ if (a != that.a) return false;
+ //noinspection SimplifiableIfStatement
+ if (b != that.b) return false;
+ return decomposed != null ? decomposed.equals(that.decomposed)
+ : that.decomposed == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = a;
+ result = 31 * result + b;
+ result = 31 * result + (decomposed != null ? decomposed.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(tableName = "pc")
+ static class PartialConstructor {
+ @PrimaryKey
+ public final int a;
+ public int b;
+ @Decompose
+ private MyDecomposed mDecomposed;
+
+ PartialConstructor(int a) {
+ this.a = a;
+ }
+
+ public MyDecomposed getDecomposed() {
+ return mDecomposed;
+ }
+
+ public void setDecomposed(MyDecomposed decomposed) {
+ mDecomposed = decomposed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PartialConstructor that = (PartialConstructor) o;
+
+ if (a != that.a) return false;
+ //noinspection SimplifiableIfStatement
+ if (b != that.b) return false;
+ return mDecomposed != null ? mDecomposed.equals(that.mDecomposed)
+ : that.mDecomposed == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = a;
+ result = 31 * result + b;
+ result = 31 * result + (mDecomposed != null ? mDecomposed.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class MyDecomposed {
+ public final String text;
+
+ MyDecomposed(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MyDecomposed that = (MyDecomposed) o;
+
+ return text != null ? text.equals(that.text) : that.text == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return text != null ? text.hashCode() : 0;
+ }
+ }
+
+ private MyDb mDb;
+ private MyDao mDao;
+
+ @Before
+ public void init() {
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), MyDb.class)
+ .build();
+ mDao = mDb.dao();
+ }
+
+ @Test
+ public void insertAndReadFullConstructor() {
+ FullConstructor inserted = new FullConstructor(1, 2, null);
+ mDao.insertFull(inserted);
+ final FullConstructor load = mDao.loadFull(1);
+ assertThat(load, is(inserted));
+ }
+
+ @Test
+ public void insertAndReadPartial() {
+ PartialConstructor item = new PartialConstructor(3);
+ item.b = 7;
+ mDao.insertPartial(item);
+ PartialConstructor load = mDao.loadPartial(3);
+ assertThat(load, is(item));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ForeignKeyTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ForeignKeyTest.java
index 40ba336..3f30d4b 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ForeignKeyTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/ForeignKeyTest.java
@@ -33,6 +33,7 @@
import com.android.support.room.Delete;
import com.android.support.room.Entity;
import com.android.support.room.ForeignKey;
+import com.android.support.room.Ignore;
import com.android.support.room.Index;
import com.android.support.room.Insert;
import com.android.support.room.PrimaryKey;
@@ -111,13 +112,11 @@
public String name;
public String lastName;
- A() {
- }
-
A(String name) {
this.name = name;
}
+ @Ignore
A(String name, String lastName) {
this.name = name;
this.lastName = lastName;
@@ -135,9 +134,6 @@
public int id;
public String aName;
- B() {
- }
-
B(String aName) {
this.aName = aName;
}
@@ -154,9 +150,6 @@
public int id;
public String aName;
- C() {
- }
-
C(String aName) {
this.aName = aName;
}
@@ -174,9 +167,6 @@
public int id;
public String aName;
- D() {
- }
-
D(String aName) {
this.aName = aName;
}
@@ -198,6 +188,7 @@
E() {
}
+ @Ignore
E(String aName, String aLastName) {
this.aName = aName;
this.aLastName = aLastName;
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java
index a9d93d1..6eed2c3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/vo/AvgWeightByAge.java
@@ -17,6 +17,7 @@
package com.android.support.room.integration.testapp.vo;
import com.android.support.room.ColumnInfo;
+import com.android.support.room.Ignore;
@SuppressWarnings("unused")
public class AvgWeightByAge {
@@ -29,6 +30,7 @@
public AvgWeightByAge() {
}
+ @Ignore
public AvgWeightByAge(int age, float weight) {
mAge = age;
mWeight = weight;