blob: 472f46e7846bd1129199b3b056c34f644fd6030c [file] [log] [blame]
/*
* Copyright (C) 2020 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
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.XProcessingEnvConfig
import androidx.room.compiler.processing.XProcessingStep
import androidx.room.compiler.processing.XTypeElement
import androidx.room.log.RLog
import androidx.room.processor.Context
import androidx.room.processor.Context.BooleanProcessorOptions.GENERATE_KOTLIN
import androidx.room.processor.DatabaseProcessor
import androidx.room.processor.ProcessorErrors
import androidx.room.util.SchemaFileResolver
import androidx.room.vo.DaoMethod
import androidx.room.vo.Warning
import androidx.room.writer.AutoMigrationWriter
import androidx.room.writer.DaoWriter
import androidx.room.writer.DatabaseWriter
import java.io.File
import java.nio.file.Path
class DatabaseProcessingStep : XProcessingStep {
override fun annotations(): Set<String> {
return mutableSetOf(Database::class.qualifiedName!!)
}
override fun process(
env: XProcessingEnv,
elementsByAnnotation: Map<String, Set<XElement>>,
isLastRound: Boolean
): Set<XTypeElement> {
check(env.config == getEnvConfig(env.options)) {
"Room Processor expected ${getEnvConfig(env.options)} " +
"but was invoked with a different " +
"configuration: ${env.config}"
}
val context = Context(env)
val rejectedElements = mutableSetOf<XTypeElement>()
val databases = elementsByAnnotation[Database::class.qualifiedName]
?.filterIsInstance<XTypeElement>()
?.mapNotNull { annotatedElement ->
if (isLastRound && !annotatedElement.validate()) {
context.reportMissingTypeReference(annotatedElement.qualifiedName)
return@mapNotNull null
}
val (database, logs) = context.collectLogs { subContext ->
DatabaseProcessor(
subContext,
annotatedElement
).process()
}
if (logs.hasMissingTypeErrors()) {
if (isLastRound) {
// Processing is done yet there are still missing type errors, only report
// those and don't generate code for the database class since compilation
// will fail anyway.
logs.writeTo(context, RLog.MissingTypeErrorFilter)
return@mapNotNull null
} else {
// Abandon processing this database class since it needed a type element
// that is missing. It is possible that the type will be generated by a
// further annotation processing round, so we will try again by adding
// this class element to a deferred set.
rejectedElements.add(annotatedElement)
return@mapNotNull null
}
} else {
logs.writeTo(context)
return@mapNotNull database
}
}
val daoMethodsMap = databases?.flatMap { db -> db.daoMethods.map { it to db } }?.toMap()
daoMethodsMap?.let {
prepareDaosForWriting(databases, it.keys.toList())
it.forEach { (daoMethod, db) ->
DaoWriter(
daoMethod.dao,
db.element,
context.codeLanguage
).write(context.processingEnv)
}
}
databases?.forEach { db ->
DatabaseWriter(db, context.codeLanguage).write(context.processingEnv)
if (db.exportSchema) {
val qName = db.element.qualifiedName
val filename = "${db.version}.json"
val exportToResources =
Context.BooleanProcessorOptions.EXPORT_SCHEMA_RESOURCE.getValue(env)
val schemaOutFolderPath = context.schemaOutFolderPath
if (exportToResources) {
context.logger.w(ProcessorErrors.EXPORTING_SCHEMA_TO_RESOURCES)
val schemaFileOutputStream = env.filer.writeResource(
filePath = Path.of("schemas", qName, filename),
originatingElements = listOf(db.element)
)
db.exportSchema(schemaFileOutputStream)
} else if (schemaOutFolderPath != null) {
val schemaOutFolder = SchemaFileResolver.RESOLVER.getFile(
Path.of(schemaOutFolderPath)
)
if (!schemaOutFolder.exists()) {
schemaOutFolder.mkdirs()
}
val dbSchemaFolder = File(schemaOutFolder, qName)
if (!dbSchemaFolder.exists()) {
dbSchemaFolder.mkdirs()
}
db.exportSchema(
File(dbSchemaFolder, "${db.version}.json")
)
} else {
context.logger.w(
warning = Warning.MISSING_SCHEMA_LOCATION,
element = db.element,
msg = ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY
)
}
}
db.autoMigrations.forEach { autoMigration ->
AutoMigrationWriter(db.element, autoMigration, context.codeLanguage)
.write(context.processingEnv)
}
}
return rejectedElements
}
/**
* Traverses all dao methods and assigns them suffix if they are used in multiple databases.
*/
private fun prepareDaosForWriting(
databases: List<androidx.room.vo.Database>,
daoMethods: List<DaoMethod>
) {
daoMethods.groupBy { it.dao.typeName }
// if used only in 1 database, nothing to do.
.filter { entry -> entry.value.size > 1 }
.forEach { entry ->
entry.value.groupBy { daoMethod ->
// first suffix guess: Database's simple name
val db = databases.first { db -> db.daoMethods.contains(daoMethod) }
db.typeName.simpleNames.last()
}.forEach { (dbName, methods) ->
if (methods.size == 1) {
// good, db names do not clash, use db name as suffix
methods.first().dao.setSuffix(dbName)
} else {
// ok looks like a dao is used in 2 different databases both of
// which have the same name. enumerate.
methods.forEachIndexed { index, method ->
method.dao.setSuffix("${dbName}_$index")
}
}
}
}
}
companion object {
internal fun getEnvConfig(options: Map<String, String>) =
XProcessingEnvConfig.DEFAULT.copy(
excludeMethodsWithInvalidJvmSourceNames = !GENERATE_KOTLIN.getValue(options)
)
}
}