1 /*
<lambda>null2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.room.processor
18 
19 import androidx.room.compiler.processing.XType
20 import androidx.room.compiler.processing.XTypeElement
21 import androidx.room.ext.isNotError
22 import androidx.room.ext.isNotNone
23 import androidx.room.parser.SQLTypeAffinity
24 import androidx.room.parser.SqlParser
25 import androidx.room.processor.EntityProcessor.Companion.createIndexName
26 import androidx.room.processor.EntityProcessor.Companion.extractForeignKeys
27 import androidx.room.processor.EntityProcessor.Companion.extractIndices
28 import androidx.room.processor.EntityProcessor.Companion.extractTableName
29 import androidx.room.processor.ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
30 import androidx.room.processor.ProcessorErrors.INVALID_INDEX_ORDERS_SIZE
31 import androidx.room.processor.ProcessorErrors.RELATION_IN_ENTITY
32 import androidx.room.processor.cache.Cache
33 import androidx.room.vo.DataClass
34 import androidx.room.vo.EmbeddedProperty
35 import androidx.room.vo.Entity
36 import androidx.room.vo.ForeignKey
37 import androidx.room.vo.Index
38 import androidx.room.vo.PrimaryKey
39 import androidx.room.vo.Properties
40 import androidx.room.vo.Property
41 import androidx.room.vo.Warning
42 import androidx.room.vo.columnNames
43 import androidx.room.vo.findPropertyByColumnName
44 
45 class TableEntityProcessor
46 internal constructor(
47     baseContext: Context,
48     val element: XTypeElement,
49     private val referenceStack: LinkedHashSet<String> = LinkedHashSet()
50 ) : EntityProcessor {
51     val context = baseContext.fork(element)
52 
53     override fun process(): Entity {
54         return context.cache.entities.get(Cache.EntityKey(element)) { doProcess() }
55     }
56 
57     private fun doProcess(): Entity {
58         if (!element.validate()) {
59             context.reportMissingTypeReference(element.qualifiedName)
60             return Entity(
61                 element = element,
62                 tableName = element.name,
63                 type = element.type,
64                 properties = emptyList(),
65                 embeddedProperties = emptyList(),
66                 indices = emptyList(),
67                 primaryKey = PrimaryKey.MISSING,
68                 foreignKeys = emptyList(),
69                 constructor = null,
70                 shadowTableName = null
71             )
72         }
73         context.checker.hasAnnotation(
74             element,
75             androidx.room.Entity::class,
76             ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
77         )
78         val annotation = element.getAnnotation(androidx.room.Entity::class)
79         val tableName: String
80         val entityIndices: List<IndexInput>
81         val foreignKeyInputs: List<ForeignKeyInput>
82         val inheritSuperIndices: Boolean
83         if (annotation != null) {
84             tableName = extractTableName(element, annotation)
85             entityIndices = extractIndices(annotation, tableName)
86             inheritSuperIndices = annotation["inheritSuperIndices"]?.asBoolean() == true
87             foreignKeyInputs = extractForeignKeys(annotation)
88         } else {
89             tableName = element.name
90             foreignKeyInputs = emptyList()
91             entityIndices = emptyList()
92             inheritSuperIndices = false
93         }
94         context.checker.notBlank(
95             tableName,
96             element,
97             ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY
98         )
99         context.checker.check(
100             !tableName.startsWith("sqlite_", true),
101             element,
102             ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_START_WITH_SQLITE
103         )
104 
105         val pojo =
106             DataClassProcessor.createFor(
107                     context = context,
108                     element = element,
109                     bindingScope = PropertyProcessor.BindingScope.TWO_WAY,
110                     parent = null,
111                     referenceStack = referenceStack
112                 )
113                 .process()
114         context.checker.check(pojo.relations.isEmpty(), element, RELATION_IN_ENTITY)
115 
116         val propertyIndices =
117             pojo.properties
118                 .filter { it.indexed }
119                 .mapNotNull {
120                     if (it.parent != null) {
121                         it.indexed = false
122                         context.logger.w(
123                             Warning.INDEX_FROM_EMBEDDED_PROPERTY_IS_DROPPED,
124                             it.element,
125                             ProcessorErrors.droppedEmbeddedPropertyIndex(
126                                 it.getPath(),
127                                 element.qualifiedName
128                             )
129                         )
130                         null
131                     } else if (it.element.enclosingElement != element && !inheritSuperIndices) {
132                         it.indexed = false
133                         context.logger.w(
134                             Warning.INDEX_FROM_PARENT_PROPERTY_IS_DROPPED,
135                             ProcessorErrors.droppedSuperClassPropertyIndex(
136                                 it.columnName,
137                                 element.qualifiedName,
138                                 it.element.enclosingElement
139                                     .asClassName()
140                                     .toString(context.codeLanguage)
141                             )
142                         )
143                         null
144                     } else {
145                         IndexInput(
146                             name = createIndexName(listOf(it.columnName), tableName),
147                             unique = false,
148                             columnNames = listOf(it.columnName),
149                             orders = emptyList()
150                         )
151                     }
152                 }
153         val superIndices = loadSuperIndices(element.superClass, tableName, inheritSuperIndices)
154         val indexInputs = entityIndices + propertyIndices + superIndices
155         val indices = validateAndCreateIndices(indexInputs, pojo)
156 
157         val primaryKey = findAndValidatePrimaryKey(pojo.properties, pojo.embeddedProperties)
158         val affinity = primaryKey.properties.firstOrNull()?.affinity ?: SQLTypeAffinity.TEXT
159         context.checker.check(
160             !primaryKey.autoGenerateId || affinity == SQLTypeAffinity.INTEGER,
161             primaryKey.properties.firstOrNull()?.element ?: element,
162             ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT
163         )
164 
165         val entityForeignKeys = validateAndCreateForeignKeyReferences(foreignKeyInputs, pojo)
166         checkIndicesForForeignKeys(entityForeignKeys, primaryKey, indices)
167 
168         context.checker.check(
169             SqlParser.isValidIdentifier(tableName),
170             element,
171             ProcessorErrors.INVALID_TABLE_NAME
172         )
173         pojo.properties.forEach {
174             context.checker.check(
175                 SqlParser.isValidIdentifier(it.columnName),
176                 it.element,
177                 ProcessorErrors.INVALID_COLUMN_NAME
178             )
179         }
180 
181         val entity =
182             Entity(
183                 element = element,
184                 tableName = tableName,
185                 type = pojo.type,
186                 properties = pojo.properties,
187                 embeddedProperties = pojo.embeddedProperties,
188                 indices = indices,
189                 primaryKey = primaryKey,
190                 foreignKeys = entityForeignKeys,
191                 constructor = pojo.constructor,
192                 shadowTableName = null
193             )
194 
195         return entity
196     }
197 
198     private fun checkIndicesForForeignKeys(
199         entityForeignKeys: List<ForeignKey>,
200         primaryKey: PrimaryKey,
201         indices: List<Index>
202     ) {
203         fun covers(columnNames: List<String>, properties: List<Property>): Boolean =
204             properties.size >= columnNames.size &&
205                 columnNames.withIndex().all { properties[it.index].columnName == it.value }
206 
207         entityForeignKeys.forEach { fKey ->
208             val columnNames = fKey.childProperties.map { it.columnName }
209             val exists =
210                 covers(columnNames, primaryKey.properties) ||
211                     indices.any { index -> covers(columnNames, index.properties) }
212             if (!exists) {
213                 if (columnNames.size == 1) {
214                     context.logger.w(
215                         Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD,
216                         element,
217                         ProcessorErrors.foreignKeyMissingIndexInChildColumn(columnNames[0])
218                     )
219                 } else {
220                     context.logger.w(
221                         Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD,
222                         element,
223                         ProcessorErrors.foreignKeyMissingIndexInChildColumns(columnNames)
224                     )
225                 }
226             }
227         }
228     }
229 
230     /** Does a validation on foreign keys except the parent table's columns. */
231     private fun validateAndCreateForeignKeyReferences(
232         foreignKeyInputs: List<ForeignKeyInput>,
233         dataClass: DataClass
234     ): List<ForeignKey> {
235         return foreignKeyInputs
236             .map {
237                 if (it.onUpdate == null) {
238                     context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
239                     return@map null
240                 }
241                 if (it.onDelete == null) {
242                     context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
243                     return@map null
244                 }
245                 if (it.childColumns.isEmpty()) {
246                     context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
247                     return@map null
248                 }
249                 if (it.parentColumns.isEmpty()) {
250                     context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
251                     return@map null
252                 }
253                 if (it.childColumns.size != it.parentColumns.size) {
254                     context.logger.e(
255                         element,
256                         ProcessorErrors.foreignKeyColumnNumberMismatch(
257                             it.childColumns,
258                             it.parentColumns
259                         )
260                     )
261                     return@map null
262                 }
263                 val parentElement = it.parent.typeElement
264                 if (parentElement == null) {
265                     context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
266                     return@map null
267                 }
268                 val parentAnnotation = parentElement.getAnnotation(androidx.room.Entity::class)
269                 if (parentAnnotation == null) {
270                     context.logger.e(
271                         element,
272                         ProcessorErrors.foreignKeyNotAnEntity(parentElement.qualifiedName)
273                     )
274                     return@map null
275                 }
276                 val tableName = extractTableName(parentElement, parentAnnotation)
277                 val properties =
278                     it.childColumns.mapNotNull { columnName ->
279                         val property = dataClass.findPropertyByColumnName(columnName)
280                         if (property == null) {
281                             context.logger.e(
282                                 dataClass.element,
283                                 ProcessorErrors.foreignKeyChildColumnDoesNotExist(
284                                     columnName,
285                                     dataClass.columnNames
286                                 )
287                             )
288                         }
289                         property
290                     }
291                 if (properties.size != it.childColumns.size) {
292                     return@map null
293                 }
294                 ForeignKey(
295                     parentTable = tableName,
296                     childProperties = properties,
297                     parentColumns = it.parentColumns,
298                     onDelete = it.onDelete,
299                     onUpdate = it.onUpdate,
300                     deferred = it.deferred
301                 )
302             }
303             .filterNotNull()
304     }
305 
306     private fun findAndValidatePrimaryKey(
307         properties: List<Property>,
308         embeddedProperties: List<EmbeddedProperty>
309     ): PrimaryKey {
310         val candidates =
311             collectPrimaryKeysFromEntityAnnotations(element, properties) +
312                 collectPrimaryKeysFromPrimaryKeyAnnotations(properties) +
313                 collectPrimaryKeysFromEmbeddedProperties(embeddedProperties)
314 
315         context.checker.check(candidates.isNotEmpty(), element, ProcessorErrors.MISSING_PRIMARY_KEY)
316 
317         // 1. If a key is not autogenerated, but is Primary key or is part of Primary key we
318         // force the @NonNull annotation. If the key is a single Primary Key, Integer or Long, we
319         // don't force the @NonNull annotation since SQLite will automatically generate IDs.
320         // 2. If a key is autogenerate, we generate NOT NULL in table spec, but we don't require
321         // @NonNull annotation on the property itself.
322         val verifiedProperties =
323             mutableSetOf<Property>() // track verified properties to not over report
324         candidates
325             .filterNot { it.autoGenerateId }
326             .forEach { candidate ->
327                 candidate.properties.forEach { property ->
328                     if (
329                         candidate.properties.size > 1 ||
330                             (candidate.properties.size == 1 &&
331                                 property.affinity != SQLTypeAffinity.INTEGER)
332                     ) {
333                         if (!verifiedProperties.contains(property)) {
334                             context.checker.check(
335                                 property.nonNull,
336                                 property.element,
337                                 ProcessorErrors.primaryKeyNull(property.getPath())
338                             )
339                             verifiedProperties.add(property)
340                         }
341                         // Validate parents for nullability
342                         var parent = property.parent
343                         while (parent != null) {
344                             val parentProperty = parent.property
345                             if (!verifiedProperties.contains(parentProperty)) {
346                                 context.checker.check(
347                                     parentProperty.nonNull,
348                                     parentProperty.element,
349                                     ProcessorErrors.primaryKeyNull(parentProperty.getPath())
350                                 )
351                                 verifiedProperties.add(parentProperty)
352                             }
353                             parent = parentProperty.parent
354                         }
355                     }
356                 }
357             }
358 
359         if (candidates.size == 1) {
360             // easy :)
361             return candidates.first()
362         }
363 
364         return choosePrimaryKey(candidates, element)
365     }
366 
367     /** Check fields for @PrimaryKey. */
368     private fun collectPrimaryKeysFromPrimaryKeyAnnotations(
369         fields: List<Property>
370     ): List<PrimaryKey> {
371         return fields.mapNotNull { field ->
372             val primaryKeyAnnotation =
373                 field.element.getAnnotation(androidx.room.PrimaryKey::class)
374                     ?: return@mapNotNull null
375             if (field.parent != null) {
376                 // the field in the entity that contains this error.
377                 val grandParentField = field.parent.mRootParent.property.element
378                 // bound for entity.
379                 context
380                     .fork(grandParentField)
381                     .logger
382                     .w(
383                         Warning.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED,
384                         grandParentField,
385                         ProcessorErrors.embeddedPrimaryKeyIsDropped(
386                             element.qualifiedName,
387                             field.name
388                         )
389                     )
390                 null
391             } else {
392                 PrimaryKey(
393                     declaredIn = field.element.enclosingElement,
394                     properties = Properties(field),
395                     autoGenerateId = primaryKeyAnnotation["autoGenerate"]?.asBoolean() == true
396                 )
397             }
398         }
399     }
400 
401     /** Check classes for @Entity(primaryKeys = ?). */
402     private fun collectPrimaryKeysFromEntityAnnotations(
403         typeElement: XTypeElement,
404         availableProperties: List<Property>
405     ): List<PrimaryKey> {
406         val myPkeys =
407             typeElement.getAnnotation(androidx.room.Entity::class)?.let {
408                 val primaryKeyColumns = it["primaryKeys"]?.asStringList() ?: emptyList()
409                 if (primaryKeyColumns.isEmpty()) {
410                     emptyList()
411                 } else {
412                     val properties =
413                         primaryKeyColumns.mapNotNull { pKeyColumnName ->
414                             val property =
415                                 availableProperties.firstOrNull { it.columnName == pKeyColumnName }
416                             context.checker.check(
417                                 property != null,
418                                 typeElement,
419                                 ProcessorErrors.primaryKeyColumnDoesNotExist(
420                                     pKeyColumnName,
421                                     availableProperties.map { it.columnName }
422                                 )
423                             )
424                             property
425                         }
426                     listOf(
427                         PrimaryKey(
428                             declaredIn = typeElement,
429                             properties = Properties(properties),
430                             autoGenerateId = false
431                         )
432                     )
433                 }
434             } ?: emptyList()
435         // checks supers.
436         val mySuper = typeElement.superClass
437         val superPKeys =
438             if (mySuper != null && mySuper.isNotNone() && mySuper.isNotError()) {
439                 // my super cannot see my properties so remove them.
440                 val remainingProperties =
441                     availableProperties.filterNot { it.element.enclosingElement == typeElement }
442                 collectPrimaryKeysFromEntityAnnotations(mySuper.typeElement!!, remainingProperties)
443             } else {
444                 emptyList()
445             }
446         return superPKeys + myPkeys
447     }
448 
449     private fun collectPrimaryKeysFromEmbeddedProperties(
450         embeddedProperties: List<EmbeddedProperty>
451     ): List<PrimaryKey> {
452         return embeddedProperties.mapNotNull { embeddedProperty ->
453             embeddedProperty.property.element.getAnnotation(androidx.room.PrimaryKey::class)?.let {
454                 val autoGenerate = it["autoGenerate"]?.asBoolean() == true
455                 context.checker.check(
456                     !autoGenerate || embeddedProperty.dataClass.properties.size == 1,
457                     embeddedProperty.property.element,
458                     ProcessorErrors.AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_PROPERTIES
459                 )
460                 PrimaryKey(
461                     declaredIn = embeddedProperty.property.element.enclosingElement,
462                     properties = embeddedProperty.dataClass.properties,
463                     autoGenerateId = autoGenerate
464                 )
465             }
466         }
467     }
468 
469     // start from my element and check if anywhere in the list we can find the only well defined
470     // pkey, if so, use it.
471     private fun choosePrimaryKey(
472         candidates: List<PrimaryKey>,
473         typeElement: XTypeElement
474     ): PrimaryKey {
475         // If 1 of these primary keys is declared in this class, then it is the winner. Just print
476         //    a note for the others.
477         // If 0 is declared, check the parent.
478         // If more than 1 primary key is declared in this class, it is an error.
479         val myPKeys = candidates.filter { candidate -> candidate.declaredIn == typeElement }
480         return if (myPKeys.size == 1) {
481             // just note, this is not worth an error or warning
482             (candidates - myPKeys).forEach {
483                 context.logger.d(
484                     element,
485                     "${it.toHumanReadableString()} is" +
486                         " overridden by ${myPKeys.first().toHumanReadableString()}"
487                 )
488             }
489             myPKeys.first()
490         } else if (myPKeys.isEmpty()) {
491             // i have not declared anything, delegate to super
492             val mySuper = typeElement.superClass
493             if (mySuper != null && mySuper.isNotNone() && mySuper.isNotError()) {
494                 return choosePrimaryKey(candidates, mySuper.typeElement!!)
495             }
496             PrimaryKey.MISSING
497         } else {
498             context.logger.e(
499                 element,
500                 ProcessorErrors.multiplePrimaryKeyAnnotations(
501                     myPKeys.map(PrimaryKey::toHumanReadableString)
502                 )
503             )
504             PrimaryKey.MISSING
505         }
506     }
507 
508     private fun validateAndCreateIndices(
509         inputs: List<IndexInput>,
510         dataClass: DataClass
511     ): List<Index> {
512         // check for columns
513         val indices =
514             inputs.mapNotNull { input ->
515                 context.checker.check(
516                     input.columnNames.isNotEmpty(),
517                     element,
518                     INDEX_COLUMNS_CANNOT_BE_EMPTY
519                 )
520                 val properties =
521                     input.columnNames.mapNotNull { columnName ->
522                         val property = dataClass.findPropertyByColumnName(columnName)
523                         context.checker.check(
524                             property != null,
525                             element,
526                             ProcessorErrors.indexColumnDoesNotExist(
527                                 columnName,
528                                 dataClass.columnNames
529                             )
530                         )
531                         property
532                     }
533                 if (input.orders.isNotEmpty()) {
534                     context.checker.check(
535                         input.columnNames.size == input.orders.size,
536                         element,
537                         INVALID_INDEX_ORDERS_SIZE
538                     )
539                 }
540                 if (properties.isEmpty()) {
541                     null
542                 } else {
543                     Index(
544                         name = input.name,
545                         unique = input.unique,
546                         properties = Properties(properties),
547                         orders = input.orders
548                     )
549                 }
550             }
551 
552         // check for duplicate indices
553         indices
554             .groupBy { it.name }
555             .filter { it.value.size > 1 }
556             .forEach { context.logger.e(element, ProcessorErrors.duplicateIndexInEntity(it.key)) }
557 
558         // see if any embedded property is an entity with indices, if so, report a warning
559         dataClass.embeddedProperties.forEach { embedded ->
560             val embeddedElement = embedded.dataClass.element
561             embeddedElement.getAnnotation(androidx.room.Entity::class)?.let {
562                 val subIndices = extractIndices(it, "")
563                 if (subIndices.isNotEmpty()) {
564                     context.logger.w(
565                         Warning.INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED,
566                         embedded.property.element,
567                         ProcessorErrors.droppedEmbeddedIndex(
568                             entityName = embedded.dataClass.typeName.toString(context.codeLanguage),
569                             propertyPath = embedded.property.getPath(),
570                             grandParent = element.qualifiedName
571                         )
572                     )
573                 }
574             }
575         }
576         return indices
577     }
578 
579     // check if parent is an Entity, if so, report its annotation indices
580     private fun loadSuperIndices(
581         typeMirror: XType?,
582         tableName: String,
583         inherit: Boolean
584     ): List<IndexInput> {
585         if (typeMirror == null || typeMirror.isNone() || typeMirror.isError()) {
586             return emptyList()
587         }
588         val parentTypeElement = typeMirror.typeElement
589         @Suppress("FoldInitializerAndIfToElvis")
590         if (parentTypeElement == null) {
591             // this is coming from a parent, shouldn't happen so no reason to report an error
592             return emptyList()
593         }
594         val myIndices =
595             parentTypeElement.getAnnotation(androidx.room.Entity::class)?.let { annotation ->
596                 val indices = extractIndices(annotation, tableName = "super")
597                 if (indices.isEmpty()) {
598                     emptyList()
599                 } else if (inherit) {
600                     // rename them
601                     indices.map {
602                         IndexInput(
603                             name = createIndexName(it.columnNames, tableName),
604                             unique = it.unique,
605                             columnNames = it.columnNames,
606                             orders = it.orders
607                         )
608                     }
609                 } else {
610                     context.logger.w(
611                         Warning.INDEX_FROM_PARENT_IS_DROPPED,
612                         parentTypeElement,
613                         ProcessorErrors.droppedSuperClassIndex(
614                             childEntity = element.qualifiedName,
615                             superEntity = parentTypeElement.qualifiedName
616                         )
617                     )
618                     emptyList()
619                 }
620             } ?: emptyList()
621         return myIndices + loadSuperIndices(parentTypeElement.superClass, tableName, inherit)
622     }
623 }
624