From 0427fa60873d18f0eab021a177b6039f19c0cd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 29 Mar 2020 18:05:56 +0200 Subject: [PATCH] [Events] Add support for selective updates and upserting. --- annotation/.gitignore | 1 + annotation/build.gradle | 29 ++ .../edziennik/annotation/SelectiveDao.kt | 14 + .../edziennik/annotation/UpdateSelective.kt | 13 + app/build.gradle | 3 + .../edziennik/data/api/models/Data.kt | 2 +- .../edziennik/data/api/task/AppSync.kt | 2 +- .../edziennik/data/db/dao/BaseDao.kt | 75 +++- .../edziennik/data/db/dao/EventDao.kt | 76 ++-- .../data/firebase/SzkolnyAppFirebase.kt | 2 +- .../ui/dialogs/event/EventManualDialog.kt | 2 +- codegen/.gitignore | 1 + codegen/build.gradle | 39 +++ .../edziennik/codegen/FileGenerator.kt | 330 ++++++++++++++++++ settings.gradle | 2 + 15 files changed, 542 insertions(+), 49 deletions(-) create mode 100644 annotation/.gitignore create mode 100644 annotation/build.gradle create mode 100644 annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt create mode 100644 annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt create mode 100644 codegen/.gitignore create mode 100644 codegen/build.gradle create mode 100644 codegen/src/main/java/pl/szczodrzynski/edziennik/codegen/FileGenerator.kt diff --git a/annotation/.gitignore b/annotation/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/annotation/.gitignore @@ -0,0 +1 @@ +/build diff --git a/annotation/build.gradle b/annotation/build.gradle new file mode 100644 index 00000000..ee7f6d51 --- /dev/null +++ b/annotation/build.gradle @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +apply plugin: 'java-library' +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} + +sourceCompatibility = "7" +targetCompatibility = "7" + +repositories { + mavenCentral() +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt new file mode 100644 index 00000000..d37e5b0a --- /dev/null +++ b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/SelectiveDao.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.annotation + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class SelectiveDao( + val db: KClass<*> +) diff --git a/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt new file mode 100644 index 00000000..224fca1c --- /dev/null +++ b/annotation/src/main/java/pl/szczodrzynski/edziennik/annotation/UpdateSelective.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class UpdateSelective( + val primaryKeys: Array, + val skippedColumns: Array = [] +) diff --git a/app/build.gradle b/app/build.gradle index ca5c9fcd..21392ed3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,6 +193,9 @@ dependencies { implementation "io.coil-kt:coil:0.9.2" implementation 'com.github.kuba2k2:NumberSlidingPicker:2921225f76' + + implementation project(":annotation") + kapt project(":codegen") } repositories { mavenCentral() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index b5086539..1292e799 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -284,7 +284,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt db.gradeDao().addAll(gradeList) } if (eventList.isNotEmpty()) { - db.eventDao().addAll(eventList) + db.eventDao().upsertAll(eventList) } if (noticeList.isNotEmpty()) { db.noticeDao().clear(profile.id) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt index df552f2f..63d4ec88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt @@ -44,7 +44,7 @@ class AppSync(val app: App, val notifications: MutableList, val pr event.addedDate ) }) - return app.db.eventDao().addAll(events).size + return app.db.eventDao().upsertAll(events).size } return 0; } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt index 7c9583bc..4b82fd01 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt @@ -5,10 +5,7 @@ package pl.szczodrzynski.edziennik.data.db.dao import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.RawQuery +import androidx.room.* import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery @@ -24,11 +21,75 @@ interface BaseDao { fun getOneNow(query: SupportSQLiteQuery): F? fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query)) - @Insert(onConflict = OnConflictStrategy.REPLACE) + /** + * INSERT an [item] into the database, + * ignoring any conflicts. + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) fun add(item: T): Long - - @Insert(onConflict = OnConflictStrategy.REPLACE) + /** + * INSERT [items] into the database, + * ignoring any conflicts. + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) fun addAll(items: List): LongArray + /** + * REPLACE an [item] in the database, + * removing any conflicting rows. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun replace(item: T) + /** + * REPLACE [items] in the database, + * removing any conflicting rows. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun replaceAll(items: List) + + /** + * Selective UPDATE an [item] in the database. + * Do nothing if a matching item does not exist. + */ + fun update(item: T): Long + /** + * Selective UPDATE [items] in the database. + * Do nothing for those items which do not exist. + */ + fun updateAll(items: List): LongArray + + /** + * Remove all items from the database, + * that match the given [profileId]. + */ fun clear(profileId: Int) + + /** + * INSERT an [item] into the database, + * doing a selective [update] on conflicts. + * @return the newly inserted item's ID or -1L if the item was updated instead + */ + @Transaction + fun upsert(item: T): Long { + val id = add(item) + if (id == -1L) update(item) + return id + } + /** + * INSERT [items] into the database, + * doing a selective [update] on conflicts. + * @return a [LongArray] of IDs of newly inserted items or -1L if the item existed before + */ + @Transaction + fun upsertAll(items: List): LongArray { + val insertResult = addAll(items) + val updateList = mutableListOf() + + insertResult.forEachIndexed { index, result -> + if (result == -1L) updateList.add(items[index]) + } + + if (updateList.isNotEmpty()) updateAll(items) + return insertResult + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt index 4ab8d15f..fbaccf30 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt @@ -10,6 +10,10 @@ import androidx.room.RawQuery import androidx.room.Transaction import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.annotation.SelectiveDao +import pl.szczodrzynski.edziennik.annotation.UpdateSelective +import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.full.EventFull @@ -17,6 +21,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @Dao +@SelectiveDao(db = AppDb::class) abstract class EventDao : BaseDao { companion object { private const val QUERY = """ @@ -37,45 +42,21 @@ abstract class EventDao : BaseDao { private const val NOT_BLACKLISTED = """events.eventBlacklisted = 0""" } - //abstract fun queryRaw(query: SupportSQLiteQuery) - //private fun queryRaw(query: String) = queryRaw(SimpleSQLiteQuery(query)) - - @Query("DELETE FROM events WHERE profileId = :profileId") - abstract override fun clear(profileId: Int) - - /*fun update(event: Event) = - queryRaw("""UPDATE events SET - eventDate = '${event.date.stringY_m_d}', - eventTime = ${event.time?.stringValue}, - eventTopic = '${event.topic}'""")*/ - - @Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id") - abstract fun remove(profileId: Int, id: Long) - - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") - abstract fun removeMetadata(profileId: Int, thingType: Int, thingId: Long) - - @Transaction - open fun remove(profileId: Int, type: Long, id: Long) { - remove(profileId, id) - removeMetadata(profileId, if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, id) - } - - @Transaction - open fun remove(event: Event) { - remove(event.profileId, event.type, event.id) - } - - @Transaction - open fun remove(profileId: Int, event: Event) { - remove(profileId, event.type, event.id) - } + private val selective by lazy { EventDaoSelective(App.db) } @RawQuery(observedEntities = [Event::class]) abstract override fun getRaw(query: SupportSQLiteQuery): LiveData> + // SELECTIVE UPDATE + @UpdateSelective(primaryKeys = ["profileId", "eventId"], skippedColumns = ["homeworkBody", "attachmentIds", "attachmentNames"]) + override fun update(item: Event) = selective.update(item) + override fun updateAll(items: List) = selective.updateAll(items) + // CLEAR + @Query("DELETE FROM events WHERE profileId = :profileId") + abstract override fun clear(profileId: Int) + // GET ALL - LIVE DATA fun getAll(profileId: Int) = getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY") fun getAllByType(profileId: Int, type: Long, filter: String = "1") = @@ -87,8 +68,7 @@ abstract class EventDao : BaseDao { fun getAllNearest(profileId: Int, today: Date, limit: Int) = getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate >= '${today.stringY_m_d}' $ORDER_BY LIMIT $limit") - - + // GET ALL - NOW fun getAllNow(profileId: Int) = getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY") fun getNotNotifiedNow() = @@ -98,13 +78,11 @@ abstract class EventDao : BaseDao { fun getAllByDateNow(profileId: Int, date: Date) = getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY") - - + // GET ONE - NOW fun getByIdNow(profileId: Int, id: Long) = getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id") - @Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1") abstract fun getBlacklistedIds(profileId: Int): List @@ -147,4 +125,26 @@ abstract class EventDao : BaseDao { @Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId") abstract fun setBlacklisted(profileId: Int, eventId: Long, blacklisted: Boolean) + @Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id") + abstract fun remove(profileId: Int, id: Long) + + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") + abstract fun removeMetadata(profileId: Int, thingType: Int, thingId: Long) + + @Transaction + open fun remove(profileId: Int, type: Long, id: Long) { + remove(profileId, id) + removeMetadata(profileId, if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, id) + } + + @Transaction + open fun remove(event: Event) { + remove(event.profileId, event.type, event.id) + } + + @Transaction + open fun remove(profileId: Int, event: Event) { + remove(profileId, event.type, event.id) + } + } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index 4aa7a855..c394c7cc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt @@ -152,7 +152,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: events += event metadataList += metadata } - app.db.eventDao().addAll(events) + app.db.eventDao().upsertAll(events) app.db.metadataDao().addAllReplace(metadataList) if (notificationList.isNotEmpty()) { app.db.notificationDao().addAll(notificationList) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index ecf1ff71..d6d175d9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -587,7 +587,7 @@ class EventManualDialog( private fun finishAdding(eventObject: Event, metadataObject: Metadata) { launch { withContext(Dispatchers.Default) { - app.db.eventDao().add(eventObject) + app.db.eventDao().upsert(eventObject) app.db.metadataDao().add(metadataObject) } } diff --git a/codegen/.gitignore b/codegen/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/codegen/.gitignore @@ -0,0 +1 @@ +/build diff --git a/codegen/build.gradle b/codegen/build.gradle new file mode 100644 index 00000000..e2630bd0 --- /dev/null +++ b/codegen/build.gradle @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +apply plugin: 'java-library' +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' + +kapt { + generateStubs = true +} + +sourceSets { + main { + java { + srcDir "${buildDir.absolutePath}/tmp/kapt/main/kotlinGenerated/" + } + } +} + + +dependencies { + kapt project(":annotation") + compileOnly project(':annotation') + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + // configuration generator for service providers + implementation "com.google.auto.service:auto-service:1.0-rc4" + kapt "com.google.auto.service:auto-service:1.0-rc4" + kapt "androidx.room:room-compiler:${versions.room}" + implementation "androidx.room:room-runtime:${versions.room}" + implementation "com.squareup:kotlinpoet:1.5.0" + implementation "androidx.sqlite:sqlite:2.1.0@aar" + +} + +sourceCompatibility = "7" +targetCompatibility = "7" diff --git a/codegen/src/main/java/pl/szczodrzynski/edziennik/codegen/FileGenerator.kt b/codegen/src/main/java/pl/szczodrzynski/edziennik/codegen/FileGenerator.kt new file mode 100644 index 00000000..dee1d098 --- /dev/null +++ b/codegen/src/main/java/pl/szczodrzynski/edziennik/codegen/FileGenerator.kt @@ -0,0 +1,330 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-28. + */ + +package pl.szczodrzynski.edziennik.codegen + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.TypeConverters +import com.google.auto.service.AutoService +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import pl.szczodrzynski.edziennik.annotation.SelectiveDao +import pl.szczodrzynski.edziennik.annotation.UpdateSelective +import java.io.File +import javax.annotation.processing.* +import javax.lang.model.SourceVersion +import javax.lang.model.element.* +import javax.lang.model.type.MirroredTypeException +import javax.lang.model.type.MirroredTypesException +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.ElementFilter +import javax.tools.Diagnostic +import kotlin.reflect.KClass + +@Suppress("unused") +@AutoService(Processor::class) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@SupportedOptions(FileGenerator.KAPT_KOTLIN_GENERATED_OPTION_NAME) +class FileGenerator : AbstractProcessor() { + companion object { + const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated" + } + + private data class TypeConverter(val dataType: TypeMirror, val converterType: TypeElement, val methodName: Name, val returnType: TypeMirror) + + private inline fun Element.getAnnotationClassValue(f: T.() -> KClass<*>) = try { + getAnnotation(T::class.java).f() + throw Exception("Expected to get a MirroredTypeException") + } catch (e: MirroredTypeException) { + e.typeMirror + } + private inline fun Element.getAnnotationClassValues(f: T.() -> Array>) = try { + getAnnotation(T::class.java).f() + throw Exception("Expected to get a MirroredTypesException") + } catch (e: MirroredTypesException) { + e.typeMirrors + } + + override fun process(set: MutableSet?, roundEnvironment: RoundEnvironment?): Boolean { + roundEnvironment?.getElementsAnnotatedWith(SelectiveDao::class.java)?.forEach { it -> + if (it.kind != ElementKind.CLASS) { + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Can only be applied to classes, element: $it") + return false + } + + val generatedSourcesRoot = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] + if (generatedSourcesRoot?.isEmpty() != false) { + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Can't find the target directory for generated Kotlin files.") + return false + } + + val file = File(generatedSourcesRoot) + file.mkdirs() + + val dao = it as TypeElement + processClass(dao, file) + + //processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "package = $packageName, className = $className, methodName = $methodName, tableName = $tableName, paramName = $paramName, paramClass = $paramClass") + } + return true + } + + private fun processClass(dao: TypeElement, file: File) { + val daoName = dao.simpleName.toString() + val packageName = processingEnv.elementUtils.getPackageOf(dao).toString() + + val dbType = processingEnv.typeUtils.asElement(dao.getAnnotationClassValue { db }) as TypeElement + val typeConverters = dbType.getAnnotationClassValues { value }.map { + processingEnv.typeUtils.asElement(it) as TypeElement + }.map { type -> + processingEnv.elementUtils.getAllMembers(type).mapNotNull { element -> + if (element is ExecutableElement) { + if (element.returnType.toString() == "java.lang.String" + || element.returnType.toString() == "java.lang.Long" + || element.returnType.toString() == "java.lang.Integer" + || element.returnType.kind.isPrimitive) { + if (element.simpleName.startsWith("to") && element.parameters.isNotEmpty()) + return@mapNotNull TypeConverter(element.parameters.first().asType(), type, element.simpleName, element.returnType) + } + } + null + } + }.flatten() + + //processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "c = ${typeConverters.joinToString()}") + + val roomDatabase = ClassName("androidx.room", "RoomDatabase") + val selective = TypeSpec.classBuilder("${daoName}Selective") + .primaryConstructor(FunSpec.constructorBuilder() + .addParameter("__db", roomDatabase, KModifier.PRIVATE) + .build()) + .addProperty(PropertySpec.builder("__db", roomDatabase) + .initializer("__db") + .addModifiers(KModifier.PRIVATE) + .build()) + + val usedTypeConverters = mutableSetOf() + + processingEnv.elementUtils.getAllMembers(dao).forEach { element -> + if (element.kind != ElementKind.METHOD) + return@forEach + val method = element as ExecutableElement + val annotation = method.getAnnotation(UpdateSelective::class.java) ?: return@forEach + usedTypeConverters.addAll(processMethod(selective, method, annotation, typeConverters)) + } + + usedTypeConverters.forEach { converter -> + selective.addProperty(PropertySpec.builder("__${converter.converterType.simpleName}", converter.converterType.asType().asTypeName(), KModifier.PRIVATE) + .delegate(CodeBlock.builder() + .beginControlFlow("lazy") + .addStatement("%T()", converter.converterType.asType().asTypeName()) + .endControlFlow() + .build()) + .build()) + } + + FileSpec.builder(packageName, "${daoName}Selective") + .addType(selective.build()) + .build() + .writeTo(file) + } + + private fun VariableElement.name() = getAnnotation(ColumnInfo::class.java)?.name ?: simpleName.toString() + + private fun processMethod(cls: TypeSpec.Builder, method: ExecutableElement, annotation: UpdateSelective, typeConverters: List): List { + val methodName = method.simpleName.toString() + val parameter = method.parameters.first() + val paramName = parameter.simpleName.toString() + val paramTypeElement = processingEnv.typeUtils.asElement(parameter.asType()) as TypeElement + val paramTypeAnnotation = paramTypeElement.getAnnotation(Entity::class.java) + + val tableName = paramTypeAnnotation.tableName + val primaryKeys = annotation.primaryKeys + val skippedColumns = annotation.skippedColumns + + val members = processingEnv.elementUtils.getAllMembers(paramTypeElement) + val allFields = ElementFilter.fieldsIn(members) + allFields.removeAll { skippedColumns.contains(it.simpleName.toString()) } + allFields.removeAll { it.getAnnotation(Ignore::class.java) != null } + allFields.removeAll { field -> field.modifiers.any { it == Modifier.STATIC || it == Modifier.FINAL } } + + val fields = allFields.filterNot { primaryKeys.contains(it.name()) } + val primaryFields = allFields.filter { primaryKeys.contains(it.name()) } + val fieldNames = fields.map { it.name() } + val primaryFieldNames = primaryFields.map { it.name() } + + val fieldNamesQuery = fieldNames.joinToString { "$it = ?" } + val primaryFieldNamesQuery = primaryFieldNames.joinToString(" AND ") { "$it = ?" } + val query = "\"\"\"UPDATE $tableName SET $fieldNamesQuery WHERE $primaryFieldNamesQuery\"\"\"" + + val entityInsertionAdapter = ClassName("androidx.room", "EntityInsertionAdapter") + val supportSQLiteStatement = ClassName("androidx.sqlite.db", "SupportSQLiteStatement") + + val usedTypeConverters = mutableListOf() + + val bind = CodeBlock.builder() + (fields+primaryFields).forEachIndexed { i, field -> + val index = i+1 + val fieldName = field.simpleName.toString() + val name = "${paramName}_$fieldName" + val realName = "${paramName}.$fieldName" + val nullable = field.getAnnotation(org.jetbrains.annotations.Nullable::class.java) != null + + var param = when (field.asType().kind) { + TypeKind.BOOLEAN -> "if ($name) 1L else 0L" + TypeKind.BYTE, + TypeKind.SHORT, + TypeKind.INT -> "$name.toLong()" + TypeKind.CHAR -> "$name.toString()" + TypeKind.FLOAT -> "$name.toDouble()" + else -> when (field.asType().toString()) { + "java.lang.String" -> name + "java.lang.Boolean" -> "if ($name == true) 1L else 0L" + "java.lang.Byte", + "java.lang.Short", + "java.lang.Integer" -> "$name.toLong()" + "java.lang.Long" -> name + "java.lang.Char" -> "$name.toString()" + "java.lang.Float" -> "$name.toDouble()" + "java.lang.Double" -> name + else -> name + } + } + + var isConvert = false + val bindMethod = when (field.asType().kind) { + TypeKind.BOOLEAN -> "bindLong" + TypeKind.BYTE -> "bindLong" + TypeKind.SHORT -> "bindLong" + TypeKind.INT -> "bindLong" + TypeKind.LONG -> "bindLong" + TypeKind.CHAR -> "bindString" + TypeKind.FLOAT -> "bindDouble" + TypeKind.DOUBLE -> "bindDouble" + else -> when (field.asType().toString()) { + "java.lang.String" -> "bindString" + "java.lang.Boolean" -> "bindLong" + "java.lang.Byte" -> "bindLong" + "java.lang.Short" -> "bindLong" + "java.lang.Integer" -> "bindLong" + "java.lang.Long" -> "bindLong" + "java.lang.Char" -> "bindString" + "java.lang.Float" -> "bindDouble" + "java.lang.Double" -> "bindDouble" + else -> { + val converter = typeConverters.firstOrNull { + it.dataType.toString() == field.asType().toString() + } + if (converter != null) { + param = "__${converter.converterType.simpleName}.${converter.methodName}($realName)" + param = when (converter.returnType.toString()) { + "java.lang.Integer", "int", + "java.lang.Short", "short", + "java.lang.Byte", "byte" -> "$param.toLong()" + "java.lang.Boolean", "boolean" -> "if ($param) 1L else 0L" + "java.lang.Char", "char" -> "$param.toString()" + "java.lang.Float", "float" -> "$param.toDouble()" + else -> param + } + isConvert = true + usedTypeConverters += converter + when (converter.returnType.toString()) { + "java.lang.Integer", "int", + "java.lang.Short", "short", + "java.lang.Byte", "byte", + "java.lang.Boolean", "boolean" -> "bindLong" + "java.lang.Char", "char" -> "bindString" + "java.lang.Float", "float" -> "bindDouble" + else -> "bindString" + } + } + else "bind${field.asType()}" + } + } + } + + if (!isConvert) { + bind.addStatement("val $name = $realName") + } + else { + bind.addStatement("val $name = $param") + param = name + } + if (nullable) { + bind.beginControlFlow("if ($name == null)") + .addStatement("stmt.bindNull($index)") + .endControlFlow() + .beginControlFlow("else") + .addStatement("stmt.$bindMethod($index, $param)") + .endControlFlow() + } + else { + bind.addStatement("stmt.$bindMethod($index, $param)") + } + } + + val adapterName = "__insertionAdapterOf$methodName" + val delegate = CodeBlock.builder().add(""" + |lazy { + | object : EntityInsertionAdapter<%T>(__db) { + | override fun createQuery() = $query + | override fun bind(stmt: %T, $paramName: %T) { + |${bind.indent().indent().indent().build()} + | } + | } + |}""".trimMargin(), paramTypeElement.asClassName(), supportSQLiteStatement, paramTypeElement.asClassName()) + + cls.addProperty(PropertySpec.builder(adapterName, entityInsertionAdapter.parameterizedBy(paramTypeElement.asClassName()), KModifier.PRIVATE) + .delegate(delegate.build()) + .build()) + + val list = ClassName("kotlin.collections", "List") + val longArray = ClassName("kotlin", "LongArray") + + val function = FunSpec.builder(methodName) + .addModifiers(KModifier.INTERNAL) + .addParameter("item", parameter.asType().asTypeName()) + .returns(Long::class.java) + .addStatement("__db.assertNotSuspendingTransaction()") + .addStatement("__db.beginTransaction()") + .addCode(""" + |try { + | val _result = $adapterName.insertAndReturnId(item) + | __db.setTransactionSuccessful() + | return _result + |} finally { + | __db.endTransaction() + |} + """.trimMargin()) + .build() + + val functionAll = FunSpec.builder(methodName+"All") + .addModifiers(KModifier.INTERNAL) + .addParameter("items", list.parameterizedBy(parameter.asType().asTypeName())) + .returns(longArray) + .addStatement("__db.assertNotSuspendingTransaction()") + .addStatement("__db.beginTransaction()") + .addCode(""" + |try { + | val _result = $adapterName.insertAndReturnIdsArray(items) + | __db.setTransactionSuccessful() + | return _result + |} finally { + | __db.endTransaction() + |} + """.trimMargin()) + .build() + + cls.addFunction(function) + cls.addFunction(functionAll) + return usedTypeConverters + } + + override fun getSupportedAnnotationTypes(): MutableSet { + return mutableSetOf(SelectiveDao::class.java.canonicalName) + } +} diff --git a/settings.gradle b/settings.gradle index d2e0a356..3625fac9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,5 @@ +include ':codegen' +include ':annotation' rootProject.name='Szkolny.eu' include ':app', ':agendacalendarview', ':mhttp', ':material-about-library', ':cafebar', ':szkolny-font', ':nachos' /*