AutoMigrationProcessor.kt

/*
 * Copyright 2021 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.AutoMigration
import androidx.room.compiler.processing.XTypeElement
import androidx.room.ext.RoomTypeNames
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.SchemaBundle.deserialize
import androidx.room.util.DiffException
import androidx.room.util.SchemaDiffer
import androidx.room.vo.AutoMigrationResult
import java.io.File

class AutoMigrationProcessor(
    val context: Context,
    val element: XTypeElement,
    val latestDbSchema: DatabaseBundle
) {
    /**
     * Retrieves two schemas of the same database provided in the @AutoMigration annotation,
     * detects the schema changes that occurred between the two versions.
     *
     * @return the AutoMigrationResult containing the schema changes detected
     */
    fun process(): AutoMigrationResult? {
        if (!element.isInterface()) {
            context.logger.e(
                ProcessorErrors.AUTOMIGRATION_ANNOTATED_TYPE_ELEMENT_MUST_BE_INTERFACE,
                element
            )
            return null
        }

        if (!context.processingEnv.requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
            .isAssignableFrom(element.type)
        ) {
            context.logger.e(
                ProcessorErrors.AUTOMIGRATION_ELEMENT_MUST_IMPLEMENT_AUTOMIGRATION_CALLBACK,
                element
            )
            return null
        }

        val annotationBox = element.toAnnotationBox(AutoMigration::class)
        if (annotationBox == null) {
            context.logger.e(
                element,
                ProcessorErrors.AUTOMIGRATION_ANNOTATION_MISSING
            )
            return null
        }

        val from = annotationBox.value.from
        val to = annotationBox.value.to

        if (to <= from) {
            context.logger.e(
                ProcessorErrors.autoMigrationToVersionMustBeGreaterThanFrom(to, from),
                element
            )
            return null
        }

        val validatedFromSchemaFile = getValidatedSchemaFile(from) ?: return null
        val fromSchemaBundle = validatedFromSchemaFile.inputStream().use {
            deserialize(it).database
        }

        val validatedToSchemaFile = getValidatedSchemaFile(to) ?: return null
        val toSchemaBundle = if (to == latestDbSchema.version) {
            latestDbSchema
        } else {
            validatedToSchemaFile.inputStream().use {
                deserialize(it).database
            }
        }

        val schemaDiff = try {
            SchemaDiffer(
                fromSchemaBundle = fromSchemaBundle,
                toSchemaBundle = toSchemaBundle
            ).diffSchemas()
        } catch (ex: DiffException) {
            context.logger.e(ex.errorMessage)
            return null
        }

        return AutoMigrationResult(
            element = element,
            from = fromSchemaBundle.version,
            to = toSchemaBundle.version,
            addedColumns = schemaDiff.addedColumn,
            addedTables = schemaDiff.addedTable
        )
    }

    // TODO: File bug for not supporting downgrades.
    // TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
    //  null and before calling process
    private fun getValidatedSchemaFile(version: Int): File? {
        val schemaFile = File(
            context.schemaOutFolder,
            "${element.qualifiedName}/$version.json"
        )
        if (!schemaFile.exists()) {
            context.logger.e(
                ProcessorErrors.autoMigrationSchemasNotFound(
                    context.schemaOutFolder.toString(),
                    "${element.qualifiedName}/$version.json"
                ),
                element
            )
            return null
        }
        return schemaFile
    }
}