diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..9b92cd51f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +DBFlow is an annotation-processing ORM for Android/SQLite. All SQL boilerplate is generated at compile-time by `dbflow-processor`; the runtime has near-zero reflection overhead. Models can be plain POJOs or extend `BaseModel`, and multiple SQLite databases per app are supported natively. + +## Module Organization + +| Module | Type | Purpose | +|--------|------|---------| +| `dbflow-core` | Java library | Core annotations (`@Database`, `@Table`, `@Column`, `@ForeignKey`, etc.) and interfaces | +| `dbflow` | Android library | Runtime: `FlowManager`, query DSL, model adapters, transactions | +| `dbflow-processor` | Java library | Annotation processor (`DBFlowProcessor.kt`) and all code generation | +| `dbflow-sqlcipher` | Android library | SQLCipher encryption support | +| `dbflow-kotlinextensions` | Android library | Kotlin DSL and extension functions | +| `dbflow-rx` / `dbflow-rx2` | Android library | RXJava 1 & 2 integration | +| `dbflow-rx-kotlinextensions` / `dbflow-rx2-kotlinextensions` | Android library | Kotlin extensions for RX modules | +| `dbflow-tests` | Android app | All tests (unit via Robolectric + instrumented) | + +**SDK/tool versions** (gradle.properties): minSdk 21, targetSdk 34, Kotlin 1.9.24, AGP 8.2.2, Java 8. + +## Architecture + +### Annotation Processing Pipeline + +`DBFlowProcessor` (extends `AbstractProcessor`) runs during `compileKotlin`/`compileJava`. For each annotated class it generates: + +- `${Model}_Table` — static column name constants +- `${Model}_Adapter` — handles cursor loading, insert, update, delete, caching +- `${Database}_HolderImpl` — registry of all tables for a given `@Database` + +Generated sources land in `build/generated/ap_generated_sources/`. Any annotation error fails the build entirely. + +### FlowManager — Central Registry + +`FlowManager` is the single entry point at runtime. It loads the generated `GeneratedDatabaseHolder` via a single reflection call during `FlowManager.init(FlowConfig)`. After that, all operations go through type-safe generated adapters — no further reflection. Required initialization: + +```kotlin +FlowManager.init(FlowConfig.Builder(context).build()) +``` + +### Query DSL + +Queries are built lazily and executed only when `.query()`, `.querySingle()`, or similar terminal methods are called: + +```kotlin +select(User_Table.name, User_Table.email) + .from(User::class.java) + .where(User_Table.id.eq(42)) + .querySingle() +``` + +Models serve double duty as table references. Column constants come from the generated `${Model}_Table` class. + +### Multiple Databases + +Each `@Database`-annotated class is a separate SQLite file. Models declare their database via `@Table(database = MyDatabase::class)`. Migrations are scoped per database version. + +## Build Commands + +```bash +# Full build +./gradlew build + +# Skip tests +./gradlew build -x test -x connectedAndroidTest + +# Single module +./gradlew :dbflow-processor:build + +# Clean +./gradlew clean build +``` + +## Test Commands + +All tests live in `:dbflow-tests`. Unit tests use Robolectric (no emulator needed); instrumented tests require a connected device. + +```bash +# All unit tests +./gradlew :dbflow-tests:test + +# Single test class +./gradlew :dbflow-tests:testDebugUnitTest --tests "*.ConfigIntegrationTest" + +# Single test method +./gradlew :dbflow-tests:testDebugUnitTest --tests "*.ConfigIntegrationTest.testSimpleConfig" + +# Instrumented tests (device/emulator required) +./gradlew :dbflow-tests:connectedAndroidTest +``` + +Custom test rules: `DBFlowTestRule` (unit), `DBFlowInstrumentedTestRule` (instrumented). Base class `BaseUnitTest` sets up a test database and context. + +## Lint + +```bash +./gradlew lint # All modules (abortOnError = false) +./gradlew :dbflow:lint # Single module +``` + +## KSP Migration (current branch: `ksp-support`) + +KSP migration is complete. `dbflow-tests/build.gradle` has KSP enabled and KAPT disabled. `DBFlowSymbolProcessor` implements `SymbolProcessor` and handles all annotation types. All 164 unit tests pass. + +**Key KSP implementation notes:** +- `DBFlowSymbolProcessor` runs two rounds: round 1 processes all annotations and writes non-holder files; if `@ManyToMany` join tables were generated, round 2 picks them up and writes the database registry + holder. +- Database registry files (`${Database}_Database.java`) are written **after** all table adapters so that `hasGlobalTypeConverters` is accurate (it's populated during `checkNeedsReferences()` in `onWriteDefinition`). +- KSP type mapping lives in `KspExtensions.kt` — Kotlin primitives, arrays (`ByteArray` → `byte[]`), and class types all have explicit mappings. +- `@MultiCacheField`/`@ModelCacheField` fields live in companion objects; the KSP path scans companion object declarations explicitly. +- `@OneToMany` methods are scanned in `createColumnDefinitionsFromKsp` via `KSFunctionDeclaration`; `OneToManyDefinition.kspInit()` initializes from KSP data. +- Java-origin fields (e.g. generated join table classes) use `VisibleScopeColumnAccessor` directly; Kotlin-origin fields without `@JvmField` use `PrivateScopeColumnAccessor` with getter/setter generation. + +## Key Files for Architecture + +- `dbflow/src/main/java/com/raizlabs/android/dbflow/config/FlowManager.java` — central registry +- `dbflow/src/main/java/com/raizlabs/android/dbflow/structure/BaseModel.java` — base model implementation +- `dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowProcessor.kt` — main annotation processor entry point +- `dbflow-core/src/main/java/com/raizlabs/android/dbflow/annotation/` — all annotations diff --git a/android-artifacts.gradle b/android-artifacts.gradle index 9c8dae956..257cdafba 100644 --- a/android-artifacts.gradle +++ b/android-artifacts.gradle @@ -1,43 +1,22 @@ -apply plugin: 'com.github.dcendents.android-maven' -install { - repositories.mavenInstaller { - pom { - project { - packaging bt_packaging - name bt_name - url bt_siteUrl - licenses { - license { - name bt_licenseName - url bt_licenseUrl - } - } - scm { - connection bt_gitUrl - developerConnection bt_gitUrl - url bt_siteUrl - } - } +apply plugin: 'maven-publish' + +android { + publishing { + singleVariant('release') { + withSourcesJar() } } } -task sourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - classifier = 'sources' -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + groupId = project.group + artifactId = project.name + version = rootProject.version + } + } + } } - -artifacts { - //archives javadocJar - archives sourcesJar -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 977b2d77e..ab3ec4bf3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,19 @@ buildscript { - ext.kotlin_version = '1.1.51' + ext.kotlin_version = '1.9.24' repositories { - jcenter() + mavenCentral() google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' - classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.7.3' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:${kotlin_version}-1.0.20" } } allprojects { repositories { - jcenter() + mavenCentral() google() maven { url "https://www.jitpack.io" } } diff --git a/dbflow-core/build.gradle b/dbflow-core/build.gradle index 8a3deae9b..788cd0fb0 100644 --- a/dbflow-core/build.gradle +++ b/dbflow-core/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' project.ext.artifactId = bt_name -targetCompatibility = JavaVersion.VERSION_1_7 -sourceCompatibility = JavaVersion.VERSION_1_7 +targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_1_8 apply from: '../java-artifacts.gradle' \ No newline at end of file diff --git a/dbflow-kotlinextensions/build.gradle b/dbflow-kotlinextensions/build.gradle index 4de4f19a1..f043ca71c 100644 --- a/dbflow-kotlinextensions/build.gradle +++ b/dbflow-kotlinextensions/build.gradle @@ -3,34 +3,32 @@ apply plugin: 'kotlin-android' project.ext.artifactId = bt_name - android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow.kotlinextensions" defaultConfig { minSdkVersion dbflow_min_sdk targetSdkVersion Integer.valueOf(dbflow_target_sdk) } buildTypes { - debug { - minifyEnabled false - } - release { - minifyEnabled false - } + debug { minifyEnabled false } + release { minifyEnabled false } } sourceSets { main.java.srcDirs += 'src/main/kotlin' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = '1.8' } } dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" api project("${dbflow_project_prefix}dbflow-core") api project("${dbflow_project_prefix}dbflow") } apply from: '../kotlin-artifacts.gradle' - - diff --git a/dbflow-kotlinextensions/src/main/AndroidManifest.xml b/dbflow-kotlinextensions/src/main/AndroidManifest.xml index e1050c1a7..b8b29fd52 100644 --- a/dbflow-kotlinextensions/src/main/AndroidManifest.xml +++ b/dbflow-kotlinextensions/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/dbflow-processor/build.gradle b/dbflow-processor/build.gradle index b78e31608..b641925bf 100644 --- a/dbflow-processor/build.gradle +++ b/dbflow-processor/build.gradle @@ -6,16 +6,22 @@ project.ext.artifactId = bt_name targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 +compileKotlin { kotlinOptions.jvmTarget = "1.8" } +compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } + dependencies { - compile project("${dbflow_project_prefix}dbflow-core") - compile 'com.squareup:javapoet:1.9.0' - compile 'com.github.agrosner:KPoet:1.0.0' - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + implementation project("${dbflow_project_prefix}dbflow-core") + implementation 'com.squareup:javapoet:1.13.0' + implementation 'com.github.agrosner:KPoet:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + // KAPT (current) compileOnly 'org.glassfish:javax.annotation:10.0-b28' - testImplementation 'junit:junit:4.12' + // KSP API — needed to implement a KSP SymbolProcessor + compileOnly "com.google.devtools.ksp:symbol-processing-api:${kotlin_version}-1.0.20" + testImplementation 'junit:junit:4.13.2' } apply from: '../java-artifacts.gradle' diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessor.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessor.kt new file mode 100644 index 000000000..fb7084da6 --- /dev/null +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessor.kt @@ -0,0 +1,293 @@ +package com.raizlabs.android.dbflow.processor + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.Origin +import com.raizlabs.android.dbflow.annotation.Database +import com.raizlabs.android.dbflow.annotation.ManyToMany +import com.raizlabs.android.dbflow.annotation.Migration +import com.raizlabs.android.dbflow.annotation.ModelView +import com.raizlabs.android.dbflow.annotation.QueryModel +import com.raizlabs.android.dbflow.annotation.Table +import com.raizlabs.android.dbflow.annotation.TypeConverter +import com.raizlabs.android.dbflow.annotation.provider.ContentProvider +import com.raizlabs.android.dbflow.annotation.provider.TableEndpoint +import com.raizlabs.android.dbflow.processor.definition.ContentProviderDefinition +import com.raizlabs.android.dbflow.processor.definition.DatabaseDefinition +import com.raizlabs.android.dbflow.processor.definition.DatabaseHolderDefinition +import com.raizlabs.android.dbflow.processor.definition.ManyToManyDefinition +import com.raizlabs.android.dbflow.processor.definition.MigrationDefinition +import com.raizlabs.android.dbflow.processor.definition.ModelViewDefinition +import com.raizlabs.android.dbflow.processor.definition.QueryModelDefinition +import com.raizlabs.android.dbflow.processor.definition.TableDefinition +import com.raizlabs.android.dbflow.processor.definition.TableEndpointDefinition +import com.raizlabs.android.dbflow.processor.definition.TypeConverterDefinition +import com.raizlabs.android.dbflow.processor.utils.WriterUtils + +class DBFlowSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + + private val manager: ProcessorManager = KspProcessorManager( + codeGenerator = environment.codeGenerator, + logger = environment.logger, + options = environment.options, + ) + + /** Qualified names of @Table-annotated classes processed in round 1. */ + private val processedTableQNames = mutableSetOf() + + /** Set to true after round 1 is complete. */ + private var firstRoundComplete = false + + /** Set to true once the aggregating database holder has been written. */ + private var holderWritten = false + + override fun process(resolver: Resolver): List { + if (!firstRoundComplete) { + firstRoundComplete = true + + processDefaultTypeConverters(resolver) + processTypeConverters(resolver) + processDatabases(resolver) + processTables(resolver) + processMigrations(resolver) + processModelViews(resolver) + processQueryModels(resolver) + processContentProviders(resolver) + processTableEndpoints(resolver) + processManyToMany(resolver) + + // Write per-class files (_Table, _Adapter, etc.) but NOT the aggregating database + // holder. The per-class file writes trigger another KSP round automatically, and by + // then all other processors (e.g. modelset-compiler) will have emitted their @Table + // classes — we can register every model in the database holder in that final round. + // + // Note: in KSP2 we cannot store KSAnnotated symbols across rounds (lifetime tokens are + // invalidated when the PSI changes). So we don't return deferred symbols; we rely on + // the file-write side effect to force a subsequent round. + writeDefinitions(writeDatabaseHolder = false) + } else if (!holderWritten) { + // A subsequent round was triggered by round-1 file writes. Pick up any @Table classes + // generated by other processors and write the aggregating database holder. + processNewTables(resolver) + processManyToMany(resolver) + writeDefinitions(writeDatabaseHolder = true) + holderWritten = true + } + return emptyList() + } + + private fun processDefaultTypeConverters(resolver: Resolver) { + DEFAULT_TYPE_CONVERTER_NAMES.forEach { qName -> + val ksClass = resolver.getClassDeclarationByName(resolver.getKSNameFromString(qName)) + ?: return@forEach + addTypeConverter(ksClass) + } + } + + private fun processTypeConverters(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(TypeConverter::class.qualifiedName!!) + .filterIsInstance() + .forEach { addTypeConverter(it) } + } + + private fun addTypeConverter(ksClass: KSClassDeclaration) { + val definition = TypeConverterDefinition.fromKsp(ksClass, manager) ?: return + if (manager.typeConverters.none { it.value.modelTypeName == definition.modelTypeName }) { + manager.addTypeConverterDefinition(definition) + } + } + + private fun processDatabases(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(Database::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = DatabaseDefinition(manager, KSP_SENTINEL_ELEMENT) + definition.kspInit(ksClass) + manager.addFlowManagerWriter(definition) + } + } + + private fun processTables(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(Table::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val qName = ksClass.qualifiedName?.asString() ?: return@forEach + if (processedTableQNames.add(qName)) { + val definition = TableDefinition(manager, KSP_SENTINEL_ELEMENT) + definition.kspInit(ksClass) + manager.addTableDefinition(definition) + } + } + } + + /** Processes only @Table classes not already handled in round 1 (i.e. generated join tables). */ + private fun processNewTables(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(Table::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val qName = ksClass.qualifiedName?.asString() ?: return@forEach + if (processedTableQNames.add(qName)) { + val definition = TableDefinition(manager, KSP_SENTINEL_ELEMENT) + definition.kspInit(ksClass) + manager.addTableDefinition(definition) + } + } + } + + private fun processMigrations(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(Migration::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = MigrationDefinition(manager, KSP_SENTINEL_ELEMENT) + definition.kspInit(ksClass) + manager.addMigrationDefinition(definition) + } + } + + private fun processModelViews(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(ModelView::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = ModelViewDefinition(manager, KSP_SENTINEL_ELEMENT) + definition.kspInit(ksClass) + manager.addModelViewDefinition(definition) + } + } + + private fun processQueryModels(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(QueryModel::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = QueryModelDefinition(KSP_SENTINEL_ELEMENT, manager) + definition.kspInit(ksClass) + manager.addQueryModelDefinition(definition) + } + } + + private fun processTableEndpoints(resolver: Resolver) { + val validator = TableEndpointValidator() + resolver.getSymbolsWithAnnotation(TableEndpoint::class.qualifiedName!!) + .filterIsInstance() + .filter { it.parentDeclaration == null } + .forEach { ksClass -> + val endpointDef = TableEndpointDefinition(KSP_SENTINEL_ELEMENT, manager) + endpointDef.kspInit(ksClass) + if (validator.validate(manager, endpointDef)) { + manager.putTableEndpointForProvider(endpointDef) + } + } + } + + private fun processContentProviders(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(ContentProvider::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = ContentProviderDefinition(KSP_SENTINEL_ELEMENT, manager) + definition.kspInit(ksClass) + if (definition.elementClassName != null) { + manager.addContentProviderDefinition(definition) + } + } + } + + /** + * Processes @ManyToMany-annotated classes, generates the join table model classes, + * and registers the definitions with the manager. + * + * @return true if any join table files were written (triggering round 2). + */ + private fun processManyToMany(resolver: Resolver): Boolean { + var wroteAny = false + resolver.getSymbolsWithAnnotation(ManyToMany::class.qualifiedName!!) + .filterIsInstance() + .forEach { ksClass -> + val definition = ManyToManyDefinition(KSP_SENTINEL_ELEMENT, manager) + definition.kspInit(ksClass) + manager.addManyToManyDefinition(definition) + } + + // Write all join table model classes now so round 2 can process their @Table annotation. + for (holderDef in manager.getDatabaseHolderDefinitionList()) { + val manyToManyMap = holderDef.manyToManyDefinitionMap + if (manyToManyMap.isEmpty()) continue + + val flatList = manyToManyMap.values.flatten().sortedBy { it.outputClassName?.simpleName() } + for (m2mDef in flatList) { + m2mDef.prepareForWrite() + WriterUtils.writeBaseDefinition(m2mDef, manager) + wroteAny = true + } + manyToManyMap.clear() + } + return wroteAny + } + + private fun writeDefinitions(writeDatabaseHolder: Boolean) { + val databaseHolderDefinitions = manager.getDatabaseHolderDefinitionList() + .sortedBy { it.databaseDefinition?.outputClassName?.simpleName() } + + for (holderDef in databaseHolderDefinitions) { + val dbDef = holderDef.databaseDefinition + if (dbDef == null) { + manager.logError(holderDef.getMissingDBRefs().joinToString("\n")) + continue + } + + dbDef.validateAndPrepareToWrite() + + // Write table/view/query adapters first so that checkNeedsReferences() runs and + // hasGlobalTypeConverters is accurate before the database registry file is written. + holderDef.tableDefinitionMap.values + .sortedBy { it.outputClassName?.simpleName() } + .forEach { WriterUtils.writeBaseDefinition(it, manager) } + + holderDef.modelViewDefinitionMap.values + .sortedByDescending { it.priority } + .forEach { WriterUtils.writeBaseDefinition(it, manager) } + + holderDef.queryModelDefinitionMap.values + .sortedBy { it.outputClassName?.simpleName() } + .forEach { WriterUtils.writeBaseDefinition(it, manager) } + + // Write content providers for this database + val validator = ContentProviderValidator() + holderDef.providerMap.values + .sortedBy { it.outputClassName?.simpleName() } + .forEach { cpDef -> + cpDef.prepareForWrite() + if (validator.validate(manager, cpDef)) { + WriterUtils.writeBaseDefinition(cpDef, manager) + } + } + + // Only write the database registry file in the final round (when the holder is also + // written), and after all adapters so hasGlobalTypeConverters is correct. + if (writeDatabaseHolder && dbDef.outputClassName != null) { + manager.writeJavaFile(dbDef.packageName, dbDef.typeSpec) + } + } + + if (writeDatabaseHolder) { + val holderDefinition = DatabaseHolderDefinition(manager) + if (!holderDefinition.isGarbage()) { + manager.writeJavaFile(ClassNames.FLOW_MANAGER_PACKAGE, holderDefinition.typeSpec) + } + } + } + + companion object { + private val DEFAULT_TYPE_CONVERTER_NAMES = listOf( + "com.raizlabs.android.dbflow.converter.CalendarConverter", + "com.raizlabs.android.dbflow.converter.BigDecimalConverter", + "com.raizlabs.android.dbflow.converter.BigIntegerConverter", + "com.raizlabs.android.dbflow.converter.DateConverter", + "com.raizlabs.android.dbflow.converter.SqlDateConverter", + "com.raizlabs.android.dbflow.converter.BooleanConverter", + "com.raizlabs.android.dbflow.converter.UUIDConverter", + "com.raizlabs.android.dbflow.converter.CharConverter" + ) + } +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessorProvider.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessorProvider.kt new file mode 100644 index 000000000..7ce3514de --- /dev/null +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/DBFlowSymbolProcessorProvider.kt @@ -0,0 +1,10 @@ +package com.raizlabs.android.dbflow.processor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class DBFlowSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = + DBFlowSymbolProcessor(environment) +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspProcessorManager.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspProcessorManager.kt new file mode 100644 index 000000000..5dbe8964e --- /dev/null +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspProcessorManager.kt @@ -0,0 +1,67 @@ +package com.raizlabs.android.dbflow.processor + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.KSFile +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import javax.annotation.processing.Messager +import javax.lang.model.util.Elements +import javax.lang.model.util.Types + +class KspProcessorManager( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, + options: Map = emptyMap(), +) : ProcessorManager(processingEnvironment = null) { + + override val messager: Messager = KspMessager(logger) + override val typeUtils: Types = NoOpTypes() + override val elements: Elements = NoOpElements() + override val options: Map = options + + /** Aggregating overload — used for outputs that depend on every source (e.g. the database holder). */ + override fun writeJavaFile(packageName: String, typeSpec: TypeSpec) { + writeFile(packageName, typeSpec, Dependencies.ALL_FILES) + } + + /** + * Isolating overload — when an [originatingFile] is provided, declare the output as + * depending only on that single source. KSP can then skip regeneration when unrelated files + * change. Falls back to aggregating semantics if [originatingFile] is null. + */ + override fun writeJavaFile(packageName: String, typeSpec: TypeSpec, originatingFile: KSFile?) { + // We deliberately ignore [originatingFile] and always declare aggregating dependencies. + // + // The natural optimisation would be `Dependencies(false, originatingFile)` for per-class + // outputs (e.g. `_Table.java` files), letting KSP skip regeneration when unrelated + // sources change. But our processor uses a deferred-holder multi-round strategy: + // round 1 writes per-class files, round 2 writes the aggregating database holder. + // Once we declare an isolating dep on a round-1 KSFile, KSP2 retains a tracking entry + // that holds the file's lifetime token. That token is invalidated when round 2 starts, + // and any subsequent aggregating write (including the holder) then fails with + // "PSI has changed since creation", leaving 0-byte files behind that break javac. + // + // Until KSP2 handles this gracefully, fall back to aggregating dependencies for every + // write. The perf regression vs. isolating outputs is the cost of correctness here. + writeFile(packageName, typeSpec, Dependencies.ALL_FILES) + } + + private fun writeFile(packageName: String, typeSpec: TypeSpec, deps: Dependencies) { + try { + val javaFile = JavaFile.builder(packageName, typeSpec).build() + val fileName = typeSpec.name ?: return + codeGenerator.createNewFile( + dependencies = deps, + packageName = packageName, + fileName = fileName, + extensionName = "java" + ).bufferedWriter().use { writer -> + javaFile.writeTo(writer) + } + } catch (e: Exception) { + logger.warn("KSP: failed to write ${typeSpec.name}: ${e.message}") + } + } +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspSentinels.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspSentinels.kt new file mode 100644 index 000000000..3818b98f3 --- /dev/null +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/KspSentinels.kt @@ -0,0 +1,127 @@ +package com.raizlabs.android.dbflow.processor + +import javax.annotation.processing.Filer +import javax.annotation.processing.Messager +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ElementVisitor +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.Name +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.TypeParameterElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.ArrayType +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ErrorType +import javax.lang.model.type.ExecutableType +import javax.lang.model.type.IntersectionType +import javax.lang.model.type.NoType +import javax.lang.model.type.NullType +import javax.lang.model.type.PrimitiveType +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.lang.model.type.TypeVariable +import javax.lang.model.type.TypeVisitor +import javax.lang.model.type.UnionType +import javax.lang.model.type.WildcardType +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +/** + * Sentinel element used during KSP processing so that parent constructors of definition classes + * can be invoked safely. KSP subclasses / kspInit() will override all relevant fields afterward. + */ +object KSP_SENTINEL_ELEMENT : Element { + override fun getKind() = ElementKind.CLASS + override fun getModifiers(): MutableSet = mutableSetOf() + override fun getSimpleName(): Name = EMPTY_NAME + override fun getEnclosingElement(): Element = this + override fun getEnclosedElements(): MutableList = mutableListOf() + override fun getAnnotation(annotationType: Class?): A? = null + override fun getAnnotationMirrors(): MutableList = mutableListOf() + override fun asType(): TypeMirror = KSP_SENTINEL_TYPE_MIRROR + override fun accept(v: ElementVisitor?, p: P): R = throw UnsupportedOperationException("KSP sentinel") + override fun getAnnotationsByType(annotationType: Class?): Array = + @Suppress("UNCHECKED_CAST") (emptyArray() as Array) +} + +/** Sentinel TypeMirror that safely represents "unknown type" in KSP mode. */ +object KSP_SENTINEL_TYPE_MIRROR : TypeMirror { + override fun getKind() = TypeKind.NONE + override fun getAnnotationMirrors(): MutableList = mutableListOf() + override fun getAnnotation(annotationType: Class?): A? = null + override fun getAnnotationsByType(annotationType: Class?): Array = + @Suppress("UNCHECKED_CAST") (emptyArray() as Array) + override fun accept(v: TypeVisitor?, p: P): R = v!!.visitUnknown(this, p) + override fun toString() = "KSP_SENTINEL" +} + +private object EMPTY_NAME : Name { + override fun contentEquals(cs: CharSequence?) = cs?.isEmpty() == true + override fun get(index: Int): Char = throw IndexOutOfBoundsException() + override val length: Int get() = 0 + override fun subSequence(start: Int, end: Int): CharSequence = "" + override fun toString() = "" +} + +// --------------------------------------------------------------------------- +// No-op javax implementations used by KspProcessorManager +// --------------------------------------------------------------------------- + +class NoOpElements : Elements { + override fun getPackageElement(name: CharSequence?): PackageElement? = null + override fun getTypeElement(name: CharSequence?): TypeElement? = null + override fun getElementValuesWithDefaults(a: javax.lang.model.element.AnnotationMirror?) = emptyMap() + override fun getDocComment(e: Element?) = null + override fun isDeprecated(e: Element?) = false + override fun getBinaryName(type: TypeElement?): Name = EMPTY_NAME + override fun getPackageOf(type: Element?): PackageElement? = null + override fun getAllMembers(type: TypeElement?): MutableList = mutableListOf() + override fun getAllAnnotationMirrors(e: Element?): MutableList = mutableListOf() + override fun hides(hider: Element?, hidden: Element?) = false + override fun overrides(overrider: ExecutableElement?, overridden: ExecutableElement?, type: TypeElement?) = false + override fun getConstantExpression(value: Any?) = value?.toString() ?: "null" + override fun printElements(w: java.io.Writer?, vararg elements: Element?) {} + override fun getName(cs: CharSequence?): Name = EMPTY_NAME + override fun isFunctionalInterface(type: TypeElement?) = false +} + +class NoOpTypes : Types { + override fun asElement(t: TypeMirror?): Element? = null + override fun isSameType(t1: TypeMirror?, t2: TypeMirror?) = false + override fun isSubtype(t1: TypeMirror?, t2: TypeMirror?) = false + override fun isAssignable(t1: TypeMirror?, t2: TypeMirror?) = false + override fun contains(t1: TypeMirror?, t2: TypeMirror?) = false + override fun isSubsignature(m1: ExecutableType?, m2: ExecutableType?) = false + override fun directSupertypes(t: TypeMirror?): MutableList = mutableListOf() + override fun erasure(t: TypeMirror?): TypeMirror? = t + override fun boxedClass(p: PrimitiveType?): TypeElement? = null + override fun unboxedType(t: TypeMirror?): PrimitiveType? = null + override fun capture(t: TypeMirror?): TypeMirror? = null + override fun getPrimitiveType(kind: TypeKind?): PrimitiveType? = null + override fun getNullType(): NullType? = null + override fun getNoType(kind: TypeKind?): NoType? = null + override fun getArrayType(componentType: TypeMirror?): ArrayType? = null + override fun getWildcardType(extendsBound: TypeMirror?, superBound: TypeMirror?): WildcardType? = null + override fun getDeclaredType(typeElem: TypeElement?, vararg typeArgs: TypeMirror?): DeclaredType? = null + override fun getDeclaredType(containing: DeclaredType?, typeElem: TypeElement?, vararg typeArgs: TypeMirror?): DeclaredType? = null + override fun asMemberOf(containing: DeclaredType?, element: Element?): TypeMirror? = null +} + +class KspMessager(private val logger: com.google.devtools.ksp.processing.KSPLogger) : Messager { + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?) { + val message = msg?.toString() ?: return + when (kind) { + Diagnostic.Kind.ERROR -> logger.error(message) + Diagnostic.Kind.WARNING, Diagnostic.Kind.MANDATORY_WARNING -> logger.warn(message) + else -> logger.info(message) + } + } + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?) = printMessage(kind, msg) + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?, a: AnnotationMirror?) = printMessage(kind, msg) + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?, a: AnnotationMirror?, v: javax.lang.model.element.AnnotationValue?) = printMessage(kind, msg) +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/ProcessorManager.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/ProcessorManager.kt index 4f93685e6..cda0ea46f 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/ProcessorManager.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/ProcessorManager.kt @@ -14,7 +14,6 @@ import com.raizlabs.android.dbflow.processor.definition.TableEndpointDefinition import com.raizlabs.android.dbflow.processor.definition.TypeConverterDefinition import com.raizlabs.android.dbflow.processor.utils.WriterUtils import com.squareup.javapoet.ClassName -import com.squareup.javapoet.JavaFile import com.squareup.javapoet.TypeName import java.io.IOException import java.util.* @@ -32,7 +31,7 @@ import kotlin.reflect.KClass * Description: The main object graph during processing. This class collects all of the * processor classes and writes them to the corresponding database holders. */ -class ProcessorManager internal constructor(val processingEnvironment: ProcessingEnvironment) : Handler { +open class ProcessorManager internal constructor(val processingEnvironment: ProcessingEnvironment? = null) : Handler { companion object { lateinit var manager: ProcessorManager @@ -55,11 +54,46 @@ class ProcessorManager internal constructor(val processingEnvironment: Processin containerHandlers.forEach { handlers.add(it) } } - val messager: Messager = processingEnvironment.messager + /** Overridable file-writing abstraction; KSP subclass supplies a CodeGenerator-backed writer. */ + open fun writeJavaFile(packageName: String, typeSpec: com.squareup.javapoet.TypeSpec) { + if (processingEnvironment == null) return + try { + com.squareup.javapoet.JavaFile.builder(packageName, typeSpec).build() + .writeTo(processingEnvironment.filer) + } catch (e: java.io.IOException) { /* ignored */ } + catch (e: FilerException) { /* already written */ } + } + + /** + * KSP-aware overload. When [originatingFile] is non-null, the KSP subclass uses it to declare + * an isolating [com.google.devtools.ksp.processing.Dependencies], letting KSP skip + * regenerating outputs whose source class hasn't changed. The KAPT path ignores it. + */ + open fun writeJavaFile( + packageName: String, + typeSpec: com.squareup.javapoet.TypeSpec, + originatingFile: com.google.devtools.ksp.symbol.KSFile? + ) { + // Default falls through to the aggregating overload — the KSP subclass overrides this. + writeJavaFile(packageName, typeSpec) + } - val typeUtils: Types = processingEnvironment.typeUtils + open val messager: Messager = processingEnvironment?.messager ?: SilentMessager - val elements: Elements = processingEnvironment.elementUtils + open val typeUtils: Types = processingEnvironment?.typeUtils ?: NoOpTypes() + + open val elements: Elements = processingEnvironment?.elementUtils ?: NoOpElements() + + /** Processor-pipeline options. KAPT reads them from the [processingEnvironment]; KSP supplies + * them via the [SymbolProcessorEnvironment.options] map and overrides this. */ + open val options: Map = processingEnvironment?.options.orEmpty() + + private object SilentMessager : Messager { + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?) {} + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?) {} + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?, a: javax.lang.model.element.AnnotationMirror?) {} + override fun printMessage(kind: Diagnostic.Kind?, msg: CharSequence?, e: Element?, a: javax.lang.model.element.AnnotationMirror?, v: javax.lang.model.element.AnnotationValue?) {} + } fun addDatabase(database: TypeName) { if (!uniqueDatabases.contains(database)) { @@ -296,8 +330,7 @@ class ProcessorManager internal constructor(val processingEnvironment: Processin if (roundEnvironment.processingOver()) { databaseHolderDefinition.databaseDefinition?.let { if (it.outputClassName != null) { - JavaFile.builder(it.packageName, it.typeSpec).build() - .writeTo(processorManager.processingEnvironment.filer) + processorManager.writeJavaFile(it.packageName, it.typeSpec) } } } @@ -317,24 +350,26 @@ class ProcessorManager internal constructor(val processingEnvironment: Processin .sortedBy { it.outputClassName?.simpleName() } queryModelDefinitions.forEach { WriterUtils.writeBaseDefinition(it, processorManager) } - tableDefinitions.forEach { - try { - it.writePackageHelper(processorManager.processingEnvironment) - } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + if (processorManager.processingEnvironment != null) { + tableDefinitions.forEach { + try { + it.writePackageHelper(processorManager.processingEnvironment) + } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + } } - } - modelViewDefinitions.forEach { - try { - it.writePackageHelper(processorManager.processingEnvironment) - } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + modelViewDefinitions.forEach { + try { + it.writePackageHelper(processorManager.processingEnvironment) + } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + } } - } - queryModelDefinitions.forEach { - try { - it.writePackageHelper(processorManager.processingEnvironment) - } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + queryModelDefinitions.forEach { + try { + it.writePackageHelper(processorManager.processingEnvironment) + } catch (e: FilerException) { /*Ignored intentionally to allow multi-round table generation*/ + } } } } catch (e: IOException) { @@ -345,9 +380,7 @@ class ProcessorManager internal constructor(val processingEnvironment: Processin try { val databaseHolderDefinition = DatabaseHolderDefinition(processorManager) if (!databaseHolderDefinition.isGarbage()) { - JavaFile.builder(ClassNames.FLOW_MANAGER_PACKAGE, - databaseHolderDefinition.typeSpec).build() - .writeTo(processorManager.processingEnvironment.filer) + processorManager.writeJavaFile(ClassNames.FLOW_MANAGER_PACKAGE, databaseHolderDefinition.typeSpec) } } catch (e: FilerException) { } catch (e: IOException) { diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/BaseDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/BaseDefinition.kt index c9fc808e4..7c0b08dc5 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/BaseDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/BaseDefinition.kt @@ -34,6 +34,17 @@ abstract class BaseDefinition : TypeDefinition { var outputClassName: ClassName? = null var erasedTypeName: TypeName? = null + /** Set to true once this definition's file has been emitted, so a second write attempt is a no-op. */ + internal var fileWritten = false + + /** + * KSP source file that this definition was generated from. Used to declare per-class outputs + * as isolating ([com.google.devtools.ksp.processing.Dependencies] with `aggregating=false`), + * so KSP can skip regenerating files whose source hasn't changed. Null for KAPT path or for + * aggregating outputs (e.g. the database holder). + */ + internal var originatingFile: com.google.devtools.ksp.symbol.KSFile? = null + var element: Element var typeElement: TypeElement? = null var elementName: String @@ -74,11 +85,19 @@ abstract class BaseDefinition : TypeDefinition { typeMirror = element.asType() elementTypeName = typeMirror.typeName } + // KSP sentinel returns TypeKind.NONE which TypeName.get() cannot handle – fall back + if (elementTypeName == null) elementTypeName = com.squareup.javapoet.TypeName.OBJECT val erasedType = processorManager.typeUtils.erasure(typeMirror) erasedTypeName = TypeName.get(erasedType) } catch (i: IllegalArgumentException) { - manager.logError("Found illegal type: ${element.asType()} for ${element.simpleName}") + elementTypeName = com.squareup.javapoet.TypeName.OBJECT + manager.logError("Found illegal type: ${element.simpleName}") manager.logError("Exception here: $i") + } catch (e: UnsupportedOperationException) { + // KSP sentinel – type info will be supplied via kspInit() + elementTypeName = com.squareup.javapoet.TypeName.OBJECT + } catch (e: Exception) { + elementTypeName = com.squareup.javapoet.TypeName.OBJECT } elementName = element.simpleName.toString() diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ContentProvider.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ContentProvider.kt index b66d3245b..9efe30c11 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ContentProvider.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ContentProvider.kt @@ -14,17 +14,30 @@ import com.grosner.kpoet.param import com.grosner.kpoet.parameterized import com.grosner.kpoet.public import com.grosner.kpoet.statement +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.raizlabs.android.dbflow.annotation.provider.ContentProvider import com.raizlabs.android.dbflow.annotation.provider.ContentUri import com.raizlabs.android.dbflow.annotation.provider.Notify import com.raizlabs.android.dbflow.annotation.provider.TableEndpoint import com.raizlabs.android.dbflow.processor.ClassNames +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.TableEndpointValidator import com.raizlabs.android.dbflow.processor.utils.`override fun` import com.raizlabs.android.dbflow.processor.utils.annotation import com.raizlabs.android.dbflow.processor.utils.controlFlow +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getArrayArgument +import com.raizlabs.android.dbflow.processor.utils.getIntArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.ArrayTypeName import com.squareup.javapoet.ClassName import com.squareup.javapoet.CodeBlock @@ -473,6 +486,29 @@ class ContentProviderDefinition(typeElement: Element, processorManager: Processo .forEach { typeBuilder.addMethod(it) } } + fun kspInit(ksClass: KSClassDeclaration) { + elementName = ksClass.simpleName.asString() + packageName = ksClass.packageName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + originatingFile = ksClass.containingFile + + val cpAnnot = ksClass.findKspAnnotation() ?: return + authority = cpAnnot.getStringArgument("authority") ?: "" + databaseTypeName = cpAnnot.getKsTypeArgument("database")?.toJavaPoetTypeName() + + val validator = TableEndpointValidator() + for (decl in ksClass.declarations.filterIsInstance()) { + if (decl.findKspAnnotation() != null) { + val endpointDef = TableEndpointDefinition(KSP_SENTINEL_ELEMENT, manager) + endpointDef.kspInit(decl) + if (validator.validate(manager, endpointDef)) { + endpointDefinitions.add(endpointDef) + } + } + } + } + companion object { internal val DEFINITION_NAME = "Provider" @@ -529,4 +565,47 @@ class ContentUriDefinition(typeElement: Element, processorManager: ProcessorMana override fun getElementClassName(element: Element?): ClassName? { return null } + + companion object { + fun fromKsp(decl: KSDeclaration, processorManager: ProcessorManager): ContentUriDefinition { + val def = ContentUriDefinition(KSP_SENTINEL_ELEMENT, processorManager) + + val enclosingName = decl.parentDeclaration?.simpleName?.asString() ?: "" + def.name = "${enclosingName}_${decl.simpleName.asString()}" + + val annot = decl.findKspAnnotation() + if (annot != null) { + def.path = annot.getStringArgument("path") ?: "" + def.type = annot.getStringArgument("type") ?: "" + def.queryEnabled = annot.getBooleanArgument("queryEnabled") ?: true + def.insertEnabled = annot.getBooleanArgument("insertEnabled") ?: true + def.deleteEnabled = annot.getBooleanArgument("deleteEnabled") ?: true + def.updateEnabled = annot.getBooleanArgument("updateEnabled") ?: true + + val segAnnotations = annot.getArrayArgument("segments") ?: emptyList() + def.segments = segAnnotations.mapNotNull { segAnnot -> + val col = segAnnot.getStringArgument("column") ?: return@mapNotNull null + val seg = segAnnot.getIntArgument("segment") ?: return@mapNotNull null + createPathSegment(col, seg) + }.toTypedArray() + } + return def + } + + private fun createPathSegment(column: String, segment: Int): ContentUri.PathSegment = + java.lang.reflect.Proxy.newProxyInstance( + ContentUri.PathSegment::class.java.classLoader, + arrayOf(ContentUri.PathSegment::class.java) + ) { _, method, _ -> + when (method.name) { + "column" -> column + "segment" -> segment + "annotationType" -> ContentUri.PathSegment::class.java + "hashCode" -> 31 * column.hashCode() + segment + "equals" -> false + "toString" -> "@PathSegment(column=\"$column\", segment=$segment)" + else -> null + } + } as ContentUri.PathSegment + } } \ No newline at end of file diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseDefinition.kt index c669a37a7..78114347a 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseDefinition.kt @@ -9,6 +9,7 @@ import com.grosner.kpoet.modifiers import com.grosner.kpoet.param import com.grosner.kpoet.public import com.grosner.kpoet.statement +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.processor.ClassNames @@ -17,6 +18,12 @@ import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.TableValidator import com.raizlabs.android.dbflow.processor.utils.`override fun` import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.getIntArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName import com.squareup.javapoet.ClassName import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeName @@ -81,6 +88,39 @@ class DatabaseDefinition(manager: ProcessorManager, element: Element) : BaseDefi } } + /** + * Initialises this definition from a KSP [KSClassDeclaration]. Called instead of the KAPT + * annotation-reading path that runs during the normal constructor via [element]. + */ + fun kspInit(ksClass: KSClassDeclaration) { + elementName = ksClass.simpleName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + // DatabaseDefinition always lives in the flow manager package + packageName = ClassNames.FLOW_MANAGER_PACKAGE + + val annot = ksClass.findKspAnnotation() ?: return + + databaseName = annot.getStringArgument("name") ?: "" + databaseExtensionName = annot.getStringArgument("databaseExtension") ?: "" + inMemory = annot.getBooleanArgument("inMemory") ?: false + databaseClassName = elementName + consistencyChecksEnabled = annot.getBooleanArgument("consistencyCheckEnabled") ?: false + backupEnabled = annot.getBooleanArgument("backupEnabled") ?: false + classSeparator = annot.getStringArgument("generatedClassSeparator") ?: "_" + fieldRefSeparator = classSeparator + setOutputClassName(databaseClassName + classSeparator + "Database") + databaseVersion = annot.getIntArgument("version") ?: 1 + foreignKeysSupported = annot.getBooleanArgument("foreignKeyConstraintsEnforced") ?: false + + insertConflict = annot.getEnumArgument("insertConflict") + ?.let { runCatching { ConflictAction.valueOf(it) }.getOrNull() } + ?: ConflictAction.NONE + updateConflict = annot.getEnumArgument("updateConflict") + ?.let { runCatching { ConflictAction.valueOf(it) }.getOrNull() } + ?: ConflictAction.NONE + } + override val extendsClass: TypeName? = ClassNames.BASE_DATABASE_DEFINITION_CLASSNAME override fun onWriteDefinition(typeBuilder: TypeSpec.Builder) { diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseHolderDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseHolderDefinition.kt index fb55ff3f8..33178fb66 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseHolderDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/DatabaseHolderDefinition.kt @@ -16,7 +16,7 @@ class DatabaseHolderDefinition(private val processorManager: ProcessorManager) : init { - val options = this.processorManager.processingEnvironment.options + val options = this.processorManager.options if (options.containsKey(OPTION_TARGET_MODULE_NAME)) { className = options[OPTION_TARGET_MODULE_NAME] ?: "" } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ManyToManyDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ManyToManyDefinition.kt index 256c1d112..fb6abf4db 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ManyToManyDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ManyToManyDefinition.kt @@ -1,6 +1,7 @@ package com.raizlabs.android.dbflow.processor.definition import com.grosner.kpoet.* +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.ForeignKey import com.raizlabs.android.dbflow.annotation.ManyToMany import com.raizlabs.android.dbflow.annotation.PrimaryKey @@ -8,12 +9,20 @@ import com.raizlabs.android.dbflow.annotation.Table import com.raizlabs.android.dbflow.processor.ClassNames import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty import com.raizlabs.android.dbflow.processor.utils.lower +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.processor.utils.toTypeElement import com.squareup.javapoet.AnnotationSpec +import com.squareup.javapoet.ClassName import com.squareup.javapoet.TypeName import com.squareup.javapoet.TypeSpec +import javax.lang.model.element.Element import javax.lang.model.element.TypeElement import javax.lang.model.type.MirroredTypeException import javax.lang.model.type.TypeMirror @@ -21,55 +30,83 @@ import javax.lang.model.type.TypeMirror /** * Description: Generates the Model class that is used in a many to many. */ -class ManyToManyDefinition(element: TypeElement, processorManager: ProcessorManager, - manyToMany: ManyToMany = element.annotation()!!) +class ManyToManyDefinition(element: Element, processorManager: ProcessorManager, + manyToMany: ManyToMany? = (element as? TypeElement)?.annotation()) : BaseDefinition(element, processorManager) { - internal var referencedTable: TypeName + internal var referencedTable: TypeName = TypeName.OBJECT var databaseTypeName: TypeName? = null internal var generateAutoIncrement: Boolean = false internal var sameTableReferenced: Boolean = false - internal val generatedTableClassName = manyToMany.generatedTableClassName + internal var generatedTableClassName: String = "" internal var saveForeignKeyModels: Boolean = false - internal val thisColumnName = manyToMany.thisTableColumnName - internal val referencedColumnName = manyToMany.referencedTableColumnName + internal var thisColumnName: String = "" + internal var referencedColumnName: String = "" init { - - var clazz: TypeMirror? = null - try { - manyToMany.referencedTable - } catch (mte: MirroredTypeException) { - clazz = mte.typeMirror - } - referencedTable = TypeName.get(clazz) - generateAutoIncrement = manyToMany.generateAutoIncrement - saveForeignKeyModels = manyToMany.saveForeignKeyModels - - sameTableReferenced = referencedTable == elementTypeName - - element.annotation()?.let { table -> + if (manyToMany != null) { + var clazz: TypeMirror? = null try { - table.database + manyToMany.referencedTable } catch (mte: MirroredTypeException) { - databaseTypeName = TypeName.get(mte.typeMirror) + clazz = mte.typeMirror + } + referencedTable = if (clazz != null) TypeName.get(clazz) else TypeName.OBJECT + generateAutoIncrement = manyToMany.generateAutoIncrement + saveForeignKeyModels = manyToMany.saveForeignKeyModels + generatedTableClassName = manyToMany.generatedTableClassName + thisColumnName = manyToMany.thisTableColumnName + referencedColumnName = manyToMany.referencedTableColumnName + + sameTableReferenced = referencedTable == elementTypeName + + (element as? TypeElement)?.annotation
()?.let { table -> + try { + table.database + } catch (mte: MirroredTypeException) { + databaseTypeName = TypeName.get(mte.typeMirror) + } } - } - if (!thisColumnName.isNullOrEmpty() && !referencedColumnName.isNullOrEmpty() - && thisColumnName == referencedColumnName) { - manager.logError(ManyToManyDefinition::class, "The thisTableColumnName and referenceTableColumnName cannot be the same") + if (!thisColumnName.isNullOrEmpty() && !referencedColumnName.isNullOrEmpty() + && thisColumnName == referencedColumnName) { + manager.logError(ManyToManyDefinition::class, "The thisTableColumnName and referenceTableColumnName cannot be the same") + } } } + fun kspInit(ksClass: KSClassDeclaration) { + elementName = ksClass.simpleName.asString() + packageName = ksClass.packageName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + originatingFile = ksClass.containingFile + + val annot = ksClass.findKspAnnotation() ?: return + + referencedTable = annot.getKsTypeArgument("referencedTable")?.toJavaPoetTypeName() ?: TypeName.OBJECT + generateAutoIncrement = annot.getBooleanArgument("generateAutoIncrement") ?: true + saveForeignKeyModels = annot.getBooleanArgument("saveForeignKeyModels") ?: false + generatedTableClassName = annot.getStringArgument("generatedTableClassName") ?: "" + thisColumnName = annot.getStringArgument("thisTableColumnName") ?: "" + referencedColumnName = annot.getStringArgument("referencedTableColumnName") ?: "" + + databaseTypeName = ksClass.findKspAnnotation
()?.getKsTypeArgument("database")?.toJavaPoetTypeName() + + sameTableReferenced = referencedTable == elementTypeName + } + fun prepareForWrite() { val databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition if (databaseDefinition == null) { manager.logError("DatabaseDefinition was null for : $elementName") } else { if (generatedTableClassName.isNullOrEmpty()) { - val referencedOutput = getElementClassName(referencedTable.toTypeElement(manager)) - setOutputClassName(databaseDefinition.classSeparator + referencedOutput?.simpleName()) + // Try ClassName.simpleName() first (works in both KSP and KAPT). + // Fall back to KAPT TypeElement lookup only if needed. + val simpleName = (referencedTable as? ClassName)?.simpleName() + ?: getElementClassName(referencedTable.toTypeElement(manager))?.simpleName() + setOutputClassName(databaseDefinition.classSeparator + simpleName) } else { setOutputClassNameFull(generatedTableClassName) } @@ -130,4 +167,4 @@ class ManyToManyDefinition(element: TypeElement, processorManager: ProcessorMana } } } -} \ No newline at end of file +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/MigrationDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/MigrationDefinition.kt index 14d521abf..8e0cc064a 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/MigrationDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/MigrationDefinition.kt @@ -1,14 +1,23 @@ package com.raizlabs.android.dbflow.processor.definition import com.grosner.kpoet.typeName +import com.google.devtools.ksp.getConstructors +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.Migration +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getIntArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.ClassName import com.squareup.javapoet.CodeBlock import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeName +import javax.lang.model.element.Element import javax.lang.model.element.ExecutableElement import javax.lang.model.element.TypeElement import javax.lang.model.type.MirroredTypeException @@ -16,8 +25,8 @@ import javax.lang.model.type.MirroredTypeException /** * Description: Used in holding data about migration files. */ -class MigrationDefinition(processorManager: ProcessorManager, typeElement: TypeElement) - : BaseDefinition(typeElement, processorManager) { +class MigrationDefinition(processorManager: ProcessorManager, element: Element) + : BaseDefinition(element, processorManager) { var databaseName: TypeName? = null @@ -31,10 +40,11 @@ class MigrationDefinition(processorManager: ProcessorManager, typeElement: TypeE init { setOutputClassName("") - val migration = typeElement.annotation() - if (migration == null) { - processorManager.logError("Migration was null for:" + typeElement) - } else { + val typeEl = element as? TypeElement + val migration = typeEl?.annotation() + if (migration == null && element !== KSP_SENTINEL_ELEMENT) { + processorManager.logError("Migration was null for: $element") + } else if (migration != null) { try { migration.database } catch (mte: MirroredTypeException) { @@ -44,19 +54,19 @@ class MigrationDefinition(processorManager: ProcessorManager, typeElement: TypeE version = migration.version priority = migration.priority - val elements = typeElement.enclosedElements - elements.forEach { element -> - if (element is ExecutableElement && element.simpleName.toString() == "") { + val enclosed = typeEl?.enclosedElements ?: emptyList() + enclosed.forEach { enclosed -> + if (enclosed is ExecutableElement && enclosed.simpleName.toString() == "") { if (!constructorName.isNullOrEmpty()) { manager.logError(MigrationDefinition::class, "Migrations cannot have more than one constructor. " + "They can only have an Empty() or single-parameter constructor Empty(Empty.class) that specifies " + "the .class of this migration class.") } - if (element.parameters.isEmpty()) { + if (enclosed.parameters.isEmpty()) { constructorName = "()" - } else if (element.parameters.size == 1) { - val params = element.parameters + } else if (enclosed.parameters.size == 1) { + val params = enclosed.parameters val param = params[0] val type = param.asType().typeName @@ -64,7 +74,7 @@ class MigrationDefinition(processorManager: ProcessorManager, typeElement: TypeE val containedType = type.typeArguments[0] constructorName = CodeBlock.of("(\$T.class)", containedType).toString() } else { - manager.logError(MigrationDefinition::class, "Wrong parameter type found for $typeElement. Found $type but required ModelClass.class") + manager.logError(MigrationDefinition::class, "Wrong parameter type found for $element. Found $type but required ModelClass.class") } } } @@ -72,4 +82,37 @@ class MigrationDefinition(processorManager: ProcessorManager, typeElement: TypeE } } + fun kspInit(ksClass: KSClassDeclaration) { + elementName = ksClass.simpleName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + packageName = ksClass.packageName.asString() + + val annot = ksClass.findKspAnnotation() ?: return + + val dbKsType = annot.getKsTypeArgument("database") + databaseName = dbKsType?.toJavaPoetTypeName() + + version = annot.getIntArgument("version") ?: 0 + priority = annot.arguments.find { it.name?.asString() == "priority" }?.value as? Int ?: -1 + + val constructors = ksClass.getConstructors().toList() + for (ctor in constructors) { + if (!constructorName.isNullOrEmpty()) { + manager.logError(MigrationDefinition::class, "Migrations cannot have more than one constructor.") + } + if (ctor.parameters.isEmpty()) { + constructorName = "()" + } else if (ctor.parameters.size == 1) { + val paramType = ctor.parameters[0].type.resolve().toJavaPoetTypeName() + if (paramType is ParameterizedTypeName && paramType.rawType == ClassName.get(Class::class.java)) { + val containedType = paramType.typeArguments[0] + constructorName = CodeBlock.of("(\$T.class)", containedType).toString() + } else { + manager.logError(MigrationDefinition::class, "Wrong parameter type for $elementName: found $paramType but required Class") + } + } + } + } + } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ModelViewDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ModelViewDefinition.kt index d5d6eabfc..4e91609b8 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ModelViewDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/ModelViewDefinition.kt @@ -7,12 +7,15 @@ import com.grosner.kpoet.`return` import com.grosner.kpoet.final import com.grosner.kpoet.modifiers import com.grosner.kpoet.public +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.Column import com.raizlabs.android.dbflow.annotation.ColumnMap import com.raizlabs.android.dbflow.annotation.ModelView import com.raizlabs.android.dbflow.annotation.ModelViewQuery import com.raizlabs.android.dbflow.processor.ClassNames import com.raizlabs.android.dbflow.processor.ColumnValidator +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.column.ColumnDefinition import com.raizlabs.android.dbflow.processor.definition.column.ReferenceColumnDefinition @@ -20,9 +23,15 @@ import com.raizlabs.android.dbflow.processor.utils.ElementUtility import com.raizlabs.android.dbflow.processor.utils.`override fun` import com.raizlabs.android.dbflow.processor.utils.annotation import com.raizlabs.android.dbflow.processor.utils.ensureVisibleStatic +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.implementsClass import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty import com.raizlabs.android.dbflow.processor.utils.simpleString +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.processor.utils.toTypeErasedElement import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeName @@ -36,7 +45,7 @@ import javax.lang.model.type.MirroredTypeException */ class ModelViewDefinition(manager: ProcessorManager, element: Element) : BaseTableDefinition(element, manager) { - internal val implementsLoadFromCursorListener: Boolean + internal var implementsLoadFromCursorListener: Boolean = false private var queryFieldName: String? = null @@ -49,6 +58,11 @@ class ModelViewDefinition(manager: ProcessorManager, element: Element) : BaseTab var priority: Int = 0 + internal var kspMode = false + internal var ksClassDeclaration: KSClassDeclaration? = null + + private var preparedKspWrite = false + init { element.annotation()?.let { modelView -> @@ -67,20 +81,50 @@ class ModelViewDefinition(manager: ProcessorManager, element: Element) : BaseTab this.priority = modelView.priority } - if (element is TypeElement) { - implementsLoadFromCursorListener = element.implementsClass(manager.processingEnvironment, - ClassNames.LOAD_FROM_CURSOR_LISTENER) + implementsLoadFromCursorListener = if (element is TypeElement) { + element.implementsClass(manager.processingEnvironment, ClassNames.LOAD_FROM_CURSOR_LISTENER) } else { - implementsLoadFromCursorListener = false + false } } + fun kspInit(ksClass: KSClassDeclaration) { + kspMode = true + ksClassDeclaration = ksClass + originatingFile = ksClass.containingFile + + elementName = ksClass.simpleName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + packageName = ksClass.packageName.asString() + + val annot = ksClass.findKspAnnotation() ?: return + + val dbKsType = annot.getKsTypeArgument("database") + databaseTypeName = dbKsType?.toJavaPoetTypeName() + + allFields = annot.getBooleanArgument("allFields") ?: false + name = annot.getStringArgument("name").takeIf { !it.isNullOrEmpty() } ?: elementName + priority = annot.arguments.find { it.name?.asString() == "priority" }?.value as? Int ?: 0 + + val superTypes = ksClass.getAllSuperTypes().map { it.declaration.qualifiedName?.asString() } + implementsLoadFromCursorListener = ClassNames.LOAD_FROM_CURSOR_LISTENER.toString() in superTypes + } + override fun prepareForWrite() { + if (kspMode && preparedKspWrite) return + classElementLookUpMap.clear() columnDefinitions.clear() queryFieldName = null + if (kspMode) { + prepareForWriteKsp() + preparedKspWrite = true + return + } + val modelView = element.getAnnotation(ModelView::class.java) if (modelView != null) { databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition @@ -92,6 +136,92 @@ class ModelViewDefinition(manager: ProcessorManager, element: Element) : BaseTab } } + private fun prepareForWriteKsp() { + val ksClass = ksClassDeclaration ?: return + databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition + if (databaseDefinition == null) { + manager.logError("DatabaseDefinition was null for KSP ModelView: $elementName") + return + } + setOutputClassName("${databaseDefinition?.classSeparator}ViewTable") + createColumnDefinitionsFromKsp(ksClass) + } + + private fun createColumnDefinitionsFromKsp(ksClass: KSClassDeclaration) { + for (property in ksClass.getAllProperties()) { + val name = property.simpleName.asString() + classElementLookUpMap[name] = KSP_SENTINEL_ELEMENT + val cap = name.replaceFirstChar { it.uppercase() } + classElementLookUpMap["get$cap"] = KSP_SENTINEL_ELEMENT + classElementLookUpMap["set$cap"] = KSP_SENTINEL_ELEMENT + if (name.startsWith("is", ignoreCase = true)) { + val withoutIs = name.removePrefix("is").removePrefix("Is") + .replaceFirstChar { it.uppercase() } + classElementLookUpMap["set$withoutIs"] = KSP_SENTINEL_ELEMENT + } + } + for (function in ksClass.declarations.filterIsInstance()) { + classElementLookUpMap[function.simpleName.asString()] = KSP_SENTINEL_ELEMENT + } + + val columnValidator = ColumnValidator() + + // Check companion object for @ModelViewQuery (Kotlin companion object pattern) + val companion = ksClass.declarations + .filterIsInstance() + .find { it.isCompanionObject } + companion?.declarations?.filterIsInstance() + ?.find { it.findKspAnnotation() != null } + ?.let { queryProp -> + if (!queryFieldName.isNullOrEmpty()) { + manager.logError("Found duplicate queryField name: $queryFieldName for $elementClassName") + } + queryFieldName = queryProp.simpleName.asString() + } + + // Check direct declarations for Java static fields with @ModelViewQuery + if (queryFieldName.isNullOrEmpty()) { + ksClass.declarations.filterIsInstance() + .find { it.findKspAnnotation() != null } + ?.let { queryProp -> + queryFieldName = queryProp.simpleName.asString() + } + } + + for (property in ksClass.getAllProperties()) { + val propName = property.simpleName.asString() + + if (property.findKspAnnotation() != null) { + if (!queryFieldName.isNullOrEmpty()) { + manager.logError("Found duplicate queryField name: $queryFieldName for $elementClassName") + } + queryFieldName = propName + continue + } + + val hasColumn = property.findKspAnnotation() != null + val isAllFieldsCandidate = allFields && + com.google.devtools.ksp.symbol.Modifier.PRIVATE !in property.modifiers && + com.google.devtools.ksp.symbol.Modifier.JAVA_STATIC !in property.modifiers + + if (!hasColumn && !isAllFieldsCandidate) continue + + val colDef = ColumnDefinition(manager, KSP_SENTINEL_ELEMENT, this, false) + colDef.kspInit(property) + + if (columnValidator.validate(manager, colDef)) { + columnDefinitions.add(colDef) + if (colDef.isPrimaryKey || colDef.isPrimaryKeyAutoIncrement || colDef.isRowId) { + manager.logError("ModelView $elementName cannot have primary keys") + } + } + } + + if (queryFieldName.isNullOrEmpty()) { + manager.logError("$elementClassName is missing the @ModelViewQuery field.") + } + } + override fun createColumnDefinitions(typeElement: TypeElement) { val variableElements = ElementUtility.getAllElements(typeElement, manager) diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/NotifyDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/NotifyDefinition.kt index dff447d93..6b1094605 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/NotifyDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/NotifyDefinition.kt @@ -1,9 +1,16 @@ package com.raizlabs.android.dbflow.processor.definition +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.raizlabs.android.dbflow.annotation.provider.Notify import com.raizlabs.android.dbflow.processor.ClassNames +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getArrayArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.ClassName import javax.lang.model.element.Element import javax.lang.model.element.ExecutableElement @@ -17,63 +24,123 @@ class NotifyDefinition(typeElement: Element, processorManager: ProcessorManager) var paths = arrayOf() var method = Notify.Method.DELETE - val parent = (typeElement.enclosingElement as TypeElement).qualifiedName.toString() - val methodName = typeElement.simpleName.toString() - var params: String + var parent: String = (typeElement.enclosingElement as? TypeElement)?.qualifiedName?.toString() ?: "" + var methodName: String = typeElement.simpleName.toString() + var params: String = "" var returnsArray: Boolean = false var returnsSingle: Boolean = false init { - - typeElement.annotation()?.let { notify -> - paths = notify.paths - method = notify.method - } - - val executableElement = typeElement as ExecutableElement - - val parameters = executableElement.parameters - val paramsBuilder = StringBuilder() - var first = true - parameters.forEach { param -> - if (first) { - first = false - } else { - paramsBuilder.append(", ") + if (typeElement !== KSP_SENTINEL_ELEMENT) { + typeElement.annotation()?.let { notify -> + paths = notify.paths + method = notify.method } - val paramType = param.asType() - val typeAsString = paramType.toString() - paramsBuilder.append( - if ("android.content.Context" == typeAsString) { - "getContext()" - } else if ("android.net.Uri" == typeAsString) { - "uri" - } else if ("android.content.ContentValues" == typeAsString) { - "values" - } else if ("long" == typeAsString) { - "id" - } else if ("java.lang.String" == typeAsString) { - "where" - } else if ("java.lang.String[]" == typeAsString) { - "whereArgs" + + val executableElement = typeElement as? ExecutableElement + if (executableElement != null) { + val parameters = executableElement.parameters + val paramsBuilder = StringBuilder() + var first = true + parameters.forEach { param -> + if (first) { + first = false } else { - "" - }) - } + paramsBuilder.append(", ") + } + val paramType = param.asType() + val typeAsString = paramType.toString() + paramsBuilder.append( + if ("android.content.Context" == typeAsString) { + "getContext()" + } else if ("android.net.Uri" == typeAsString) { + "uri" + } else if ("android.content.ContentValues" == typeAsString) { + "values" + } else if ("long" == typeAsString) { + "id" + } else if ("java.lang.String" == typeAsString) { + "where" + } else if ("java.lang.String[]" == typeAsString) { + "whereArgs" + } else { + "" + }) + } - params = paramsBuilder.toString() + params = paramsBuilder.toString() - val typeMirror = executableElement.returnType - if (ClassNames.URI.toString() + "[]" == typeMirror.toString()) { - returnsArray = true - } else if (ClassNames.URI.toString() == typeMirror.toString()) { - returnsSingle = true - } else { - processorManager.logError("Notify method returns wrong type. It must return Uri or Uri[]") + val typeMirror = executableElement.returnType + if (ClassNames.URI.toString() + "[]" == typeMirror.toString()) { + returnsArray = true + } else if (ClassNames.URI.toString() == typeMirror.toString()) { + returnsSingle = true + } else { + processorManager.logError("Notify method returns wrong type. It must return Uri or Uri[]") + } + } } } override fun getElementClassName(element: Element?): ClassName? { return null } + + companion object { + fun fromKsp(fn: KSFunctionDeclaration, processorManager: ProcessorManager): NotifyDefinition { + val def = NotifyDefinition(KSP_SENTINEL_ELEMENT, processorManager) + + // parent = enclosing class qualified name + def.parent = (fn.parentDeclaration as? KSClassDeclaration) + ?.qualifiedName?.asString() ?: "" + def.methodName = fn.simpleName.asString() + + val notifyAnnot = fn.findKspAnnotation() + if (notifyAnnot != null) { + def.paths = notifyAnnot.getArrayArgument("paths")?.toTypedArray() ?: arrayOf() + val methodStr = notifyAnnot.getEnumArgument("method") + def.method = when (methodStr) { + "INSERT" -> Notify.Method.INSERT + "UPDATE" -> Notify.Method.UPDATE + else -> Notify.Method.DELETE + } + } + + // Map KSP parameter types to param strings + val paramsBuilder = StringBuilder() + var first = true + for (param in fn.parameters) { + if (!first) paramsBuilder.append(", ") + first = false + val resolvedType = param.type.resolve() + if (resolvedType.isError) continue + val isArray = resolvedType.declaration.qualifiedName?.asString() == "kotlin.Array" + val elementQName = if (isArray) { + val inner = resolvedType.arguments.firstOrNull()?.type?.resolve() + if (inner != null && !inner.isError) inner.declaration.qualifiedName?.asString() ?: "" else "" + } else { + resolvedType.declaration.qualifiedName?.asString() ?: "" + } + paramsBuilder.append(when (elementQName) { + "android.content.Context" -> "getContext()" + "android.net.Uri" -> "uri" + "android.content.ContentValues" -> "values" + "kotlin.Long", "java.lang.Long", "long" -> "id" + "kotlin.String", "java.lang.String" -> "where" + else -> "" + }) + } + def.params = paramsBuilder.toString() + + // Determine return type: Uri[] or Uri + val returnType = fn.returnType?.resolve() + val returnDecl = if (returnType != null && !returnType.isError) returnType.declaration.qualifiedName?.asString() ?: "" else "" + when { + returnDecl == "kotlin.Array" -> def.returnsArray = true + returnDecl == "android.net.Uri" -> def.returnsSingle = true + } + + return def + } + } } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/OneToManyDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/OneToManyDefinition.kt index 90cf24ffa..12d5bbbce 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/OneToManyDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/OneToManyDefinition.kt @@ -5,8 +5,11 @@ import com.grosner.kpoet.`if` import com.grosner.kpoet.end import com.grosner.kpoet.statement import com.grosner.kpoet.typeName +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.raizlabs.android.dbflow.annotation.OneToMany import com.raizlabs.android.dbflow.processor.ClassNames +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.column.ColumnAccessor import com.raizlabs.android.dbflow.processor.definition.column.GetterSetter @@ -17,9 +20,16 @@ import com.raizlabs.android.dbflow.processor.definition.column.wrapperCommaIfBas import com.raizlabs.android.dbflow.processor.definition.column.wrapperIfBaseModel import com.raizlabs.android.dbflow.processor.utils.ModelUtils import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getArrayArgument +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.isSubclass import com.raizlabs.android.dbflow.processor.utils.simpleString import com.raizlabs.android.dbflow.processor.utils.statement +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.processor.utils.toTypeElement import com.squareup.javapoet.ClassName import com.squareup.javapoet.CodeBlock @@ -35,13 +45,13 @@ import javax.lang.model.element.TypeElement /** * Description: Represents the [OneToMany] annotation. */ -class OneToManyDefinition(executableElement: ExecutableElement, +class OneToManyDefinition(element: Element, processorManager: ProcessorManager, - parentElements: Collection) : BaseDefinition(executableElement, processorManager) { + parentElements: Collection = emptyList()) : BaseDefinition(element, processorManager) { - private var _methodName: String + private var _methodName: String = "" - private var _variableName: String + private var _variableName: String = "" var methods = mutableListOf() @@ -60,86 +70,83 @@ class OneToManyDefinition(executableElement: ExecutableElement, var referencedTableType: TypeName? = null var hasWrapper = false - private var columnAccessor: ColumnAccessor + private var columnAccessor: ColumnAccessor = VisibleScopeColumnAccessor("") private var extendsModel = false private var referencedType: TypeElement? = null + private var kspReferencedClassName: ClassName? = null private var efficientCodeMethods = false init { + if (element is ExecutableElement) { + val oneToMany = element.annotation()!! - val oneToMany = executableElement.annotation()!! + efficientCodeMethods = oneToMany.efficientMethods - efficientCodeMethods = oneToMany.efficientMethods - - _methodName = executableElement.simpleName.toString() - _variableName = oneToMany.variableName - if (_variableName.isEmpty()) { - _variableName = _methodName.replace("get", "") - _variableName = _variableName.substring(0, 1).toLowerCase() + _variableName.substring(1) - } - - val privateAccessor = PrivateScopeColumnAccessor(_variableName, object : GetterSetter { - override val getterName: String = "" - override val setterName: String = "" - }, optionalGetterParam = if (hasWrapper) ModelUtils.wrapper else "") + _methodName = element.simpleName.toString() + _variableName = oneToMany.variableName + if (_variableName.isEmpty()) { + _variableName = _methodName.replace("get", "") + _variableName = _variableName.substring(0, 1).toLowerCase() + _variableName.substring(1) + } - var isVariablePrivate = false - val referencedElement = parentElements.firstOrNull { it.simpleString == _variableName } - if (referencedElement == null) { - // check on setter. if setter exists, we can reference it safely since a getter has already been defined. - if (!parentElements.any { it.simpleString == privateAccessor.setterNameElement }) { - manager.logError(OneToManyDefinition::class, - "@OneToMany definition $elementName Cannot find referenced variable $_variableName.") + val privateAccessor = PrivateScopeColumnAccessor(_variableName, object : GetterSetter { + override val getterName: String = "" + override val setterName: String = "" + }, optionalGetterParam = if (hasWrapper) ModelUtils.wrapper else "") + + var isVariablePrivate = false + val referencedElement = parentElements.firstOrNull { it.simpleString == _variableName } + if (referencedElement == null) { + // check on setter. if setter exists, we can reference it safely since a getter has already been defined. + if (!parentElements.any { it.simpleString == privateAccessor.setterNameElement }) { + manager.logError(OneToManyDefinition::class, + "@OneToMany definition $elementName Cannot find referenced variable $_variableName.") + } else { + isVariablePrivate = true + } } else { - isVariablePrivate = true + isVariablePrivate = referencedElement.modifiers.contains(Modifier.PRIVATE) } - } else { - isVariablePrivate = referencedElement.modifiers.contains(Modifier.PRIVATE) - } - methods.addAll(oneToMany.methods) + methods.addAll(oneToMany.methods) - val parameters = executableElement.parameters - if (parameters.isNotEmpty()) { - if (parameters.size > 1) { - manager.logError(OneToManyDefinition::class, "OneToMany Methods can only have one parameter and that be the DatabaseWrapper.") - } else { - val param = parameters[0] - val name = param.asType().typeName - if (name == ClassNames.DATABASE_WRAPPER) { - hasWrapper = true + val parameters = element.parameters + if (parameters.isNotEmpty()) { + if (parameters.size > 1) { + manager.logError(OneToManyDefinition::class, "OneToMany Methods can only have one parameter and that be the DatabaseWrapper.") } else { - manager.logError(OneToManyDefinition::class, "OneToMany Methods can only specify a ${ClassNames.DATABASE_WRAPPER} as its parameter.") + val param = parameters[0] + val name = param.asType().typeName + if (name == ClassNames.DATABASE_WRAPPER) { + hasWrapper = true + } else { + manager.logError(OneToManyDefinition::class, "OneToMany Methods can only specify a ${ClassNames.DATABASE_WRAPPER} as its parameter.") + } } } - } - if (isVariablePrivate) { - columnAccessor = privateAccessor - } else { - columnAccessor = VisibleScopeColumnAccessor(_variableName) - } + columnAccessor = if (isVariablePrivate) privateAccessor else VisibleScopeColumnAccessor(_variableName) - val returnType = executableElement.returnType - val typeName = TypeName.get(returnType) - if (typeName is ParameterizedTypeName) { - val typeArguments = typeName.typeArguments - if (typeArguments.size == 1) { - var refTableType = typeArguments[0] - if (refTableType is WildcardTypeName) { - refTableType = refTableType.upperBounds[0] - } - referencedTableType = refTableType + val returnType = element.returnType + val typeName = TypeName.get(returnType) + if (typeName is ParameterizedTypeName) { + val typeArguments = typeName.typeArguments + if (typeArguments.size == 1) { + var refTableType = typeArguments[0] + if (refTableType is WildcardTypeName) { + refTableType = refTableType.upperBounds[0] + } + referencedTableType = refTableType - referencedType = referencedTableType.toTypeElement(manager) - extendsModel = referencedType.isSubclass(manager.processingEnvironment, ClassNames.MODEL) + referencedType = referencedTableType.toTypeElement(manager) + extendsModel = referencedType.isSubclass(manager.processingEnvironment, ClassNames.MODEL) + } } } - } - private val methodName = "${ModelUtils.variable}.$_methodName(${wrapperIfBaseModel(hasWrapper)})" + private val methodName get() = "${ModelUtils.variable}.$_methodName(${wrapperIfBaseModel(hasWrapper)})" fun writeWrapperStatement(method: MethodSpec.Builder) { method.statement("\$T ${ModelUtils.wrapper} = \$T.getWritableDatabaseForTable(\$T.class)", @@ -179,6 +186,7 @@ class OneToManyDefinition(executableElement: ExecutableElement, private fun writeLoopWithMethod(codeBuilder: MethodSpec.Builder, methodName: String, useWrapper: Boolean) { val oneToManyMethodName = this@OneToManyDefinition.methodName + val loopType: TypeName = kspReferencedClassName ?: ClassName.get(referencedType) codeBuilder.apply { `if`("$oneToManyMethodName != null") { // need to load adapter for non-model classes @@ -191,7 +199,7 @@ class OneToManyDefinition(executableElement: ExecutableElement, if (efficientCodeMethods) { statement("adapter.${methodName}All($oneToManyMethodName${wrapperCommaIfBaseModel(useWrapper)})") } else { - `for`("\$T value: $oneToManyMethodName", ClassName.get(referencedType)) { + `for`("\$T value: $oneToManyMethodName", loopType) { if (!extendsModel) { statement("adapter.$methodName(value${wrapperCommaIfBaseModel(useWrapper)})") } else { @@ -203,5 +211,71 @@ class OneToManyDefinition(executableElement: ExecutableElement, } }.end() } + + fun kspInit(fn: KSFunctionDeclaration, classElementLookUpMap: Map) { + elementName = fn.simpleName.asString() + + val annot = fn.findKspAnnotation() ?: return + + efficientCodeMethods = annot.getBooleanArgument("efficientMethods") ?: true + + _methodName = fn.simpleName.asString() + _variableName = annot.getStringArgument("variableName") ?: "" + if (_variableName.isEmpty()) { + _variableName = _methodName.removePrefix("get").replaceFirstChar { it.lowercase() } + } + + val isVariablePrivate = annot.getBooleanArgument("isVariablePrivate") ?: run { + !classElementLookUpMap.containsKey(_variableName) + } + + // Read methods as enum names from the annotation arguments. KSP1 delivers each entry as a + // KSType; KSP2 delivers a KSClassDeclaration for the enum entry directly. Handle all three. + val methodsArg = annot.arguments.find { it.name?.asString() == "methods" }?.value + val methodList = (methodsArg as? List<*>)?.mapNotNull { item -> + val name = when (item) { + is com.google.devtools.ksp.symbol.KSClassDeclaration -> item.simpleName.asString() + is com.google.devtools.ksp.symbol.KSType -> item.declaration.simpleName.asString() + is String -> item + else -> null + } + name?.let { runCatching { OneToMany.Method.valueOf(it) }.getOrNull() } + } ?: emptyList() + methods.addAll(methodList) + + // Check if function has a DatabaseWrapper parameter + hasWrapper = fn.parameters.any { param -> + val typeName = param.type.resolve().toJavaPoetTypeName() + typeName == ClassNames.DATABASE_WRAPPER + } + + val privateAccessor = PrivateScopeColumnAccessor(_variableName, object : GetterSetter { + override val getterName: String = "" + override val setterName: String = "" + }, optionalGetterParam = if (hasWrapper) ModelUtils.wrapper else "") + + columnAccessor = if (isVariablePrivate) privateAccessor else VisibleScopeColumnAccessor(_variableName) + + // Extract return type's single type argument (e.g. List → TwoColumnModel) + val returnType = fn.returnType?.resolve() ?: return + val returnTypeName = returnType.toJavaPoetTypeName() + if (returnTypeName is ParameterizedTypeName && returnTypeName.typeArguments.size == 1) { + var refTableType = returnTypeName.typeArguments[0] + if (refTableType is WildcardTypeName) refTableType = refTableType.upperBounds[0] + referencedTableType = refTableType + kspReferencedClassName = refTableType as? ClassName + } + + // Determine if referenced type extends Model + val returnTypeDecl = (returnType.arguments.firstOrNull()?.type?.resolve()?.declaration + as? com.google.devtools.ksp.symbol.KSClassDeclaration) + if (returnTypeDecl != null) { + val superTypeNames = returnTypeDecl.getAllSuperTypes() + .mapNotNull { it.declaration.qualifiedName?.asString() }.toSet() + extendsModel = ClassNames.MODEL.toString() in superTypeNames + kspReferencedClassName = returnTypeDecl.toJavaPoetClassName() + referencedTableType = kspReferencedClassName + } + } } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/QueryModelDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/QueryModelDefinition.kt index fee449d55..9bd74e79d 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/QueryModelDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/QueryModelDefinition.kt @@ -4,18 +4,26 @@ import com.grosner.kpoet.`return` import com.grosner.kpoet.final import com.grosner.kpoet.modifiers import com.grosner.kpoet.public +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.Column import com.raizlabs.android.dbflow.annotation.ColumnMap import com.raizlabs.android.dbflow.annotation.QueryModel import com.raizlabs.android.dbflow.processor.ClassNames import com.raizlabs.android.dbflow.processor.ColumnValidator +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.column.ColumnDefinition import com.raizlabs.android.dbflow.processor.definition.column.ReferenceColumnDefinition import com.raizlabs.android.dbflow.processor.utils.ElementUtility import com.raizlabs.android.dbflow.processor.utils.`override fun` import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument import com.raizlabs.android.dbflow.processor.utils.implementsClass +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeName import com.squareup.javapoet.TypeSpec @@ -36,6 +44,11 @@ class QueryModelDefinition(typeElement: Element, processorManager: ProcessorMana internal var methods: Array + internal var kspMode = false + internal var ksClassDeclaration: KSClassDeclaration? = null + + private var preparedKspWrite = false + init { typeElement.annotation()?.let { queryModel -> @@ -58,11 +71,44 @@ class QueryModelDefinition(typeElement: Element, processorManager: ProcessorMana } + fun kspInit(ksClass: KSClassDeclaration) { + kspMode = true + ksClassDeclaration = ksClass + originatingFile = ksClass.containingFile + + elementName = ksClass.simpleName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + packageName = ksClass.packageName.asString() + + val annot = ksClass.findKspAnnotation() ?: return + + val dbKsType = annot.getKsTypeArgument("database") + databaseTypeName = dbKsType?.toJavaPoetTypeName() + + allFields = annot.getBooleanArgument("allFields") ?: false + + elementClassName?.let { className -> + databaseTypeName?.let { dbType -> manager.addModelToDatabase(className, dbType) } + } + + val superTypes = ksClass.getAllSuperTypes().map { it.declaration.qualifiedName?.asString() } + implementsLoadFromCursorListener = ClassNames.LOAD_FROM_CURSOR_LISTENER.toString() in superTypes + } + override fun prepareForWrite() { + if (kspMode && preparedKspWrite) return + classElementLookUpMap.clear() columnDefinitions.clear() packagePrivateList.clear() + if (kspMode) { + prepareForWriteKsp() + preparedKspWrite = true + return + } + val queryModel = typeElement.annotation() if (queryModel != null) { allFields = queryModel.allFields @@ -76,6 +122,56 @@ class QueryModelDefinition(typeElement: Element, processorManager: ProcessorMana typeElement?.let { createColumnDefinitions(it) } } + private fun prepareForWriteKsp() { + val ksClass = ksClassDeclaration ?: return + databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition + if (databaseDefinition == null) { + manager.logError("DatabaseDefinition was null for KSP QueryModel: $elementName") + return + } + setOutputClassName("${databaseDefinition?.classSeparator}QueryTable") + createColumnDefinitionsFromKsp(ksClass) + } + + private fun createColumnDefinitionsFromKsp(ksClass: KSClassDeclaration) { + for (property in ksClass.getAllProperties()) { + val name = property.simpleName.asString() + classElementLookUpMap[name] = KSP_SENTINEL_ELEMENT + val cap = name.replaceFirstChar { it.uppercase() } + classElementLookUpMap["get$cap"] = KSP_SENTINEL_ELEMENT + classElementLookUpMap["set$cap"] = KSP_SENTINEL_ELEMENT + if (name.startsWith("is", ignoreCase = true)) { + val withoutIs = name.removePrefix("is").removePrefix("Is") + .replaceFirstChar { it.uppercase() } + classElementLookUpMap["set$withoutIs"] = KSP_SENTINEL_ELEMENT + } + } + for (function in ksClass.declarations.filterIsInstance()) { + classElementLookUpMap[function.simpleName.asString()] = KSP_SENTINEL_ELEMENT + } + + val columnValidator = ColumnValidator() + + for (property in ksClass.getAllProperties()) { + val hasColumn = property.findKspAnnotation() != null + val isAllFieldsCandidate = allFields && + com.google.devtools.ksp.symbol.Modifier.PRIVATE !in property.modifiers && + com.google.devtools.ksp.symbol.Modifier.JAVA_STATIC !in property.modifiers + + if (!hasColumn && !isAllFieldsCandidate) continue + + val colDef = ColumnDefinition(manager, KSP_SENTINEL_ELEMENT, this, false) + colDef.kspInit(property) + + if (columnValidator.validate(manager, colDef)) { + columnDefinitions.add(colDef) + if (colDef.isPrimaryKey || colDef.isPrimaryKeyAutoIncrement || colDef.isRowId) { + manager.logError("QueryModel $elementName cannot have primary keys") + } + } + } + } + override val extendsClass: TypeName? get() = ParameterizedTypeName.get(ClassNames.QUERY_MODEL_ADAPTER, elementClassName) diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableDefinition.kt index 9efa374ba..e0c7c8dbf 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableDefinition.kt @@ -15,6 +15,8 @@ import com.grosner.kpoet.protected import com.grosner.kpoet.public import com.grosner.kpoet.statement import com.grosner.kpoet.switch +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.KSClassDeclaration import com.raizlabs.android.dbflow.annotation.Column import com.raizlabs.android.dbflow.annotation.ColumnMap import com.raizlabs.android.dbflow.annotation.ConflictAction @@ -28,6 +30,7 @@ import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Table import com.raizlabs.android.dbflow.processor.ClassNames import com.raizlabs.android.dbflow.processor.ColumnValidator +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.OneToManyValidator import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.BindToStatementMethod.Mode.* @@ -40,8 +43,16 @@ import com.raizlabs.android.dbflow.processor.utils.ModelUtils.wrapper import com.raizlabs.android.dbflow.processor.utils.`override fun` import com.raizlabs.android.dbflow.processor.utils.annotation import com.raizlabs.android.dbflow.processor.utils.ensureVisibleStatic +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.getIntArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.implementsClass import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.sql.QueryBuilder import com.squareup.javapoet.ArrayTypeName import com.squareup.javapoet.ClassName @@ -61,7 +72,7 @@ import javax.lang.model.type.MirroredTypeException /** * Description: Used in writing ModelAdapters */ -class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTableDefinition(element, manager) { +class TableDefinition(manager: ProcessorManager, element: javax.lang.model.element.Element) : BaseTableDefinition(element, manager) { var tableName: String? = null @@ -100,6 +111,17 @@ class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTab var oneToManyDefinitions = mutableListOf() + /** Set to true when this definition is populated via [kspInit] rather than from a TypeElement. */ + internal var kspMode = false + internal var ksClassDeclaration: KSClassDeclaration? = null + + /** + * KSP2 invalidates [com.google.devtools.ksp.symbol.KSDeclaration]s captured in earlier rounds, + * so [prepareForWrite] (which re-touches the round-1 declaration) must run only once per + * definition. Round-2-discovered tables get prepared the first time they hit this method. + */ + private var preparedKspWrite = false + var inheritedColumnMap = hashMapOf() var inheritedFieldNameList = mutableListOf() var inheritedPrimaryKeyMap = hashMapOf() @@ -155,14 +177,14 @@ class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTab inheritedPrimaryKeyMap.put(it.fieldName, it) } - implementsLoadFromCursorListener = element.implementsClass(manager.processingEnvironment, - ClassNames.LOAD_FROM_CURSOR_LISTENER) + implementsLoadFromCursorListener = typeElement?.implementsClass(manager.processingEnvironment, + ClassNames.LOAD_FROM_CURSOR_LISTENER) ?: false - implementsContentValuesListener = element.implementsClass(manager.processingEnvironment, - ClassNames.CONTENT_VALUES_LISTENER) + implementsContentValuesListener = typeElement?.implementsClass(manager.processingEnvironment, + ClassNames.CONTENT_VALUES_LISTENER) ?: false - implementsSqlStatementListener = element.implementsClass(manager.processingEnvironment, - ClassNames.SQLITE_STATEMENT_LISTENER) + implementsSqlStatementListener = typeElement?.implementsClass(manager.processingEnvironment, + ClassNames.SQLITE_STATEMENT_LISTENER) ?: false } methods = arrayOf(BindToContentValuesMethod(this, true, implementsContentValuesListener), @@ -182,78 +204,6 @@ class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTab OneToManySaveMethod(this, OneToManySaveMethod.METHOD_UPDATE, true)) } - override fun prepareForWrite() { - columnDefinitions = ArrayList() - columnMap.clear() - classElementLookUpMap.clear() - _primaryColumnDefinitions.clear() - uniqueGroupsDefinitions.clear() - indexGroupsDefinitions.clear() - foreignKeyDefinitions.clear() - columnUniqueMap.clear() - oneToManyDefinitions.clear() - customCacheFieldName = null - customMultiCacheFieldName = null - - val table = element.getAnnotation(Table::class.java) - if (table != null) { - databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition - if (databaseDefinition == null) { - manager.logError("DatabaseDefinition was null for : $tableName for db type: $databaseTypeName") - } - databaseDefinition?.let { - - setOutputClassName("${it.classSeparator}Table") - - // globular default - var insertConflict = table.insertConflict - if (insertConflict == ConflictAction.NONE && it.insertConflict != ConflictAction.NONE) { - insertConflict = it.insertConflict ?: ConflictAction.NONE - } - - var updateConflict = table.updateConflict - if (updateConflict == ConflictAction.NONE && it.updateConflict != ConflictAction.NONE) { - updateConflict = it.updateConflict ?: ConflictAction.NONE - } - - val primaryKeyConflict = table.primaryKeyConflict - - insertConflictActionName = if (insertConflict == ConflictAction.NONE) "" else insertConflict.name - updateConflictActionName = if (updateConflict == ConflictAction.NONE) "" else updateConflict.name - primaryKeyConflictActionName = if (primaryKeyConflict == ConflictAction.NONE) "" else primaryKeyConflict.name - } - - typeElement?.let { createColumnDefinitions(it) } - - val groups = table.uniqueColumnGroups - var uniqueNumbersSet: MutableSet = HashSet() - for (uniqueGroup in groups) { - if (uniqueNumbersSet.contains(uniqueGroup.groupNumber)) { - manager.logError("A duplicate unique group with number %1s was found for %1s", uniqueGroup.groupNumber, tableName) - } - val definition = UniqueGroupsDefinition(uniqueGroup) - columnDefinitions.filter { it.uniqueGroups.contains(definition.number) } - .forEach { definition.addColumnDefinition(it) } - uniqueGroupsDefinitions.add(definition) - uniqueNumbersSet.add(uniqueGroup.groupNumber) - } - - val indexGroups = table.indexGroups - uniqueNumbersSet = HashSet() - for (indexGroup in indexGroups) { - if (uniqueNumbersSet.contains(indexGroup.number)) { - manager.logError(TableDefinition::class, "A duplicate unique index number %1s was found for %1s", indexGroup.number, elementName) - } - val definition = IndexGroupsDefinition(this, indexGroup) - columnDefinitions.filter { it.indexGroups.contains(definition.indexNumber) } - .forEach { definition.columnDefinitionList.add(it) } - indexGroupsDefinitions.add(definition) - uniqueNumbersSet.add(indexGroup.number) - } - } - - } - override fun createColumnDefinitions(typeElement: TypeElement) { val elements = ElementUtility.getAllElements(typeElement, manager) @@ -348,7 +298,7 @@ class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTab } } } else if (element.annotation() != null) { - val oneToManyDefinition = OneToManyDefinition(element as ExecutableElement, manager, elements) + val oneToManyDefinition = OneToManyDefinition(element, manager, elements) if (oneToManyValidator.validate(manager, oneToManyDefinition)) { oneToManyDefinitions.add(oneToManyDefinition) } @@ -370,6 +320,356 @@ class TableDefinition(manager: ProcessorManager, element: TypeElement) : BaseTab } } + /** + * KSP initialiser. Reads @Table annotation data from KSP instead of the KAPT init block. + * Must be called right after construction when the definition is created in KSP mode. + */ + fun kspInit(ksClass: KSClassDeclaration) { + kspMode = true + ksClassDeclaration = ksClass + originatingFile = ksClass.containingFile + + elementName = ksClass.simpleName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + packageName = ksClass.packageName.asString() + + val annot = ksClass.findKspAnnotation
() ?: return + + tableName = annot.getStringArgument("name").takeIf { !it.isNullOrEmpty() } ?: elementName + + val dbKsType = annot.getKsTypeArgument("database") + databaseTypeName = dbKsType?.toJavaPoetTypeName() + + cachingEnabled = annot.getBooleanArgument("cachingEnabled") ?: false + cacheSize = annot.arguments.find { it.name?.asString() == "cacheSize" }?.value as? Int ?: 0 + orderedCursorLookUp = annot.getBooleanArgument("orderedCursorLookUp") ?: false + assignDefaultValuesFromCursor = annot.getBooleanArgument("assignDefaultValuesFromCursor") ?: true + createWithDatabase = annot.getBooleanArgument("createWithDatabase") ?: true + allFields = annot.getBooleanArgument("allFields") ?: false + useIsForPrivateBooleans = annot.getBooleanArgument("useBooleanGetterSetters") ?: true + + elementClassName?.let { databaseTypeName?.let { dbt -> manager.addModelToDatabase(it, dbt) } } + + // Interface checks via KSP super types + val superTypes = ksClass.getAllSuperTypes().map { it.declaration.qualifiedName?.asString() } + implementsLoadFromCursorListener = ClassNames.LOAD_FROM_CURSOR_LISTENER.toString() in superTypes + implementsContentValuesListener = ClassNames.CONTENT_VALUES_LISTENER.toString() in superTypes + implementsSqlStatementListener = ClassNames.SQLITE_STATEMENT_LISTENER.toString() in superTypes + } + + override fun prepareForWrite() { + if (kspMode && preparedKspWrite) return + + columnDefinitions = ArrayList() + columnMap.clear() + classElementLookUpMap.clear() + _primaryColumnDefinitions.clear() + uniqueGroupsDefinitions.clear() + indexGroupsDefinitions.clear() + foreignKeyDefinitions.clear() + columnUniqueMap.clear() + oneToManyDefinitions.clear() + customCacheFieldName = null + customMultiCacheFieldName = null + + if (kspMode) { + prepareForWriteKsp() + preparedKspWrite = true + return + } + + val table = element.getAnnotation(Table::class.java) + if (table != null) { + databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition + if (databaseDefinition == null) { + manager.logError("DatabaseDefinition was null for : $tableName for db type: $databaseTypeName") + } + databaseDefinition?.let { + setOutputClassName("${it.classSeparator}Table") + + var insertConflict = table.insertConflict + if (insertConflict == ConflictAction.NONE && it.insertConflict != ConflictAction.NONE) { + insertConflict = it.insertConflict ?: ConflictAction.NONE + } + + var updateConflict = table.updateConflict + if (updateConflict == ConflictAction.NONE && it.updateConflict != ConflictAction.NONE) { + updateConflict = it.updateConflict ?: ConflictAction.NONE + } + + val primaryKeyConflict = table.primaryKeyConflict + + insertConflictActionName = if (insertConflict == ConflictAction.NONE) "" else insertConflict.name + updateConflictActionName = if (updateConflict == ConflictAction.NONE) "" else updateConflict.name + primaryKeyConflictActionName = if (primaryKeyConflict == ConflictAction.NONE) "" else primaryKeyConflict.name + } + + typeElement?.let { createColumnDefinitions(it) } + + val groups = table.uniqueColumnGroups + var uniqueNumbersSet: MutableSet = HashSet() + for (uniqueGroup in groups) { + if (uniqueNumbersSet.contains(uniqueGroup.groupNumber)) { + manager.logError("A duplicate unique group with number %1s was found for %1s", uniqueGroup.groupNumber, tableName) + } + val definition = UniqueGroupsDefinition(uniqueGroup) + columnDefinitions.filter { it.uniqueGroups.contains(definition.number) } + .forEach { definition.addColumnDefinition(it) } + uniqueGroupsDefinitions.add(definition) + uniqueNumbersSet.add(uniqueGroup.groupNumber) + } + + val indexGroups = table.indexGroups + uniqueNumbersSet = HashSet() + for (indexGroup in indexGroups) { + if (uniqueNumbersSet.contains(indexGroup.number)) { + manager.logError(TableDefinition::class, "A duplicate unique index number %1s was found for %1s", indexGroup.number, elementName) + } + val definition = IndexGroupsDefinition(this, indexGroup) + columnDefinitions.filter { it.indexGroups.contains(definition.indexNumber) } + .forEach { definition.columnDefinitionList.add(it) } + indexGroupsDefinitions.add(definition) + uniqueNumbersSet.add(indexGroup.number) + } + } + } + + private fun prepareForWriteKsp() { + val ksClass = ksClassDeclaration ?: return + databaseDefinition = manager.getDatabaseHolderDefinition(databaseTypeName)?.databaseDefinition + if (databaseDefinition == null) { + manager.logError("DatabaseDefinition was null for KSP table: $tableName for db type: $databaseTypeName") + return + } + + // Determine whether the class has a usable no-arg constructor. + // In Kotlin, a primary ctor where all params have defaults generates a synthetic no-arg ctor. + val primaryCtor = ksClass.primaryConstructor + hasPrimaryConstructor = if (primaryCtor == null) { + true // implicit no-arg constructor + } else { + com.google.devtools.ksp.symbol.Modifier.PRIVATE !in primaryCtor.modifiers && + (primaryCtor.parameters.isEmpty() || primaryCtor.parameters.all { it.hasDefault }) + } + + databaseDefinition?.let { dbDef -> + setOutputClassName("${dbDef.classSeparator}Table") + + val annot = ksClass.findKspAnnotation
() + val insertConflictName = annot?.getEnumArgument("insertConflict") ?: "NONE" + val updateConflictName = annot?.getEnumArgument("updateConflict") ?: "NONE" + val primaryKeyConflictName = annot?.getEnumArgument("primaryKeyConflict") ?: "NONE" + + var insertConflict = runCatching { ConflictAction.valueOf(insertConflictName) }.getOrDefault(ConflictAction.NONE) + if (insertConflict == ConflictAction.NONE && dbDef.insertConflict != ConflictAction.NONE) { + insertConflict = dbDef.insertConflict ?: ConflictAction.NONE + } + var updateConflict = runCatching { ConflictAction.valueOf(updateConflictName) }.getOrDefault(ConflictAction.NONE) + if (updateConflict == ConflictAction.NONE && dbDef.updateConflict != ConflictAction.NONE) { + updateConflict = dbDef.updateConflict ?: ConflictAction.NONE + } + val primaryKeyConflict = runCatching { ConflictAction.valueOf(primaryKeyConflictName) }.getOrDefault(ConflictAction.NONE) + + insertConflictActionName = if (insertConflict == ConflictAction.NONE) "" else insertConflict.name + updateConflictActionName = if (updateConflict == ConflictAction.NONE) "" else updateConflict.name + primaryKeyConflictActionName = if (primaryKeyConflict == ConflictAction.NONE) "" else primaryKeyConflict.name + } + + createColumnDefinitionsFromKsp(ksClass) + + // Process @Table(uniqueColumnGroups = {...}). KSP exposes the annotation arguments as + // raw lists of nested KSAnnotation instances rather than reflective annotation objects, + // so we read groupNumber + uniqueConflict directly off each. + val tableAnnot = ksClass.findKspAnnotation
() + val rawUniqueGroups = tableAnnot + ?.arguments + ?.find { it.name?.asString() == "uniqueColumnGroups" } + ?.value + @Suppress("UNCHECKED_CAST") + val uniqueGroupAnnots = (rawUniqueGroups as? List<*>) + ?.filterIsInstance() + val seenGroupNumbers = mutableSetOf() + uniqueGroupAnnots?.forEach { groupAnnot -> + val groupNumber = groupAnnot.getIntArgument("groupNumber") ?: return@forEach + if (!seenGroupNumbers.add(groupNumber)) { + manager.logError("A duplicate unique group with number $groupNumber was found for $tableName") + return@forEach + } + val conflictName = groupAnnot.getEnumArgument("uniqueConflict") ?: "NONE" + val conflict = runCatching { ConflictAction.valueOf(conflictName) }.getOrDefault(ConflictAction.NONE) + val definition = UniqueGroupsDefinition(groupNumber, conflict) + columnDefinitions + .filter { it.uniqueGroups.contains(groupNumber) } + .forEach { definition.addColumnDefinition(it) } + uniqueGroupsDefinitions.add(definition) + } + } + + // KSP's getAllProperties() skips private members from superclasses; we need them too (e.g. + // @PrimaryKey private Integer id in a Java superclass). Collect own declarations + all + // superclass declarations without a private-visibility filter. + private fun collectAllPropertiesIncludingInherited( + decl: KSClassDeclaration, + seen: MutableSet = mutableSetOf() + ): List { + val result = mutableListOf() + for (prop in decl.declarations.filterIsInstance()) { + if (seen.add(prop.simpleName.asString())) result.add(prop) + } + for (superTypeRef in decl.superTypes) { + val superDecl = superTypeRef.resolve().declaration as? KSClassDeclaration ?: continue + val qName = superDecl.qualifiedName?.asString() ?: continue + if (qName == "java.lang.Object" || qName == "kotlin.Any") continue + result.addAll(collectAllPropertiesIncludingInherited(superDecl, seen)) + } + return result + } + + private fun createColumnDefinitionsFromKsp(ksClass: KSClassDeclaration) { + // Collect all properties including private ones from superclasses. + val allProperties = collectAllPropertiesIncludingInherited(ksClass) + + // Pre-populate classElementLookUpMap so ColumnValidator's getter/setter checks pass. + // Kotlin generates get/set accessors for every var property that lacks @JvmField. + for (property in allProperties) { + val name = property.simpleName.asString() + classElementLookUpMap[name] = KSP_SENTINEL_ELEMENT + val cap = name.replaceFirstChar { it.uppercase() } + classElementLookUpMap["get$cap"] = KSP_SENTINEL_ELEMENT + classElementLookUpMap["set$cap"] = KSP_SENTINEL_ELEMENT + // Boolean "isXxx" → getter stays "isXxx", setter becomes "setXxx" + if (name.startsWith("is", ignoreCase = true)) { + classElementLookUpMap[name] = KSP_SENTINEL_ELEMENT + val withoutIs = name.removePrefix("is").removePrefix("Is") + .replaceFirstChar { it.uppercase() } + classElementLookUpMap["set$withoutIs"] = KSP_SENTINEL_ELEMENT + } + } + // Also add functions from the whole hierarchy (needed for Java getter/setter presence checks) + fun addFunctions(decl: KSClassDeclaration) { + for (fn in decl.declarations.filterIsInstance()) { + classElementLookUpMap[fn.simpleName.asString()] = KSP_SENTINEL_ELEMENT + } + for (superTypeRef in decl.superTypes) { + val superDecl = superTypeRef.resolve().declaration as? KSClassDeclaration ?: continue + val qName = superDecl.qualifiedName?.asString() ?: continue + if (qName == "java.lang.Object" || qName == "kotlin.Any") continue + addFunctions(superDecl) + } + } + addFunctions(ksClass) + + // Scan companion objects for @MultiCacheField and @ModelCacheField properties + for (decl in ksClass.declarations) { + val companion = decl as? KSClassDeclaration ?: continue + if (!companion.isCompanionObject) continue + for (prop in companion.declarations.filterIsInstance()) { + when { + prop.findKspAnnotation() != null -> { + if (!customMultiCacheFieldName.isNullOrEmpty()) { + manager.logError("MultiCacheField can only be declared once from: $elementName") + } else { + customMultiCacheFieldName = prop.simpleName.asString() + } + } + prop.findKspAnnotation() != null -> { + if (!customCacheFieldName.isNullOrEmpty()) { + manager.logError("ModelCacheField can only be declared once from: $elementName") + } else { + customCacheFieldName = prop.simpleName.asString() + } + } + } + } + } + + // Scan methods for @OneToMany annotations + val oneToManyValidator = OneToManyValidator() + fun scanFunctionsForOneToMany(decl: KSClassDeclaration) { + for (fn in decl.declarations.filterIsInstance()) { + if (fn.findKspAnnotation() != null) { + val def = OneToManyDefinition(KSP_SENTINEL_ELEMENT, manager) + def.kspInit(fn, classElementLookUpMap) + if (oneToManyValidator.validate(manager, def)) { + oneToManyDefinitions.add(def) + } + } + } + } + scanFunctionsForOneToMany(ksClass) + for (superTypeRef in ksClass.superTypes) { + val superDecl = superTypeRef.resolve().declaration as? KSClassDeclaration ?: continue + val qName = superDecl.qualifiedName?.asString() ?: continue + if (qName == "java.lang.Object" || qName == "kotlin.Any") continue + scanFunctionsForOneToMany(superDecl) + } + + val columnValidator = ColumnValidator() + val properties = allProperties + + for (property in properties) { + val hasColumn = property.findKspAnnotation() != null + val hasPrimaryKey = property.findKspAnnotation() != null + val hasForeignKey = property.findKspAnnotation() != null + val isColumnMap = property.findKspAnnotation() != null + + // allFields: include public non-static mutable fields that aren't ignored + // Exclude val/final fields to match KAPT behavior (KAPT skips Java `final` backing fields) + val isAllFieldsCandidate = allFields && + com.google.devtools.ksp.symbol.Modifier.PRIVATE !in property.modifiers && + com.google.devtools.ksp.symbol.Modifier.JAVA_STATIC !in property.modifiers && + property.isMutable + + if (!hasColumn && !hasPrimaryKey && !hasForeignKey && !isColumnMap && !isAllFieldsCandidate) continue + + if (isColumnMap) { + manager.logWarning("KSP: @ColumnMap on ${property.simpleName.asString()} in $tableName " + + "is not yet fully supported – skipped.") + continue + } + + val colDef: ColumnDefinition + if (hasForeignKey) { + val refDef = ReferenceColumnDefinition(manager, this, KSP_SENTINEL_ELEMENT, false) + refDef.kspInit(property) + colDef = refDef + } else { + colDef = ColumnDefinition(manager, KSP_SENTINEL_ELEMENT, this, false) + colDef.kspInit(property) + } + + if (columnValidator.validate(manager, colDef)) { + columnDefinitions.add(colDef) + columnMap[colDef.columnName] = colDef + when { + colDef.isPrimaryKey -> _primaryColumnDefinitions.add(colDef) + colDef.isPrimaryKeyAutoIncrement -> { + autoIncrementColumn = colDef + hasAutoIncrement = true + } + colDef.isRowId -> { + autoIncrementColumn = colDef + hasRowID = true + } + } + autoIncrementColumn?.let { + if (it.isNullableType) { + manager.logWarning("Nullable autoincrement column ${it.columnName} on $tableName") + } + } + if (colDef is ReferenceColumnDefinition && !colDef.isColumnMap) { + foreignKeyDefinitions.add(colDef) + } + if (!colDef.uniqueGroups.isEmpty()) { + colDef.uniqueGroups.forEach { group -> + columnUniqueMap.getOrPut(group) { mutableSetOf() }.add(colDef) + } + } + } + } + } + override val primaryColumnDefinitions: List get() = autoIncrementColumn?.let { arrayListOf(it) } ?: _primaryColumnDefinitions diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableEndpointDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableEndpointDefinition.kt index 02030f7c2..5eaa20eae 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableEndpointDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TableEndpointDefinition.kt @@ -1,10 +1,19 @@ package com.raizlabs.android.dbflow.processor.definition +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.raizlabs.android.dbflow.annotation.provider.ContentUri import com.raizlabs.android.dbflow.annotation.provider.Notify import com.raizlabs.android.dbflow.annotation.provider.TableEndpoint import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.TypeName import javax.lang.model.element.Element import javax.lang.model.element.PackageElement @@ -47,7 +56,8 @@ class TableEndpointDefinition(typeElement: Element, processorManager: ProcessorM isTopLevel = typeElement.enclosingElement is PackageElement - val elements = processorManager.elements.getAllMembers(typeElement as TypeElement) + val typeEl = typeElement as? TypeElement + val elements = if (typeEl != null) processorManager.elements.getAllMembers(typeEl) else emptyList() for (innerElement in elements) { if (innerElement.annotation() != null) { val contentUriDefinition = ContentUriDefinition(innerElement, processorManager) @@ -77,4 +87,57 @@ class TableEndpointDefinition(typeElement: Element, processorManager: ProcessorM } } + + fun kspInit(ksClass: KSClassDeclaration) { + elementName = ksClass.simpleName.asString() + packageName = ksClass.packageName.asString() + elementClassName = ksClass.toJavaPoetClassName() + elementTypeName = elementClassName + originatingFile = ksClass.containingFile + + val endpointAnnot = ksClass.findKspAnnotation() ?: return + tableName = endpointAnnot.getStringArgument("name") ?: "" + contentProviderName = endpointAnnot.getKsTypeArgument("contentProvider")?.toJavaPoetTypeName() + + isTopLevel = ksClass.parentDeclaration == null + + fun processDecl(decl: KSDeclaration) { + when { + decl is KSPropertyDeclaration && decl.findKspAnnotation() != null -> { + val contentUriDef = ContentUriDefinition.fromKsp(decl, manager) + if (!pathValidationMap.containsKey(contentUriDef.path)) { + contentUriDefinitions.add(contentUriDef) + } else { + manager.logError("Duplicate @ContentUri path %1s in %1s", contentUriDef.path, elementName) + } + } + decl is KSFunctionDeclaration && decl.findKspAnnotation() != null -> { + val contentUriDef = ContentUriDefinition.fromKsp(decl, manager) + if (!pathValidationMap.containsKey(contentUriDef.path)) { + contentUriDefinitions.add(contentUriDef) + } else { + manager.logError("Duplicate @ContentUri path %1s in %1s", contentUriDef.path, elementName) + } + } + decl is KSFunctionDeclaration && decl.findKspAnnotation() != null -> { + val notifyDef = NotifyDefinition.fromKsp(decl, manager) + for (path in notifyDef.paths) { + val methodListMap = notifyDefinitionPathMap.getOrPut(path) { mutableMapOf() } + val list = methodListMap.getOrPut(notifyDef.method) { mutableListOf() } + list.add(notifyDef) + } + } + } + } + + for (decl in ksClass.declarations) { + processDecl(decl) + // Also traverse companion objects for @ContentUri / @Notify members + if (decl is KSClassDeclaration && decl.isCompanionObject) { + for (innerDecl in decl.declarations) { + processDecl(innerDecl) + } + } + } + } } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TypeConverterDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TypeConverterDefinition.kt index 0f4e698c0..7b9d4757d 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TypeConverterDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/TypeConverterDefinition.kt @@ -1,9 +1,17 @@ package com.raizlabs.android.dbflow.processor.definition +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType import com.raizlabs.android.dbflow.annotation.TypeConverter import com.raizlabs.android.dbflow.processor.ClassNames +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_TYPE_MIRROR import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation +import com.raizlabs.android.dbflow.processor.utils.getArrayArgument +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.squareup.javapoet.ClassName import com.squareup.javapoet.TypeName import javax.lang.model.element.TypeElement @@ -19,10 +27,8 @@ class TypeConverterDefinition(val className: ClassName, typeElement: TypeElement? = null) { var modelTypeName: TypeName? = null - private set var dbTypeName: TypeName? = null - private set var allowedSubTypes: List? = null @@ -59,4 +65,41 @@ class TypeConverterDefinition(val className: ClassName, } } + companion object { + private val TYPE_CONVERTER_QNAME = ClassNames.TYPE_CONVERTER.toString() + + fun fromKsp(ksClass: KSClassDeclaration, manager: ProcessorManager): TypeConverterDefinition? { + val className = ksClass.toJavaPoetClassName() + + var dbTypeName: TypeName? = null + var modelTypeName: TypeName? = null + + for (superType in ksClass.getAllSuperTypes()) { + val qName = superType.declaration.qualifiedName?.asString() ?: continue + if (qName == TYPE_CONVERTER_QNAME) { + val args = superType.arguments + if (args.size >= 2) { + // Box primitives — Java generics cannot use primitive types as type arguments + dbTypeName = args[0].type?.resolve()?.toJavaPoetTypeName()?.box() + modelTypeName = args[1].type?.resolve()?.toJavaPoetTypeName()?.box() + } + break + } + } + + if (dbTypeName == null || modelTypeName == null) return null + + val definition = TypeConverterDefinition(className, KSP_SENTINEL_TYPE_MIRROR, manager) + definition.dbTypeName = dbTypeName + definition.modelTypeName = modelTypeName + + ksClass.findKspAnnotation()?.let { annot -> + val subTypes = annot.getArrayArgument("allowedSubtypes") + definition.allowedSubTypes = subTypes?.map { it.toJavaPoetTypeName() } ?: emptyList() + } + + return definition + } + } + } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/UniqueGroupsDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/UniqueGroupsDefinition.kt index eebeffd9a..f0e0f4772 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/UniqueGroupsDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/UniqueGroupsDefinition.kt @@ -11,14 +11,15 @@ import java.util.* /** * Description: */ -class UniqueGroupsDefinition(uniqueGroup: UniqueGroup) { +class UniqueGroupsDefinition( + var number: Int, + private val uniqueConflict: ConflictAction +) { + /** Convenience for the KAPT path which has a real annotation instance. */ + constructor(uniqueGroup: UniqueGroup) : this(uniqueGroup.groupNumber, uniqueGroup.uniqueConflict) var columnDefinitionList: MutableList = ArrayList() - var number: Int = uniqueGroup.groupNumber - - private val uniqueConflict: ConflictAction = uniqueGroup.uniqueConflict - fun addColumnDefinition(columnDefinition: ColumnDefinition) { columnDefinitionList.add(columnDefinition) } diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnAccessCombiner.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnAccessCombiner.kt index a716abf2d..a1ab429b7 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnAccessCombiner.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnAccessCombiner.kt @@ -263,8 +263,10 @@ class LoadFromCursorAccessCombiner(combiner: Combiner, var defaultValueBlock = defaultValue if (!assignDefaultValuesFromCursor) { defaultValueBlock = fieldLevelAccessor.get(modelBlock) - } else if (!hasDefault && fieldTypeName.isBoxedPrimitive) { - hasDefault = true // force a null on it. + } else if (!hasDefault && !fieldTypeName.isPrimitive) { + // Any non-primitive reference type (boxed numeric, String, etc.) with no + // explicit default should produce null when the cursor column is null. + hasDefault = true } val cursorAccess = CodeBlock.of("cursor.\$LOrDefault(\$L${if (hasDefault) ", $defaultValueBlock" else ""})", SQLiteHelper.getMethod(wrapperFieldTypeName ?: fieldTypeName), indexName) diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnDefinition.kt index 1fb2ee096..2988ae930 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ColumnDefinition.kt @@ -1,6 +1,9 @@ package com.raizlabs.android.dbflow.processor.definition.column import com.grosner.kpoet.code +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.Nullability +import com.google.devtools.ksp.symbol.Origin import com.raizlabs.android.dbflow.annotation.Collate import com.raizlabs.android.dbflow.annotation.Column import com.raizlabs.android.dbflow.annotation.ConflictAction @@ -11,16 +14,27 @@ import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Unique import com.raizlabs.android.dbflow.data.Blob import com.raizlabs.android.dbflow.processor.ClassNames +import com.raizlabs.android.dbflow.processor.KSP_SENTINEL_ELEMENT import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.BaseDefinition import com.raizlabs.android.dbflow.processor.definition.BaseTableDefinition import com.raizlabs.android.dbflow.processor.definition.TableDefinition import com.raizlabs.android.dbflow.processor.definition.TypeConverterDefinition import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findAnnotationByName +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation import com.raizlabs.android.dbflow.processor.utils.fromTypeMirror +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.getIntArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument +import com.raizlabs.android.dbflow.processor.utils.getStringArgument import com.raizlabs.android.dbflow.processor.utils.getTypeElement +import com.raizlabs.android.dbflow.processor.utils.isEnum import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty import com.raizlabs.android.dbflow.processor.utils.toClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.processor.utils.toTypeElement import com.raizlabs.android.dbflow.sql.QueryBuilder import com.squareup.javapoet.ArrayTypeName @@ -271,6 +285,21 @@ constructor(processorManager: ProcessorManager, element: Element, subWrapperAccessor) } + /** + * Reads an `int[]` annotation argument (e.g. `uniqueGroups`, `indexGroups`). Java permits + * writing a single-element array as a bare scalar (`uniqueGroups = 1`), and KSP exposes that + * form as a single Int rather than a List, so we have to handle both shapes. + */ + private fun readIntArrayArgument(annot: com.google.devtools.ksp.symbol.KSAnnotation, name: String): List { + val raw = annot.arguments.find { it.name?.asString() == name }?.value ?: return emptyList() + return when (raw) { + is List<*> -> raw.filterIsInstance() + is IntArray -> raw.toList() + is Int -> listOf(raw) + else -> emptyList() + } + } + private fun evaluateTypeConverter(typeConverterDefinition: TypeConverterDefinition?, isCustom: Boolean) { // Any annotated members, otherwise we will use the scanner to find other ones @@ -299,6 +328,142 @@ constructor(processorManager: ProcessorManager, element: Element, } } + /** + * KSP initialiser – called after construction with [KSP_SENTINEL_ELEMENT] to populate all + * fields from a [KSPropertyDeclaration]. Mirrors the logic in the primary [init] block but + * reads annotation data from KSP instead of javax.lang.model. + */ + open fun kspInit(property: KSPropertyDeclaration) { + val ksType = property.type.resolve() + + elementName = property.simpleName.asString() + elementTypeName = ksType.toJavaPoetTypeName() + elementClassName = (ksType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration)?.toJavaPoetClassName() + packageName = property.parentDeclaration?.packageName?.asString() ?: "" + + // PLATFORM = unannotated Java type: treat as nullable (same as KAPT did for unannotated Java). + isNullableType = ksType.isMarkedNullable || ksType.nullability == Nullability.PLATFORM + isNotNullType = ksType.nullability == Nullability.NOT_NULL || elementTypeName?.isPrimitive == true + + // @NotNull + property.findKspAnnotation()?.let { notNullAnnot -> + notNull = true + val conflictName = notNullAnnot.getEnumArgument("onNullConflict") ?: "NONE" + onNullConflict = runCatching { ConflictAction.valueOf(conflictName) }.getOrDefault(ConflictAction.NONE) + } + + // @Column + val columnAnnot = property.findKspAnnotation() + column = null // sentinel already set it to null; keep null since we read via KSP + val rawColumnName = columnAnnot?.getStringArgument("name").takeIf { !it.isNullOrEmpty() } ?: elementName + columnName = rawColumnName + length = columnAnnot?.getIntArgument("length") ?: -1 + collate = columnAnnot?.getEnumArgument("collate") + ?.let { runCatching { Collate.valueOf(it) }.getOrNull() } + ?: Collate.NONE + defaultValue = columnAnnot?.getStringArgument("defaultValue")?.takeIf { it.isNotBlank() } + + val isString = elementTypeName == ClassName.get(String::class.java) + if (defaultValue != null && isString && !QUOTE_PATTERN.matcher(defaultValue).find()) { + defaultValue = "\"$defaultValue\"" + } + if (isNotNullType && defaultValue == null && isString) { + defaultValue = "\"\"" + } + + val nameAllocator = NameAllocator() + propertyFieldName = nameAllocator.newName(columnName) + + // Accessor determination: + // - Java fields (JAVA/JAVA_LIB/SYNTHETIC origin): non-private → direct access. + // - Kotlin properties: need @JvmField for direct access; otherwise use getter/setter. + val isPrivate = com.google.devtools.ksp.symbol.Modifier.PRIVATE in property.modifiers + val isKotlinOrigin = property.origin == Origin.KOTLIN || property.origin == Origin.KOTLIN_LIB + val hasJvmField = property.findAnnotationByName("kotlin.jvm.JvmField") != null + val useDirectAccess = if (!isKotlinOrigin) !isPrivate else (!isPrivate && hasJvmField) + if (!useDirectAccess) { + val isBoolean = elementTypeName?.box() == TypeName.BOOLEAN.box() + val useIs = isBoolean && baseTableDefinition is TableDefinition && + (baseTableDefinition as TableDefinition).useIsForPrivateBooleans + columnAccessor = PrivateScopeColumnAccessor(elementName, object : GetterSetter { + override val getterName: String = columnAnnot?.getStringArgument("getterName") ?: "" + override val setterName: String = columnAnnot?.getStringArgument("setterName") ?: "" + }, useIsForPrivateBooleans = useIs) + } else { + columnAccessor = VisibleScopeColumnAccessor(elementName) + } + + // @PrimaryKey + val pkAnnot = property.findKspAnnotation() + if (pkAnnot != null) { + val rowId = pkAnnot.getBooleanArgument("rowID") ?: false + val autoincrement = pkAnnot.getBooleanArgument("autoincrement") ?: false + val quickCheck = pkAnnot.getBooleanArgument("quickCheckAutoIncrement") ?: false + when { + rowId -> isRowId = true + autoincrement -> { + isPrimaryKeyAutoIncrement = true + isQuickCheckPrimaryKeyAutoIncrement = quickCheck + } + else -> isPrimaryKey = true + } + } + + // @Unique + property.findKspAnnotation()?.let { uniqueAnnot -> + unique = uniqueAnnot.getBooleanArgument("unique") ?: true + val conflictName = uniqueAnnot.getEnumArgument("onUniqueConflict") ?: "NONE" + onUniqueConflict = runCatching { ConflictAction.valueOf(conflictName) }.getOrDefault(ConflictAction.NONE) + uniqueGroups.addAll(readIntArrayArgument(uniqueAnnot, "uniqueGroups")) + } + + // @Index + property.findKspAnnotation()?.let { indexAnnot -> + val groups = readIntArrayArgument(indexAnnot, "indexGroups") + if (groups.isEmpty()) { + indexGroups.add(IndexGroup.GENERIC) + } else { + indexGroups.addAll(groups) + } + } + + // @Column(typeConverter = SomeConverter::class) — custom converter + hasCustomConverter = false + val customConverterKsType = columnAnnot?.getKsTypeArgument("typeConverter") + val customConverterDecl = customConverterKsType?.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration + if (customConverterDecl != null && + customConverterDecl.qualifiedName?.asString() != ClassNames.TYPE_CONVERTER.toString()) { + val tcDef = TypeConverterDefinition.fromKsp(customConverterDecl, manager) + if (tcDef != null) { + evaluateTypeConverter(tcDef, true) + } + } + + // Type-converter detection (enum, blob, built-in converters) + val ksClassDecl = ksType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration + if (hasCustomConverter) { + // already handled above + } else if (ksClassDecl?.isEnum() == true) { + wrapperAccessor = EnumColumnAccessor(elementTypeName!!) + wrapperTypeName = ClassName.get(String::class.java) + } else if (elementTypeName == ClassName.get(Blob::class.java)) { + wrapperAccessor = BlobColumnAccessor() + wrapperTypeName = ArrayTypeName.of(TypeName.BYTE) + } else { + when (elementTypeName) { + TypeName.BOOLEAN -> { wrapperAccessor = BooleanColumnAccessor(); wrapperTypeName = TypeName.BOOLEAN } + TypeName.CHAR -> { wrapperAccessor = CharColumnAccessor(); wrapperTypeName = TypeName.CHAR } + TypeName.BYTE -> { wrapperAccessor = ByteColumnAccessor(); wrapperTypeName = TypeName.BYTE } + else -> { + typeConverterDefinition = elementTypeName?.let { manager.getTypeConverterDefinition(it) } + evaluateTypeConverter(typeConverterDefinition, false) + } + } + } + + combiner = Combiner(columnAccessor, elementTypeName!!, wrapperAccessor, wrapperTypeName, subWrapperAccessor) + } + override fun toString(): String { val tableDef = baseTableDefinition var tableName = tableDef.elementName diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceColumnDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceColumnDefinition.kt index 1a4b6f45c..3fe59860b 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceColumnDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceColumnDefinition.kt @@ -15,11 +15,20 @@ import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.BaseTableDefinition import com.raizlabs.android.dbflow.processor.definition.QueryModelDefinition import com.raizlabs.android.dbflow.processor.definition.TableDefinition +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.symbol.Origin import com.raizlabs.android.dbflow.processor.utils.annotation +import com.raizlabs.android.dbflow.processor.utils.findAnnotationByName +import com.raizlabs.android.dbflow.processor.utils.findKspAnnotation import com.raizlabs.android.dbflow.processor.utils.fromTypeMirror +import com.raizlabs.android.dbflow.processor.utils.getBooleanArgument +import com.raizlabs.android.dbflow.processor.utils.getEnumArgument +import com.raizlabs.android.dbflow.processor.utils.getKsTypeArgument import com.raizlabs.android.dbflow.processor.utils.implementsClass import com.raizlabs.android.dbflow.processor.utils.isNullOrEmpty import com.raizlabs.android.dbflow.processor.utils.isSubclass +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetClassName +import com.raizlabs.android.dbflow.processor.utils.toJavaPoetTypeName import com.raizlabs.android.dbflow.processor.utils.toTypeElement import com.raizlabs.android.dbflow.processor.utils.toTypeErasedElement import com.raizlabs.android.dbflow.sql.QueryBuilder @@ -431,6 +440,99 @@ class ReferenceColumnDefinition(manager: ProcessorManager, tableDefinition: Base } } } + + override fun kspInit(property: com.google.devtools.ksp.symbol.KSPropertyDeclaration) { + val ksType = property.type.resolve() + elementName = property.simpleName.asString() + elementTypeName = ksType.toJavaPoetTypeName() + elementClassName = (ksType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration) + ?.toJavaPoetClassName() + packageName = property.parentDeclaration?.packageName?.asString() ?: "" + + isNullableType = ksType.isMarkedNullable + isNotNullType = false // FK fields must be nullable + + val nameAllocator = com.squareup.javapoet.NameAllocator() + columnName = elementName + propertyFieldName = nameAllocator.newName(columnName) + + // Accessor for the FK field itself (e.g. model.author) + val isPrivate = com.google.devtools.ksp.symbol.Modifier.PRIVATE in property.modifiers + val isKotlinOrigin = property.origin == Origin.KOTLIN || property.origin == Origin.KOTLIN_LIB + val hasJvmField = property.findAnnotationByName("kotlin.jvm.JvmField") != null + val useDirectAccess = if (!isKotlinOrigin) !isPrivate else (!isPrivate && hasJvmField) + columnAccessor = if (!useDirectAccess) { + PrivateScopeColumnAccessor(elementName, object : GetterSetter { + override val getterName: String = "" + override val setterName: String = "" + }) + } else { + VisibleScopeColumnAccessor(elementName) + } + combiner = Combiner(columnAccessor, elementTypeName!!, wrapperAccessor, wrapperTypeName, subWrapperAccessor) + + // @PrimaryKey — FK primary keys are composite keys (not autoincrement) + val pkAnnot = property.findKspAnnotation() + if (pkAnnot != null) { + val rowId = pkAnnot.getBooleanArgument("rowID") ?: false + when { + rowId -> isRowId = true + else -> isPrimaryKey = true + } + } + + // @ForeignKey + val fkAnnot = property.findKspAnnotation() ?: return + + val onDeleteStr = fkAnnot.getEnumArgument("onDelete") ?: "NO_ACTION" + val onUpdateStr = fkAnnot.getEnumArgument("onUpdate") ?: "NO_ACTION" + onDelete = runCatching { ForeignKeyAction.valueOf(onDeleteStr) }.getOrDefault(ForeignKeyAction.NO_ACTION) + onUpdate = runCatching { ForeignKeyAction.valueOf(onUpdateStr) }.getOrDefault(ForeignKeyAction.NO_ACTION) + deferred = fkAnnot.getBooleanArgument("deferred") ?: false + isStubbedRelationship = fkAnnot.getBooleanArgument("stubbedRelationship") ?: false + saveForeignKeyModel = fkAnnot.getBooleanArgument("saveForeignKeyModel") ?: false + deleteForeignKeyModel = fkAnnot.getBooleanArgument("deleteForeignKeyModel") ?: false + + // tableClass override (if not TypeConverter base) + val tableKsType = fkAnnot.getKsTypeArgument("tableClass") + val tableQName = tableKsType?.declaration?.qualifiedName?.asString() + if (tableQName != null && tableQName != "kotlin.Any" && tableQName != "java.lang.Object") { + referencedClassName = (tableKsType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration) + ?.toJavaPoetClassName() + } + + // Determine referenced class from field type if not set via tableClass + if (referencedClassName == null || referencedClassName == ClassName.OBJECT) { + val decl = ksType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration + referencedClassName = decl?.toJavaPoetClassName() + } + + // Determine if the referenced type is a model + val referencedDecl = (ksType.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration) + if (referencedDecl != null) { + val superTypeNames = referencedDecl.getAllSuperTypes() + .mapNotNull { it.declaration.qualifiedName?.asString() }.toSet() + extendsBaseModel = ClassNames.BASE_MODEL.toString() in superTypeNames + implementsModel = ClassNames.MODEL.toString() in superTypeNames + isReferencingTableObject = implementsModel || + referencedDecl.findKspAnnotation() != null + nonModelColumn = !isReferencingTableObject + } + + // @ForeignKeyReference array + @Suppress("UNCHECKED_CAST") + val refAnnotations = fkAnnot.arguments + .find { it.name?.asString() == "references" } + ?.value as? List + if (!refAnnotations.isNullOrEmpty()) { + references = refAnnotations.mapNotNull { refAnnot -> + val colName = (refAnnot.arguments.find { it.name?.asString() == "columnName" }?.value as? String) ?: return@mapNotNull null + val fkColName = (refAnnot.arguments.find { it.name?.asString() == "foreignKeyColumnName" }?.value as? String) ?: "" + ReferenceSpecificationDefinition(columnName = colName, referenceName = fkColName, + onNullConflictAction = ConflictAction.NONE) + } + } + } } /** diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceDefinition.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceDefinition.kt index bf69f6104..f8affb784 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceDefinition.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/definition/column/ReferenceDefinition.kt @@ -128,7 +128,8 @@ class ReferenceDefinition(private val manager: ProcessorManager, referenceColumnDefinition.element) isReferencedFieldPackagePrivate = isReferencedFieldPackagePrivate || isPackagePrivateNotInSamePackage val packageName = referencedColumn.packageName - val name = ClassName.get(referencedColumn.element.enclosingElement as TypeElement).simpleName() + val name = (referencedColumn.element.enclosingElement as? TypeElement) + ?.let { ClassName.get(it).simpleName() } ?: "" createScopes(referenceColumnDefinition, foreignKeyElementName, object : GetterSetter { override val getterName: String = referencedColumn.column?.getterName ?: "" override val setterName: String = referencedColumn.column?.setterName ?: "" diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/CodeExtensions.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/CodeExtensions.kt index ad1a58f68..75f425322 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/CodeExtensions.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/CodeExtensions.kt @@ -37,7 +37,7 @@ fun MethodSpec.Builder.statement(codeBlock: CodeBlock?): MethodSpec.Builder inline fun CodeBlock.Builder.catch(exception: KClass, function: CodeBlock.Builder.() -> CodeBlock.Builder) - = nextControl("catch", statement = "\$T e", args = exception.java, function = function).end() + = nextControl("catch", statement = "\$T e", args = arrayOf(exception.java), function = function).end() fun codeBlock(function: CodeBlock.Builder.() -> CodeBlock.Builder) = CodeBlock.builder().function().build() diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ElementUtility.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ElementUtility.kt index 6257ac6df..501e40b12 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ElementUtility.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ElementUtility.kt @@ -32,7 +32,9 @@ object ElementUtility { } fun isInSamePackage(manager: ProcessorManager, elementToCheck: Element, original: Element): Boolean { - return manager.elements.getPackageOf(elementToCheck).toString() == manager.elements.getPackageOf(original).toString() + val pkg1 = manager.elements.getPackageOf(elementToCheck)?.toString() ?: return true + val pkg2 = manager.elements.getPackageOf(original)?.toString() ?: return true + return pkg1 == pkg2 } fun isPackagePrivate(element: Element): Boolean { diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/KspExtensions.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/KspExtensions.kt new file mode 100644 index 000000000..fe9c6c0fe --- /dev/null +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/KspExtensions.kt @@ -0,0 +1,139 @@ +package com.raizlabs.android.dbflow.processor.utils + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.Nullability +import com.google.devtools.ksp.symbol.Variance +import com.squareup.javapoet.ArrayTypeName +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.ParameterizedTypeName +import com.squareup.javapoet.TypeName +import com.squareup.javapoet.TypeVariableName +import com.squareup.javapoet.WildcardTypeName + +fun KSClassDeclaration.toJavaPoetClassName(): ClassName { + val pkg = packageName.asString() + val names = mutableListOf() + var decl: KSDeclaration? = this + while (decl is KSClassDeclaration) { + names.add(0, decl.simpleName.asString()) + decl = decl.parentDeclaration + } + return if (names.size == 1) { + ClassName.get(pkg, names[0]) + } else { + ClassName.get(pkg, names[0], *names.drop(1).toTypedArray()) + } +} + +private val KOTLIN_PRIMITIVE_MAP: Map = mapOf( + "kotlin.Int" to TypeName.INT, + "kotlin.Long" to TypeName.LONG, + "kotlin.Double" to TypeName.DOUBLE, + "kotlin.Float" to TypeName.FLOAT, + "kotlin.Boolean" to TypeName.BOOLEAN, + "kotlin.Byte" to TypeName.BYTE, + "kotlin.Short" to TypeName.SHORT, + "kotlin.Char" to TypeName.CHAR +) + +private val KOTLIN_CLASS_MAP: Map = mapOf( + "kotlin.String" to ClassName.get("java.lang", "String"), + "kotlin.Any" to ClassName.get("java.lang", "Object"), + "kotlin.Unit" to ClassName.get("java.lang", "Void") +) + +// Kotlin array types map to Java array types (not representable as ClassName) +private val KOTLIN_ARRAY_TYPE_MAP: Map = mapOf( + "kotlin.ByteArray" to ArrayTypeName.of(TypeName.BYTE), + "kotlin.IntArray" to ArrayTypeName.of(TypeName.INT), + "kotlin.LongArray" to ArrayTypeName.of(TypeName.LONG), + "kotlin.ShortArray" to ArrayTypeName.of(TypeName.SHORT), + "kotlin.FloatArray" to ArrayTypeName.of(TypeName.FLOAT), + "kotlin.DoubleArray" to ArrayTypeName.of(TypeName.DOUBLE), + "kotlin.BooleanArray" to ArrayTypeName.of(TypeName.BOOLEAN), + "kotlin.CharArray" to ArrayTypeName.of(TypeName.CHAR) +) + +fun KSType.toJavaPoetTypeName(): TypeName { + if (isError) return TypeName.OBJECT + return when (val declaration = this.declaration) { + is KSTypeParameter -> TypeVariableName.get(declaration.name.asString()) + is KSClassDeclaration -> { + val qualifiedName = declaration.qualifiedName?.asString() + // Map Kotlin primitives to Java primitives/wrappers + KOTLIN_PRIMITIVE_MAP[qualifiedName]?.let { primitive -> + // PLATFORM nullability means a Java type without a nullability annotation — treat + // as boxed (nullable) to avoid NPE on auto-unboxing in generated bind* calls. + return if (isMarkedNullable || nullability == Nullability.PLATFORM) primitive.box() else primitive + } + KOTLIN_ARRAY_TYPE_MAP[qualifiedName]?.let { arrayType -> return arrayType } + KOTLIN_CLASS_MAP[qualifiedName]?.let { classType -> return classType } + + val className = declaration.toJavaPoetClassName() + val typeArgs = arguments + if (typeArgs.isEmpty()) { + className + } else { + val resolvedArgs = typeArgs.map { arg -> + when (arg.variance) { + Variance.STAR -> WildcardTypeName.subtypeOf(TypeName.OBJECT) + Variance.CONTRAVARIANT -> WildcardTypeName.supertypeOf( + arg.type?.resolve()?.toJavaPoetTypeName() ?: TypeName.OBJECT + ) + else -> arg.type?.resolve()?.toJavaPoetTypeName() ?: TypeName.OBJECT + } + } + ParameterizedTypeName.get(className, *resolvedArgs.toTypedArray()) + } + } + else -> TypeName.OBJECT + } +} + +fun KSAnnotated.findAnnotationByName(qualifiedName: String): KSAnnotation? = + annotations.find { + val resolved = it.annotationType.resolve() + !resolved.isError && resolved.declaration.qualifiedName?.asString() == qualifiedName + } + +inline fun KSAnnotated.findKspAnnotation(): KSAnnotation? = + findAnnotationByName(T::class.qualifiedName ?: "") + +fun KSAnnotation.getKsTypeArgument(name: String): KSType? = + arguments.find { it.name?.asString() == name }?.value as? KSType + +fun KSAnnotation.getStringArgument(name: String): String? = + (arguments.find { it.name?.asString() == name }?.value as? String) + +fun KSAnnotation.getIntArgument(name: String): Int? = + arguments.find { it.name?.asString() == name }?.value as? Int + +fun KSAnnotation.getBooleanArgument(name: String): Boolean? = + arguments.find { it.name?.asString() == name }?.value as? Boolean + +@Suppress("UNCHECKED_CAST") +fun KSAnnotation.getArrayArgument(name: String): List? = + (arguments.find { it.name?.asString() == name }?.value as? List<*>)?.filterNotNull() as? List + +fun KSClassDeclaration.isEnum(): Boolean = classKind == ClassKind.ENUM_CLASS + +/** Returns the simple name string for an enum annotation argument. + * + * KSP1 typically delivers the value as a [KSType] (the enum entry's type) or a [String]; + * KSP2 delivers a [KSClassDeclaration] for the enum entry directly. Handle all three. + */ +fun KSAnnotation.getEnumArgument(name: String): String? { + val value = arguments.find { it.name?.asString() == name }?.value + return when (value) { + is KSClassDeclaration -> value.simpleName.asString() + is KSType -> value.declaration.simpleName.asString() + is String -> value + else -> null + } +} diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ProcessorUtils.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ProcessorUtils.kt index 9d35a41c9..180188829 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ProcessorUtils.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/ProcessorUtils.kt @@ -13,22 +13,17 @@ import javax.tools.Diagnostic /** * Whether the specified element implements the [ClassName] */ -fun TypeElement?.implementsClass(processingEnvironment: ProcessingEnvironment - = manager.processingEnvironment, className: ClassName) - = implementsClass(processingEnvironment, className.toString()) +fun TypeElement?.implementsClass(processingEnvironment: ProcessingEnvironment? + = manager.processingEnvironment, className: ClassName): Boolean { + if (processingEnvironment == null) return false + return implementsClass(processingEnvironment, className.toString()) +} /** * Whether the specified element is assignable to the fqTn parameter - - * @param processingEnvironment The environment this runs in - * * - * @param fqTn THe fully qualified type name of the element we want to check - * * - * @param element The element to check that implements - * * - * @return true if element implements the fqTn */ -fun TypeElement?.implementsClass(processingEnvironment: ProcessingEnvironment, fqTn: String): Boolean { +fun TypeElement?.implementsClass(processingEnvironment: ProcessingEnvironment?, fqTn: String): Boolean { + if (processingEnvironment == null) return false val typeElement = processingEnvironment.elementUtils.getTypeElement(fqTn) if (typeElement == null) { processingEnvironment.messager.printMessage(Diagnostic.Kind.ERROR, @@ -47,14 +42,17 @@ fun TypeElement?.implementsClass(processingEnvironment: ProcessingEnvironment, f /** * Whether the specified element is assignable to the [className] parameter */ -fun TypeElement?.isSubclass(processingEnvironment: ProcessingEnvironment - = manager.processingEnvironment, className: ClassName) - = isSubclass(processingEnvironment, className.toString()) +fun TypeElement?.isSubclass(processingEnvironment: ProcessingEnvironment? + = manager.processingEnvironment, className: ClassName): Boolean { + if (processingEnvironment == null) return false + return isSubclass(processingEnvironment, className.toString()) +} /** * Whether the specified element is assignable to the [fqTn] parameter */ -fun TypeElement?.isSubclass(processingEnvironment: ProcessingEnvironment, fqTn: String): Boolean { +fun TypeElement?.isSubclass(processingEnvironment: ProcessingEnvironment?, fqTn: String): Boolean { + if (processingEnvironment == null) return false val typeElement = processingEnvironment.elementUtils.getTypeElement(fqTn) if (typeElement == null) { processingEnvironment.messager.printMessage(Diagnostic.Kind.ERROR, "Type Element was null for: $fqTn ensure that the visibility of the class is not private.") diff --git a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/WriterUtils.kt b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/WriterUtils.kt index 281dedeca..4a9c5ae41 100644 --- a/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/WriterUtils.kt +++ b/dbflow-processor/src/main/java/com/raizlabs/android/dbflow/processor/utils/WriterUtils.kt @@ -1,6 +1,5 @@ package com.raizlabs.android.dbflow.processor.utils -import com.grosner.kpoet.javaFile import com.raizlabs.android.dbflow.processor.ProcessorManager import com.raizlabs.android.dbflow.processor.definition.BaseDefinition import java.io.IOException @@ -11,19 +10,20 @@ import java.io.IOException object WriterUtils { fun writeBaseDefinition(baseDefinition: BaseDefinition, processorManager: ProcessorManager): Boolean { - var success = false - try { - javaFile(baseDefinition.packageName) { baseDefinition.typeSpec } - .writeTo(processorManager.processingEnvironment.filer) - success = true + if (baseDefinition.fileWritten) return true + return try { + // Per-class definitions pass their originating KSFile so KSP can mark the output + // isolating; the KAPT path ignores the extra parameter. + processorManager.writeJavaFile(baseDefinition.packageName, baseDefinition.typeSpec, baseDefinition.originatingFile) + baseDefinition.fileWritten = true + true } catch (e: IOException) { - // ignored + false } catch (i: IllegalStateException) { processorManager.logError(WriterUtils::class, "Found error for class:" + baseDefinition.elementName) processorManager.logError(WriterUtils::class, i.message) + false } - - return success } } diff --git a/dbflow-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/dbflow-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 000000000..252f6ad9c --- /dev/null +++ b/dbflow-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.raizlabs.android.dbflow.processor.DBFlowSymbolProcessorProvider diff --git a/dbflow-rx-kotlinextensions/build.gradle b/dbflow-rx-kotlinextensions/build.gradle index 225aeaadc..4b0b47ca1 100644 --- a/dbflow-rx-kotlinextensions/build.gradle +++ b/dbflow-rx-kotlinextensions/build.gradle @@ -5,18 +5,23 @@ project.ext.artifactId = bt_name android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow.rx.kotlinextensions" defaultConfig { minSdkVersion dbflow_min_sdk_rx targetSdkVersion Integer.valueOf(dbflow_target_sdk) } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = '1.8' } } dependencies { api project("${dbflow_project_prefix}dbflow") api project("${dbflow_project_prefix}dbflow-rx") - api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } apply from: '../kotlin-artifacts.gradle' diff --git a/dbflow-rx-kotlinextensions/src/main/AndroidManifest.xml b/dbflow-rx-kotlinextensions/src/main/AndroidManifest.xml index 903cee5f9..b8b29fd52 100644 --- a/dbflow-rx-kotlinextensions/src/main/AndroidManifest.xml +++ b/dbflow-rx-kotlinextensions/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/dbflow-rx/build.gradle b/dbflow-rx/build.gradle index ef9aeeb3a..2d538b5a6 100644 --- a/dbflow-rx/build.gradle +++ b/dbflow-rx/build.gradle @@ -4,7 +4,7 @@ project.ext.artifactId = bt_name android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow.rx" defaultConfig { minSdkVersion dbflow_min_sdk_rx @@ -12,14 +12,14 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { api project("${dbflow_project_prefix}dbflow") - api 'io.reactivex:rxjava:1.2.7' + api 'io.reactivex:rxjava:1.3.8' } apply from: '../android-artifacts.gradle' diff --git a/dbflow-rx/src/main/AndroidManifest.xml b/dbflow-rx/src/main/AndroidManifest.xml index d05f0ea55..b8b29fd52 100644 --- a/dbflow-rx/src/main/AndroidManifest.xml +++ b/dbflow-rx/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/dbflow-rx2-kotlinextensions/build.gradle b/dbflow-rx2-kotlinextensions/build.gradle index 415af0113..46a1cc462 100644 --- a/dbflow-rx2-kotlinextensions/build.gradle +++ b/dbflow-rx2-kotlinextensions/build.gradle @@ -5,18 +5,23 @@ project.ext.artifactId = bt_name android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow.rx2.kotlinextensions" defaultConfig { minSdkVersion dbflow_min_sdk_rx targetSdkVersion Integer.valueOf(dbflow_target_sdk) } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = '1.8' } } dependencies { api project("${dbflow_project_prefix}dbflow") api project("${dbflow_project_prefix}dbflow-rx2") - api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } apply from: '../kotlin-artifacts.gradle' diff --git a/dbflow-rx2-kotlinextensions/src/main/AndroidManifest.xml b/dbflow-rx2-kotlinextensions/src/main/AndroidManifest.xml index 816732f3f..b8b29fd52 100644 --- a/dbflow-rx2-kotlinextensions/src/main/AndroidManifest.xml +++ b/dbflow-rx2-kotlinextensions/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/dbflow-rx2/build.gradle b/dbflow-rx2/build.gradle index 1fb162c01..55ce21534 100644 --- a/dbflow-rx2/build.gradle +++ b/dbflow-rx2/build.gradle @@ -4,7 +4,7 @@ project.ext.artifactId = bt_name android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow.rx2" defaultConfig { minSdkVersion dbflow_min_sdk_rx @@ -12,14 +12,14 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { api project("${dbflow_project_prefix}dbflow") - api 'io.reactivex.rxjava2:rxjava:2.1.3' + api 'io.reactivex.rxjava2:rxjava:2.2.21' } apply from: '../android-artifacts.gradle' diff --git a/dbflow-rx2/src/main/AndroidManifest.xml b/dbflow-rx2/src/main/AndroidManifest.xml index 34ee46575..b8b29fd52 100644 --- a/dbflow-rx2/src/main/AndroidManifest.xml +++ b/dbflow-rx2/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/dbflow-sqlcipher/build.gradle b/dbflow-sqlcipher/build.gradle index 74428766d..7038b36fb 100644 --- a/dbflow-sqlcipher/build.gradle +++ b/dbflow-sqlcipher/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'com.android.library' project.ext.artifactId = bt_name android { - compileSdkVersion 26 - buildToolsVersion dbflow_build_tools_version + compileSdkVersion Integer.valueOf(dbflow_target_sdk) + namespace "com.raizlabs.android.dbflow.sqlcipher" defaultConfig { - minSdkVersion 7 - targetSdkVersion 26 + minSdkVersion 21 + targetSdkVersion Integer.valueOf(dbflow_target_sdk) versionCode = version_code } @@ -17,13 +17,14 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { - api "net.zetetic:android-database-sqlcipher:3.5.7@aar" + api "net.zetetic:android-database-sqlcipher:4.5.4@aar" + api "androidx.sqlite:sqlite:2.4.0" api project("${dbflow_project_prefix}dbflow") } diff --git a/dbflow-sqlcipher/src/main/AndroidManifest.xml b/dbflow-sqlcipher/src/main/AndroidManifest.xml index fa75ae33c..19147dc8a 100644 --- a/dbflow-sqlcipher/src/main/AndroidManifest.xml +++ b/dbflow-sqlcipher/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ - + diff --git a/dbflow-tests/build.gradle b/dbflow-tests/build.gradle index d1ec2f4d7..0e6f83bf9 100644 --- a/dbflow-tests/build.gradle +++ b/dbflow-tests/build.gradle @@ -1,26 +1,23 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'com.getkeepsafe.dexcount' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' android { - - useLibrary 'org.apache.http.legacy' - - compileSdkVersion 26 - buildToolsVersion dbflow_build_tools_version + compileSdkVersion 34 + namespace "com.raizlabs.android.dbflow.test" compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = '1.8' } defaultConfig { - minSdkVersion 15 - targetSdkVersion 26 + minSdkVersion 21 + targetSdkVersion 34 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -43,11 +40,11 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - kapt project("${dbflow_project_prefix}dbflow-processor") + ksp project("${dbflow_project_prefix}dbflow-processor") implementation project(':dbflow') - implementation 'com.android.support:appcompat-v7:26.0.1' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation project("${dbflow_project_prefix}dbflow") implementation project("${dbflow_project_prefix}dbflow-sqlcipher") implementation project("${dbflow_project_prefix}dbflow-kotlinextensions") @@ -56,42 +53,19 @@ dependencies { implementation project("${dbflow_project_prefix}dbflow-rx2") implementation project("${dbflow_project_prefix}dbflow-rx2-kotlinextensions") - kaptTest project("${dbflow_project_prefix}dbflow-processor") - kaptAndroidTest project("${dbflow_project_prefix}dbflow-processor") + kspTest project("${dbflow_project_prefix}dbflow-processor") + kspAndroidTest project("${dbflow_project_prefix}dbflow-processor") testImplementation 'org.glassfish:javax.annotation:10.0-b28' - - testImplementation 'junit:junit:4.12' - testImplementation "org.robolectric:robolectric:3.4.2" - testImplementation("com.nhaarman:mockito-kotlin:1.5.0") { + testImplementation 'junit:junit:4.13.2' + testImplementation "org.robolectric:robolectric:4.11.1" + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") { exclude group: "org.jetbrains.kotlin" } - testImplementation 'org.mockito:mockito-core:2.8.9' - - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation('com.android.support.test:runner:0.5') { - exclude group: 'com.android.support', module: 'support-annotations' - } - androidTestImplementation('com.android.support.test:rules:0.5') { - exclude group: 'com.android.support', module: 'support-annotations' - } - androidTestImplementation 'org.awaitility:awaitility:3.0.0-rc1' - -} - -android.applicationVariants.all { variant -> - String taskName = "copy${variant.name.capitalize()}ResDirectoryToClasses" - task(taskName, type: Copy) { - from "${projectDir}/src/test/res" - into "${buildDir}/intermediates/classes/test/${variant.buildType.name}/res" - - from "${projectDir}/src/test/assets" - into "${buildDir}/intermediates/classes/test/${variant.buildType.name}/assets" - } - project.getTasksByName("generate${variant.name.capitalize()}Resources", false)[0].dependsOn(taskName) -} + testImplementation 'org.mockito:mockito-core:5.7.0' -dexcount { - includeClasses = true - orderByMethodCount = true + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'org.awaitility:awaitility:4.2.0' } \ No newline at end of file diff --git a/dbflow-tests/src/androidTest/AndroidManifest.xml b/dbflow-tests/src/androidTest/AndroidManifest.xml index 1f4db6ec2..54beab1ad 100644 --- a/dbflow-tests/src/androidTest/AndroidManifest.xml +++ b/dbflow-tests/src/androidTest/AndroidManifest.xml @@ -1,12 +1,13 @@ + android:name=".DemoActivity" + android:exported="true"> diff --git a/dbflow-tests/src/main/AndroidManifest.xml b/dbflow-tests/src/main/AndroidManifest.xml index 201a00531..5479bfaf4 100644 --- a/dbflow-tests/src/main/AndroidManifest.xml +++ b/dbflow-tests/src/main/AndroidManifest.xml @@ -1,12 +1,13 @@ + android:name=".DemoActivity" + android:exported="true"> diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/ConfigIntegrationTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/ConfigIntegrationTest.kt index e475503cb..cf421b4cb 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/ConfigIntegrationTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/ConfigIntegrationTest.kt @@ -1,6 +1,6 @@ package com.raizlabs.android.dbflow.config -import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockitokotlin2.mock import com.raizlabs.android.dbflow.BaseUnitTest import com.raizlabs.android.dbflow.TestDatabase import com.raizlabs.android.dbflow.models.SimpleModel diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/DatabaseConfigTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/DatabaseConfigTest.kt index 4ac3564ad..9b03c1c59 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/DatabaseConfigTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/config/DatabaseConfigTest.kt @@ -1,6 +1,6 @@ package com.raizlabs.android.dbflow.config -import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockitokotlin2.mock import com.raizlabs.android.dbflow.BaseUnitTest import com.raizlabs.android.dbflow.TestDatabase import com.raizlabs.android.dbflow.runtime.BaseTransactionManager diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowCursorListTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowCursorListTest.kt index 301fcec18..48b0a08de 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowCursorListTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowCursorListTest.kt @@ -1,8 +1,8 @@ package com.raizlabs.android.dbflow.list -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.times -import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify import com.raizlabs.android.dbflow.BaseUnitTest import com.raizlabs.android.dbflow.kotlinextensions.cursor import com.raizlabs.android.dbflow.kotlinextensions.from diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowQueryListTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowQueryListTest.kt index 49680b476..1a20fa4a3 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowQueryListTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/list/FlowQueryListTest.kt @@ -1,9 +1,10 @@ package com.raizlabs.android.dbflow.list -import com.nhaarman.mockito_kotlin.argumentCaptor -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify import com.raizlabs.android.dbflow.BaseUnitTest +import org.robolectric.shadows.ShadowLooper import com.raizlabs.android.dbflow.kotlinextensions.from import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.models.SimpleModel @@ -52,6 +53,7 @@ class FlowQueryListTest : BaseUnitTest() { .error(mockError) .build() list += SimpleModel("1") + ShadowLooper.idleMainLooper() // verify added assertEquals(1, list.count) @@ -61,24 +63,31 @@ class FlowQueryListTest : BaseUnitTest() { verify(mockSuccess).onSuccess(argumentCaptor().capture()) list -= SimpleModel("1") + ShadowLooper.idleMainLooper() assertEquals(0, list.count) list += SimpleModel("1") + ShadowLooper.idleMainLooper() list.removeAt(0) + ShadowLooper.idleMainLooper() assertEquals(0, list.count) val elements = arrayListOf(SimpleModel("1"), SimpleModel("2")) list.addAll(elements) + ShadowLooper.idleMainLooper() assertEquals(2, list.count) list.removeAll(elements) + ShadowLooper.idleMainLooper() assertEquals(0, list.count) list.addAll(elements) + ShadowLooper.idleMainLooper() val typedArray = list.toTypedArray() assertEquals(typedArray.size, list.size) list.clear() + ShadowLooper.idleMainLooper() assertEquals(0, list.size) } diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/runtime/DirectNotifierTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/runtime/DirectNotifierTest.kt index 34834cca5..50e2ad5fe 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/runtime/DirectNotifierTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/runtime/DirectNotifierTest.kt @@ -1,9 +1,9 @@ package com.raizlabs.android.dbflow.runtime import android.content.Context -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.times -import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify import com.raizlabs.android.dbflow.ImmediateTransactionManager2 import com.raizlabs.android.dbflow.TestDatabase import com.raizlabs.android.dbflow.config.DatabaseConfig diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/language/InsertTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/language/InsertTest.kt index 21178f025..a9befb1e9 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/language/InsertTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/language/InsertTest.kt @@ -11,6 +11,7 @@ import com.raizlabs.android.dbflow.models.TwoColumnModel import com.raizlabs.android.dbflow.models.TwoColumnModel_Table.id import com.raizlabs.android.dbflow.models.TwoColumnModel_Table.name import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test class InsertTest : BaseUnitTest() { @@ -66,8 +67,10 @@ class InsertTest : BaseUnitTest() { contentValues["name"] = "name" contentValues["id"] = 0.toInt() - assertEquals("INSERT INTO `TwoColumnModel`(`name`, `id`) VALUES('name', 0)", - insert().columnValues(contentValues).query.trim()) + // ContentValues iteration order is not guaranteed, so just verify both columns are present + val contentValuesQuery = insert().columnValues(contentValues).query.trim() + assertTrue(contentValuesQuery.contains("`name`") && contentValuesQuery.contains("`id`")) + assertTrue(contentValuesQuery.contains("'name'") && contentValuesQuery.contains("0")) } } \ No newline at end of file diff --git a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/queriable/AsyncQueryTest.kt b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/queriable/AsyncQueryTest.kt index 10925cc4f..ab69fd056 100644 --- a/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/queriable/AsyncQueryTest.kt +++ b/dbflow-tests/src/test/java/com/raizlabs/android/dbflow/sql/queriable/AsyncQueryTest.kt @@ -1,6 +1,7 @@ package com.raizlabs.android.dbflow.sql.queriable import com.raizlabs.android.dbflow.BaseUnitTest +import org.robolectric.shadows.ShadowLooper import com.raizlabs.android.dbflow.kotlinextensions.async import com.raizlabs.android.dbflow.kotlinextensions.cursorResult import com.raizlabs.android.dbflow.kotlinextensions.from @@ -24,6 +25,7 @@ class AsyncQueryTest : BaseUnitTest() { (select from SimpleModel::class).async result { _, result -> model = result } + ShadowLooper.idleMainLooper() assertNotNull(model) assertEquals("name", model?.name) } @@ -37,6 +39,7 @@ class AsyncQueryTest : BaseUnitTest() { (select from SimpleModel::class).async list { _, mutableList -> list = mutableList } + ShadowLooper.idleMainLooper() assertEquals(2, list.size) } @@ -49,6 +52,7 @@ class AsyncQueryTest : BaseUnitTest() { (select from SimpleModel::class).async cursorResult { _, cursorResult -> result = cursorResult } + ShadowLooper.idleMainLooper() assertNotNull(result) assertEquals(2L, result?.count) result?.close() diff --git a/dbflow/build.gradle b/dbflow/build.gradle index 3a9e901b2..63a2ed083 100644 --- a/dbflow/build.gradle +++ b/dbflow/build.gradle @@ -4,7 +4,7 @@ project.ext.artifactId = bt_name android { compileSdkVersion Integer.valueOf(dbflow_target_sdk) - buildToolsVersion dbflow_build_tools_version + namespace "com.raizlabs.android.dbflow" defaultConfig { minSdkVersion dbflow_min_sdk @@ -17,15 +17,14 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } - } dependencies { api project("${dbflow_project_prefix}dbflow-core") - api "androidx.annotation:annotation:1.2.0" + api "androidx.annotation:annotation:1.7.0" } apply from: '../android-artifacts.gradle' diff --git a/dbflow/src/main/AndroidManifest.xml b/dbflow/src/main/AndroidManifest.xml index 166b778f3..8b016fb4b 100644 --- a/dbflow/src/main/AndroidManifest.xml +++ b/dbflow/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ diff --git a/gradle.properties b/gradle.properties index 5deb56233..067ca2e15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -version=4.2.4 +version=5.0.4 version_code=1 -group=com.raizlabs.android +group=com.github.simon1867.dbflow bt_siteUrl=https://github.com/Raizlabs/DBFlow bt_gitUrl=https://github.com/Raizlabs/DBFlow.git bt_licenseName=The MIT License @@ -8,10 +8,11 @@ bt_licenseUrl=http://opensource.org/licenses/MIT bt_repo=Libraries dbflow_project_prefix=: kotlin.incremental=false -dbflow_build_tools_version=26.0.2 +ksp.useKSP2=true +dbflow_build_tools_version=34.0.0 -dbflow_min_sdk=4 -dbflow_min_sdk_rx=15 -dbflow_target_sdk=26 +dbflow_min_sdk=21 +dbflow_min_sdk_rx=21 +dbflow_target_sdk=34 android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4227ae5c7..0eb0f6b84 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip diff --git a/java-artifacts.gradle b/java-artifacts.gradle index 0dfd8047b..eacc46556 100644 --- a/java-artifacts.gradle +++ b/java-artifacts.gradle @@ -1,39 +1,16 @@ -apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'maven-publish' -install { - repositories.mavenInstaller { - pom { - project { - packaging bt_packaging - name bt_name - url bt_siteUrl - licenses { - license { - name bt_licenseName - url bt_licenseUrl - } - } - scm { - connection bt_gitUrl - developerConnection bt_gitUrl - url bt_siteUrl - } - } - } - } -} - -task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource +java { + withSourcesJar() } -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir +publishing { + publications { + release(MavenPublication) { + from components.java + groupId = project.group + artifactId = project.name + version = rootProject.version + } + } } - -artifacts { - archives sourcesJar - archives javadocJar -} \ No newline at end of file diff --git a/kotlin-artifacts.gradle b/kotlin-artifacts.gradle index b4f2d1740..257cdafba 100644 --- a/kotlin-artifacts.gradle +++ b/kotlin-artifacts.gradle @@ -1,40 +1,22 @@ -apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'maven-publish' -install { - repositories.mavenInstaller { - pom { - project { - packaging bt_packaging - name bt_name - url bt_siteUrl - licenses { - license { - name bt_licenseName - url bt_licenseUrl - } - } - scm { - connection bt_gitUrl - developerConnection bt_gitUrl - url bt_siteUrl - } - } +android { + publishing { + singleVariant('release') { + withSourcesJar() } } } -// restore when found. -/*task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource -}*/ - -/*task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -}*/ - -artifacts { - //archives sourcesJar - //archives javadocJar -} \ No newline at end of file +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + groupId = project.group + artifactId = project.name + version = rootProject.version + } + } + } +}