| /* |
| * Copyright 2018 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 androidx.room.processor |
| |
| import androidx.room.Fts3 |
| import androidx.room.Fts4 |
| import androidx.room.FtsOptions.MatchInfo |
| import androidx.room.FtsOptions.Order |
| import androidx.room.parser.FtsVersion |
| import androidx.room.parser.SQLTypeAffinity |
| import androidx.room.compiler.processing.XAnnotationBox |
| import androidx.room.compiler.processing.XType |
| import androidx.room.compiler.processing.XTypeElement |
| import androidx.room.processor.EntityProcessor.Companion.extractForeignKeys |
| import androidx.room.processor.EntityProcessor.Companion.extractIndices |
| import androidx.room.processor.EntityProcessor.Companion.extractTableName |
| import androidx.room.processor.cache.Cache |
| import androidx.room.vo.Entity |
| import androidx.room.vo.Field |
| import androidx.room.vo.Fields |
| import androidx.room.vo.FtsEntity |
| import androidx.room.vo.FtsOptions |
| import androidx.room.vo.LanguageId |
| import androidx.room.vo.PrimaryKey |
| import androidx.room.vo.columnNames |
| |
| class FtsTableEntityProcessor internal constructor( |
| baseContext: Context, |
| val element: XTypeElement, |
| private val referenceStack: LinkedHashSet<String> = LinkedHashSet() |
| ) : EntityProcessor { |
| |
| val context = baseContext.fork(element) |
| |
| override fun process(): FtsEntity { |
| return context.cache.entities.get(Cache.EntityKey(element)) { |
| doProcess() |
| } as FtsEntity |
| } |
| |
| private fun doProcess(): FtsEntity { |
| context.checker.hasAnnotation(element, androidx.room.Entity::class, |
| ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY) |
| val entityAnnotation = element.toAnnotationBox(androidx.room.Entity::class) |
| val tableName: String |
| if (entityAnnotation != null) { |
| tableName = extractTableName(element, entityAnnotation.value) |
| context.checker.check(extractIndices(entityAnnotation, tableName).isEmpty(), |
| element, ProcessorErrors.INDICES_IN_FTS_ENTITY) |
| context.checker.check(extractForeignKeys(entityAnnotation).isEmpty(), |
| element, ProcessorErrors.FOREIGN_KEYS_IN_FTS_ENTITY) |
| } else { |
| tableName = element.name |
| } |
| |
| val pojo = PojoProcessor.createFor( |
| context = context, |
| element = element, |
| bindingScope = FieldProcessor.BindingScope.TWO_WAY, |
| parent = null, |
| referenceStack = referenceStack).process() |
| |
| context.checker.check(pojo.relations.isEmpty(), element, ProcessorErrors.RELATION_IN_ENTITY) |
| |
| val (ftsVersion, ftsOptions) = if (element.hasAnnotation(androidx.room.Fts3::class)) { |
| FtsVersion.FTS3 to getFts3Options(element.toAnnotationBox(Fts3::class)!!) |
| } else { |
| FtsVersion.FTS4 to getFts4Options(element.toAnnotationBox(Fts4::class)!!) |
| } |
| |
| val shadowTableName = if (ftsOptions.contentEntity != null) { |
| // In 'external content' mode the FTS table content is in another table. |
| // See: https://www.sqlite.org/fts3.html#_external_content_fts4_tables_ |
| ftsOptions.contentEntity.tableName |
| } else { |
| // The %_content table contains the unadulterated data inserted by the user into the FTS |
| // virtual table. See: https://www.sqlite.org/fts3.html#shadow_tables |
| "${tableName}_content" |
| } |
| |
| val primaryKey = findAndValidatePrimaryKey(entityAnnotation, pojo.fields) |
| findAndValidateLanguageId(pojo.fields, ftsOptions.languageIdColumnName) |
| |
| val missingNotIndexed = ftsOptions.notIndexedColumns - pojo.columnNames |
| context.checker.check(missingNotIndexed.isEmpty(), element, |
| ProcessorErrors.missingNotIndexedField(missingNotIndexed)) |
| |
| pojo.fields.filter { it.element.hasAnnotation(androidx.room.ForeignKey::class) }.forEach { |
| context.logger.e(ProcessorErrors.INVALID_FOREIGN_KEY_IN_FTS_ENTITY, it.element) |
| } |
| |
| context.checker.check(ftsOptions.prefixSizes.all { it > 0 }, |
| element, ProcessorErrors.INVALID_FTS_ENTITY_PREFIX_SIZES) |
| |
| val entity = FtsEntity( |
| element = element, |
| tableName = tableName, |
| type = pojo.type, |
| fields = pojo.fields, |
| embeddedFields = pojo.embeddedFields, |
| primaryKey = primaryKey, |
| constructor = pojo.constructor, |
| ftsVersion = ftsVersion, |
| ftsOptions = ftsOptions, |
| shadowTableName = shadowTableName) |
| |
| validateExternalContentEntity(entity) |
| |
| return entity |
| } |
| |
| private fun getFts3Options(annotation: XAnnotationBox<Fts3>) = |
| FtsOptions( |
| tokenizer = annotation.value.tokenizer, |
| tokenizerArgs = annotation.value.tokenizerArgs.asList(), |
| contentEntity = null, |
| languageIdColumnName = "", |
| matchInfo = MatchInfo.FTS4, |
| notIndexedColumns = emptyList(), |
| prefixSizes = emptyList(), |
| preferredOrder = Order.ASC) |
| |
| private fun getFts4Options(annotation: XAnnotationBox<Fts4>): FtsOptions { |
| val contentEntity: Entity? = getContentEntity(annotation.getAsType("contentEntity")) |
| return FtsOptions( |
| tokenizer = annotation.value.tokenizer, |
| tokenizerArgs = annotation.value.tokenizerArgs.asList(), |
| contentEntity = contentEntity, |
| languageIdColumnName = annotation.value.languageId, |
| matchInfo = annotation.value.matchInfo, |
| notIndexedColumns = annotation.value.notIndexed.asList(), |
| prefixSizes = annotation.value.prefix.asList(), |
| preferredOrder = annotation.value.order) |
| } |
| |
| private fun getContentEntity(entityType: XType?): Entity? { |
| if (entityType == null) { |
| context.logger.e(element, ProcessorErrors.FTS_EXTERNAL_CONTENT_CANNOT_FIND_ENTITY) |
| return null |
| } |
| |
| val defaultType = context.processingEnv.requireType(Object::class) |
| if (entityType.isSameType(defaultType)) { |
| return null |
| } |
| val contentEntityElement = entityType.asTypeElement() |
| if (!contentEntityElement.hasAnnotation(androidx.room.Entity::class)) { |
| context.logger.e(contentEntityElement, |
| ProcessorErrors.externalContentNotAnEntity(contentEntityElement.toString())) |
| return null |
| } |
| return EntityProcessor(context, contentEntityElement, referenceStack).process() |
| } |
| |
| private fun findAndValidatePrimaryKey( |
| entityAnnotation: XAnnotationBox<androidx.room.Entity>?, |
| fields: List<Field> |
| ): PrimaryKey { |
| val keysFromEntityAnnotation = |
| entityAnnotation?.value?.primaryKeys?.mapNotNull { pkColumnName -> |
| val field = fields.firstOrNull { it.columnName == pkColumnName } |
| context.checker.check(field != null, element, |
| ProcessorErrors.primaryKeyColumnDoesNotExist(pkColumnName, |
| fields.map { it.columnName })) |
| field?.let { pkField -> |
| PrimaryKey( |
| declaredIn = pkField.element.enclosingElement, |
| fields = Fields(pkField), |
| autoGenerateId = true) |
| } |
| } ?: emptyList() |
| |
| val keysFromPrimaryKeyAnnotations = fields.mapNotNull { field -> |
| if (field.element.hasAnnotation(androidx.room.PrimaryKey::class)) { |
| PrimaryKey( |
| declaredIn = field.element.enclosingElement, |
| fields = Fields(field), |
| autoGenerateId = true) |
| } else { |
| null |
| } |
| } |
| val primaryKeys = keysFromEntityAnnotation + keysFromPrimaryKeyAnnotations |
| if (primaryKeys.isEmpty()) { |
| fields.firstOrNull { it.columnName == "rowid" }?.let { |
| context.checker.check(it.element.hasAnnotation(androidx.room.PrimaryKey::class), |
| it.element, ProcessorErrors.MISSING_PRIMARY_KEYS_ANNOTATION_IN_ROW_ID) |
| } |
| return PrimaryKey.MISSING |
| } |
| context.checker.check(primaryKeys.size == 1, element, |
| ProcessorErrors.TOO_MANY_PRIMARY_KEYS_IN_FTS_ENTITY) |
| val primaryKey = primaryKeys.first() |
| context.checker.check(primaryKey.columnNames.first() == "rowid", |
| primaryKey.declaredIn ?: element, |
| ProcessorErrors.INVALID_FTS_ENTITY_PRIMARY_KEY_NAME) |
| context.checker.check(primaryKey.fields.first().affinity == SQLTypeAffinity.INTEGER, |
| primaryKey.declaredIn ?: element, |
| ProcessorErrors.INVALID_FTS_ENTITY_PRIMARY_KEY_AFFINITY) |
| return primaryKey |
| } |
| |
| private fun validateExternalContentEntity(ftsEntity: FtsEntity) { |
| val contentEntity = ftsEntity.ftsOptions.contentEntity |
| if (contentEntity == null) { |
| return |
| } |
| |
| // Verify external content columns are a superset of those defined in the FtsEntity |
| ftsEntity.nonHiddenFields.filterNot { |
| contentEntity.fields.any { contentField -> contentField.columnName == it.columnName } |
| }.forEach { |
| context.logger.e(it.element, ProcessorErrors.missingFtsContentField( |
| element.qualifiedName, it.columnName, |
| contentEntity.element.qualifiedName |
| )) |
| } |
| } |
| |
| private fun findAndValidateLanguageId( |
| fields: List<Field>, |
| languageIdColumnName: String |
| ): LanguageId { |
| if (languageIdColumnName.isEmpty()) { |
| return LanguageId.MISSING |
| } |
| |
| val languageIdField = fields.firstOrNull { it.columnName == languageIdColumnName } |
| if (languageIdField == null) { |
| context.logger.e(element, ProcessorErrors.missingLanguageIdField(languageIdColumnName)) |
| return LanguageId.MISSING |
| } |
| |
| context.checker.check(languageIdField.affinity == SQLTypeAffinity.INTEGER, |
| languageIdField.element, ProcessorErrors.INVALID_FTS_ENTITY_LANGUAGE_ID_AFFINITY) |
| return LanguageId(languageIdField.element, languageIdField) |
| } |
| } |