Database.kt

/*
 * Copyright (C) 2016 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.vo

import androidx.room.RoomMasterTable
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.SCHEMA_LATEST_FORMAT_VERSION
import androidx.room.migration.bundle.SchemaBundle
import androidx.room.util.SchemaFileResolver
import java.io.IOException
import java.io.OutputStream
import java.nio.file.Path
import org.apache.commons.codec.digest.DigestUtils

/**
 * Holds information about a class annotated with Database.
 */
data class Database(
    val element: XTypeElement,
    val type: XType,
    val entities: List<Entity>,
    val views: List<DatabaseView>,
    val daoMethods: List<DaoMethod>,
    val version: Int,
    val exportSchema: Boolean,
    val enableForeignKeys: Boolean,
    val overrideClearAllTables: Boolean
) {
    // This variable will be set once auto-migrations are processed given the DatabaseBundle from
    // this object. This is necessary for tracking the versions involved in the auto-migration.
    lateinit var autoMigrations: List<AutoMigration>
    val typeName: XClassName by lazy { element.asClassName() }

    private val implClassName by lazy {
        "${typeName.simpleNames.joinToString("_")}_Impl"
    }

    val implTypeName: XClassName by lazy {
        XClassName.get(typeName.packageName, implClassName)
    }

    val bundle by lazy {
        DatabaseBundle(
            version, identityHash, entities.map(Entity::toBundle),
            views.map(DatabaseView::toBundle),
            listOf(
                RoomMasterTable.CREATE_QUERY,
                RoomMasterTable.createInsertQuery(identityHash)
            )
        )
    }

    /**
     * Create a has that identifies this database definition so that at runtime we can check to
     * ensure developer didn't forget to update the version.
     */
    val identityHash: String by lazy {
        val idKey = SchemaIdentityKey()
        idKey.appendSorted(entities)
        idKey.appendSorted(views)
        idKey.hash()
    }

    val legacyIdentityHash: String by lazy {
        val entityDescriptions = entities
            .sortedBy { it.tableName }
            .map { it.createTableQuery }
        val indexDescriptions = entities
            .flatMap { entity ->
                entity.indices.map { index ->
                    // For legacy purposes we need to remove the later added 'IF NOT EXISTS'
                    // part of the create statement, otherwise old valid legacy hashes stop
                    // being accepted even though the schema has not changed. b/139306173
                    if (index.unique) {
                        "CREATE UNIQUE INDEX"
                    } else {
                        // The extra space between 'CREATE' and 'INDEX' is on purpose, this
                        // is a typo we have to live with.
                        "CREATE  INDEX"
                    } + index.createQuery(entity.tableName).substringAfter("IF NOT EXISTS")
                }
            }
        val viewDescriptions = views
            .sortedBy { it.viewName }
            .map { it.viewName + it.query.original }
        val input = (entityDescriptions + indexDescriptions + viewDescriptions)
            .joinToString("¯\_(ツ)_/¯")
        DigestUtils.md5Hex(input)
    }

    // Writes schema file to output path, using the input path to check if the schema has changed
    // otherwise it is not written.
    fun exportSchema(inputPath: Path, outputPath: Path) {
        val schemaBundle = SchemaBundle(SCHEMA_LATEST_FORMAT_VERSION, bundle)
        val inputStream = try {
            SchemaFileResolver.RESOLVER.readPath(inputPath)
        } catch (e: IOException) {
            null
        }
        if (inputStream != null) {
            val existing = inputStream.use {
                SchemaBundle.deserialize(it)
            }
            // If existing schema file is the same as the current schema then do not write the file
            // which helps the copy task configured by the Room Gradle Plugin skip execution due
            // to empty variant schema output directory.
            if (existing.isSchemaEqual(schemaBundle)) {
                return
            }
        }
        val outputStream = try {
            SchemaFileResolver.RESOLVER.writePath(outputPath)
        } catch (e: IOException) {
            throw IllegalStateException("Couldn't write schema file!", e)
        }
        SchemaBundle.serialize(schemaBundle, outputStream)
    }

    // Writes scheme file to output stream, the stream should be for a resource which disregards
    // existing schema equality, otherwise use the version of `exportSchema` that takes input and
    // output paths.
    fun exportSchemaOnly(outputStream: OutputStream) {
        val schemaBundle = SchemaBundle(SCHEMA_LATEST_FORMAT_VERSION, bundle)
        SchemaBundle.serialize(schemaBundle, outputStream)
    }
}