diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb7e3a0..fc0a309 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,4 +16,4 @@ jobs: with: java-version: 1.8 - name: Perform base checks - run: ./gradlew demo:assembleDebug firestore:dokka \ No newline at end of file + run: ./gradlew demo:assembleDebug publishToDirectory \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af45bb2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +Starting from v0.7.0, you can [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program. +Companies can share a tiny part of their revenue and get private support hours in return. Thanks! + +## v0.7.0 + +- New: Upgrade to Kotlin 1.4, Firestore 21.6.0 ([#12][12]) +- Breaking change: `FirestoreParcelers.add` is now `registerParceler` or `FirestoreParceler.register` ([#12][12]) +- Breaking change: `FirestoreDocument.CacheState` is now `FirestoreCacheState` ([#12][12]) +- Breaking change: parcelers moved to `com.otaliastudios.firestore.parcel.*` package ([#12][12]) +- Fix: do not crash exception when metadata is not present ([#12][12]) + + + +[natario1]: https://github.com/natario1 + +[12]: https://github.com/natario1/Firestore/pull/12 diff --git a/README.md b/README.md index 86b89d0..81a18ab 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ The lightweight, efficient wrapper for Firestore model data, written in Kotlin, with data-binding and Parcelable support. ```groovy -implementation 'com.otaliastudios:firestore:0.6.0' -kapt 'com.otaliastudios:firestore-compiler:0.6.0' +implementation 'com.otaliastudios:firestore:0.7.0' +kapt 'com.otaliastudios:firestore-compiler:0.7.0' ``` - Efficient and lightweight @@ -185,15 +185,15 @@ a parceler using `FirestoreDocument.registerParceler()`: class App : Application() { override fun onCreate() { - FirestoreDocument.registerParceler(GeoPoint::class, GeoPointParceler()) - FirestoreDocument.registerParceler(Whatever::class, WhateverParceler()) + registerParceler(GeoPointParceler) + registerParceler(WhateverParceler) } - class GeoPointParceler : FirestoreDocument.Parceler() { + object GeoPointParceler : FirestoreDocument.Parceler() { // ... } - class WhateverParceler : FirestoreDocument.Parceler() { + object WhateverParceler : FirestoreDocument.Parceler() { // ... } } diff --git a/build.gradle.kts b/build.gradle.kts index 342f69e..9ca7688 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { extra["libDescription"] = "The efficient wrapper for Firestore model data." - extra["libVersion"] = "0.6.0" + extra["libVersion"] = "0.7.0" extra["libGroup"] = "com.otaliastudios" extra["githubUrl"] = "https://github.com/natario1/Firestore" extra["githubGit"] = "https://github.com/natario1/Firestore.git" @@ -13,7 +13,6 @@ buildscript { extra["minSdkVersion"] = 16 extra["compileSdkVersion"] = 29 extra["targetSdkVersion"] = 29 - extra["kotlinVersion"] = "1.3.61" repositories { google() @@ -22,10 +21,9 @@ buildscript { } dependencies { - val kotlinVersion = property("kotlinVersion") as String - classpath("com.android.tools.build:gradle:3.6.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") - classpath("com.otaliastudios.tools:publisher:0.1.5") + classpath("com.android.tools.build:gradle:4.0.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0") + classpath("com.otaliastudios.tools:publisher:0.3.3") } } diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/buildSrc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 55be88e..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2020 Otalia Studios. Author: Mattia Iavarone. - */ - -plugins { - `kotlin-dsl` -} - -repositories { - // Nothing yet -} - -dependencies { - // Nothing yet -} \ No newline at end of file diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 27bfb9c..672f227 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -2,12 +2,12 @@ * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. */ -import com.otaliastudios.tools.publisher.PublisherExtension.License -import com.otaliastudios.tools.publisher.PublisherExtension.Release +import com.otaliastudios.tools.publisher.common.License +import com.otaliastudios.tools.publisher.common.Release plugins { id("kotlin") - id("maven-publisher-bintray") + id("com.otaliastudios.tools.publisher") } java { @@ -16,17 +16,12 @@ java { } dependencies { - val kotlinVersion = property("kotlinVersion") as String - api("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion") api("com.squareup:kotlinpoet:1.5.0") api("com.squareup:kotlinpoet-metadata:1.5.0") api("com.squareup:kotlinpoet-metadata-specs:1.5.0") } publisher { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" project.artifact = "firestore-compiler" project.description = property("libDescription") as String project.group = property("libGroup") as String @@ -36,4 +31,12 @@ publisher { release.version = property("libVersion") as String release.setSources(Release.SOURCES_AUTO) release.setDocs(Release.DOCS_AUTO) + bintray { + auth.user = "BINTRAY_USER" + auth.key = "BINTRAY_KEY" + auth.repo = "BINTRAY_REPO" + } + directory { + directory = "../build/maven" + } } diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 3cdff56..b2c7e89 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -34,10 +34,7 @@ dependencies { implementation(project(":firestore")) kapt(project(":compiler")) - - val kotlin = rootProject.extra["kotlinVersion"] - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin") - implementation("androidx.appcompat:appcompat:1.1.0") - implementation("androidx.core:core-ktx:1.2.0") - implementation("androidx.constraintlayout:constraintlayout:1.1.3") + implementation("androidx.appcompat:appcompat:1.2.0") + implementation("androidx.core:core-ktx:1.3.1") + implementation("androidx.constraintlayout:constraintlayout:2.0.1") } diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 50e35b8..b061986 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -2,13 +2,13 @@ * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. */ -import com.otaliastudios.tools.publisher.PublisherExtension.License -import com.otaliastudios.tools.publisher.PublisherExtension.Release +import com.otaliastudios.tools.publisher.common.License +import com.otaliastudios.tools.publisher.common.Release plugins { id("com.android.library") id("kotlin-android") - id("maven-publisher-bintray") + id("com.otaliastudios.tools.publisher") } android { @@ -19,11 +19,13 @@ android { versionName = property("libVersion") as String } - dataBinding.isEnabled = true + buildFeatures { + dataBinding = true + } sourceSets { - get("main").java.srcDirs("src/main/kotlin") - get("test").java.srcDirs("src/test/kotlin") + getByName("main").java.srcDirs("src/main/kotlin") + getByName("test").java.srcDirs("src/test/kotlin") } compileOptions { @@ -32,21 +34,21 @@ android { } buildTypes { - get("release").consumerProguardFile("proguard-rules.pro") + getByName("release").consumerProguardFile("proguard-rules.pro") + } + + kotlinOptions { + // Until the explicitApi() works in the Kotlin block... + // https://youtrack.jetbrains.com/issue/KT-37652 + freeCompilerArgs += listOf("-Xexplicit-api=strict") } } dependencies { - val kotlinVersion = property("kotlinVersion") - api("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion") - api("com.google.firebase:firebase-firestore:21.4.1") - api("com.jakewharton.timber:timber:4.7.1") + api("com.google.firebase:firebase-firestore-ktx:21.6.0") } publisher { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" project.artifact = "firestore" project.description = property("libDescription") as String project.group = property("libGroup") as String @@ -55,4 +57,12 @@ publisher { project.addLicense(License.APACHE_2_0) release.setSources(Release.SOURCES_AUTO) release.setDocs(Release.DOCS_AUTO) + bintray { + auth.user = "BINTRAY_USER" + auth.key = "BINTRAY_KEY" + auth.repo = "BINTRAY_REPO" + } + directory { + directory = "../build/maven" + } } \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt deleted file mode 100644 index 96baa5d..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. - */ - -package com.otaliastudios.firestore - -import androidx.annotation.Keep - -@Keep -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class FirestoreClass - -@Keep -public interface FirestoreMetadata { - fun create(key: String): T? - fun isNullable(key: String): Boolean - fun getBindableResource(key: String): Int? - fun createInnerType(): T? - - companion object { - const val SUFFIX = "MetadataImpl" - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt deleted file mode 100644 index 778b205..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.otaliastudios.firestore - -import com.google.android.gms.tasks.Task -import com.google.firebase.firestore.DocumentSnapshot -import com.otaliastudios.firestore.batch.FirestoreBatchWrite -import com.otaliastudios.firestore.batch.FirestoreBatchWriter -import kotlin.reflect.KClass - -@Suppress("UNCHECKED_CAST", "RedundantVisibilityModifier") -public fun DocumentSnapshot.toFirestoreDocument(type: KClass, cache: Boolean = true): T { - var needsCacheState = false - val result = if (cache) { - val cached = FirestoreDocument.CACHE.get(reference.id) as? T - if (cached == null) { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Cache miss." } - val new = type.java.newInstance() - new.clearDirt() // Clear dirtyness from init(). - FirestoreDocument.CACHE.put(reference.id, new) - new.cacheState = FirestoreDocument.CacheState.FRESH - new - } else { - if (metadata.isFromCache) { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because metadata.isFromCache." } - cached.cacheState = FirestoreDocument.CacheState.CACHED_EQUAL - } else { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Was found. We'll see if something changed." } - needsCacheState = true - /* val map = mutableMapOf() - cached.collectAllValues(map, "") - cached.cacheState = if (map.any { get(it.key) != it.value }) { - FirestoreLogger.v("Id ${reference.id} asked for cache. Was found. Using CACHED_CHANGED because some values were different.") - FirestoreDocument.CacheState.CACHED_CHANGED - } else { - FirestoreLogger.v("Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because everything matches.") - FirestoreDocument.CacheState.CACHED_EQUAL - } */ - } - cached - } - } else { - FirestoreLogger.i { "Id ${reference.id} created with no cache." } - val new = type.java.newInstance() - new.clearDirt() // Clear dirtyness from init(). - FirestoreDocument.CACHE.put(reference.id, new) - new.cacheState = FirestoreDocument.CacheState.FRESH - new - } - result.id = reference.id - result.collection = reference.parent.path - val changed = result.mergeValues(data!!, needsCacheState, reference.id) - if (needsCacheState) { - result.cacheState = if (changed) { - FirestoreLogger.v { "Id ${reference.id} Setting cache state to CACHED_CHANGED." } - FirestoreDocument.CacheState.CACHED_CHANGED - } else { - FirestoreLogger.v { "Id ${reference.id} Setting cache state to CACHED_EQUAL." } - FirestoreDocument.CacheState.CACHED_EQUAL - } - } - return result -} - -@Suppress("RedundantVisibilityModifier") -public inline fun DocumentSnapshot.toFirestoreDocument(cache: Boolean = true): T { - return toFirestoreDocument(T::class, cache) -} - -public fun firestoreListOf(vararg elements: T): FirestoreList { - return FirestoreList(elements.asList()) -} - -public fun firestoreMapOf(vararg pairs: Pair): FirestoreMap { - return FirestoreMap(pairs.toMap()) -} - -public fun batchWrite(updates: FirestoreBatchWriter.() -> Unit): Task { - return FirestoreBatchWrite().run { - perform(updates) - commit() - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt new file mode 100644 index 0000000..0bb134e --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. + */ + +package com.otaliastudios.firestore + +import androidx.annotation.Keep + +/** + * Identifies [FirestoreDocument], [FirestoreMap]s and [FirestoreList]s + * so that they can be processed by the annotation processor. + */ +@Keep +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class FirestoreClass +// Keep in sync with compiler! \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt index cebaa28..4d1db36 100755 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt @@ -4,30 +4,30 @@ package com.otaliastudios.firestore -import android.annotation.SuppressLint import android.os.Bundle -import android.util.LruCache import androidx.annotation.Keep -import com.google.android.gms.common.api.Batch import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.firebase.Timestamp import com.google.firebase.firestore.* +import com.otaliastudios.firestore.parcel.DocumentReferenceParceler +import com.otaliastudios.firestore.parcel.FieldValueParceler +import com.otaliastudios.firestore.parcel.TimestampParceler import kotlin.reflect.KClass /** * The base document class. */ @Keep -abstract class FirestoreDocument( - @get:Exclude var collection: String? = null, - @get:Exclude var id: String? = null, +public abstract class FirestoreDocument( + @get:Exclude public var collection: String? = null, + @get:Exclude public var id: String? = null, source: Map? = null ) : FirestoreMap(source = source) { @Suppress("MemberVisibilityCanBePrivate", "RedundantModalityModifier") @Exclude - final fun isNew(): Boolean { + public final fun isNew(): Boolean { return createdAt == null } @@ -40,39 +40,42 @@ abstract class FirestoreDocument( } @Exclude - fun getReference(): DocumentReference { + public fun getReference(): DocumentReference { if (id == null) throw IllegalStateException("Cant return reference for unsaved data.") return requireReference() } @get:Keep @set:Keep - var createdAt: Timestamp? by this + public var createdAt: Timestamp? by this @get:Keep @set:Keep - var updatedAt: Timestamp? by this + public var updatedAt: Timestamp? by this - internal var cacheState: CacheState = CacheState.FRESH + internal var cacheState: FirestoreCacheState = FirestoreCacheState.FRESH + @Suppress("unused") @Exclude - fun getCacheState() = cacheState + public fun getCacheState(): FirestoreCacheState = cacheState + @Suppress("unused") @Exclude - fun delete(): Task { + public fun delete(): Task { @Suppress("UNCHECKED_CAST") return getReference().delete() as Task } @Exclude - internal fun delete(batch: WriteBatch): BatchOp { + internal fun delete(batch: WriteBatch): FirestoreBatchOp { batch.delete(getReference()) - return object: BatchOp { + return object: FirestoreBatchOp { override fun notifyFailure() {} override fun notifySuccess() {} } } + @Suppress("unused") @Exclude - fun save(): Task { + public fun save(): Task { return when { isNew() -> create() else -> update() @@ -80,7 +83,7 @@ abstract class FirestoreDocument( } @Exclude - internal fun save(batch: WriteBatch): BatchOp { + internal fun save(batch: WriteBatch): FirestoreBatchOp { return when { isNew() -> create(batch) else -> update(batch) @@ -112,9 +115,9 @@ abstract class FirestoreDocument( // Add to cache NOW, then eventually revert. // This is because when reference.set() succeeds, any query listener is notified // before our onSuccessTask() is called. So a new item is created. - FirestoreDocument.CACHE.put(reference.id, this) + FirestoreCache[reference.id] = this return reference.set(map).addOnFailureListener { - FirestoreDocument.CACHE.remove(reference.id) + FirestoreCache.remove(reference.id) }.onSuccessTask { id = reference.id createdAt = Timestamp.now() @@ -125,13 +128,13 @@ abstract class FirestoreDocument( } } - private fun update(batch: WriteBatch): BatchOp { + private fun update(batch: WriteBatch): FirestoreBatchOp { if (isNew()) throw IllegalStateException("Can not update a new object. Please call create().") val map = mutableMapOf() flattenValues(map, prefix = "", dirtyOnly = true) map["updatedAt"] = FieldValue.serverTimestamp() batch.update(getReference(), map) - return object: BatchOp { + return object: FirestoreBatchOp { override fun notifyFailure() {} override fun notifySuccess() { updatedAt = Timestamp.now() @@ -140,17 +143,17 @@ abstract class FirestoreDocument( } } - private fun create(batch: WriteBatch): BatchOp { + private fun create(batch: WriteBatch): FirestoreBatchOp { if (!isNew()) throw IllegalStateException("Can not create an existing object.") val reference = requireReference() val map = collectValues(dirtyOnly = false).toMutableMap() map["createdAt"] = FieldValue.serverTimestamp() map["updatedAt"] = FieldValue.serverTimestamp() batch.set(reference, map) - FirestoreDocument.CACHE.put(reference.id, this) - return object: BatchOp { + FirestoreCache[reference.id] = this + return object: FirestoreBatchOp { override fun notifyFailure() { - FirestoreDocument.CACHE.remove(reference.id) + FirestoreCache.remove(reference.id) } override fun notifySuccess() { @@ -162,13 +165,9 @@ abstract class FirestoreDocument( } } - internal interface BatchOp { - fun notifySuccess() - fun notifyFailure() - } - + @Suppress("unused") @Exclude - fun trySave(vararg updates: Pair): Task { + public fun trySave(vararg updates: Pair): Task { if (isNew()) throw IllegalStateException("Can not trySave a new object. Please call save() first.") val reference = requireReference() val values = updates.toMap().toMutableMap() @@ -193,54 +192,6 @@ abstract class FirestoreDocument( } } - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is FirestoreDocument && - other.id == this.id && - other.collection == this.collection && - super.equals(other) - } - - companion object { - - @SuppressLint("StaticFieldLeak") - internal val FIRESTORE = FirebaseFirestore.getInstance().apply { - firestoreSettings = FirebaseFirestoreSettings.Builder() - .setTimestampsInSnapshotsEnabled(true) - .build() - } - - internal val CACHE = LruCache(100) - - private val METADATA_PROVIDERS = mutableMapOf() - - internal fun metadataProvider(klass: KClass<*>): FirestoreMetadata { - val name = klass.java.name - if (!METADATA_PROVIDERS.containsKey(name)) { - val classPackage = klass.java.`package`!!.name - val className = klass.java.simpleName - val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") - METADATA_PROVIDERS[name] = metadata.newInstance() as FirestoreMetadata - } - return METADATA_PROVIDERS[name] as FirestoreMetadata - } - - init { - FirestoreParcelers.add(DocumentReference::class, DocumentReferenceParceler) - FirestoreParcelers.add(Timestamp::class, TimestampParceler) - FirestoreParcelers.add(FieldValue::class, FieldValueParceler) - } - - fun getCached(id: String, type: KClass): T? { - @Suppress("UNCHECKED_CAST") - return CACHE.get(id) as? T - } - - inline fun getCached(id: String): T? { - return getCached(id, T::class) - } - } - override fun onWriteToBundle(bundle: Bundle) { super.onWriteToBundle(bundle) bundle.putString("id", id) @@ -253,7 +204,22 @@ abstract class FirestoreDocument( collection = bundle.getString("collection", null) } - enum class CacheState { - FRESH, CACHED_EQUAL, CACHED_CHANGED + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is FirestoreDocument && + other.id == this.id && + other.collection == this.collection && + super.equals(other) + } + + public companion object { + @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get")) + public fun getCached(id: String, type: KClass): T? + = FirestoreCache.get(id, type) + + @Suppress("unused") + @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get")) + public inline fun getCached(id: String): T? + = FirestoreCache[id] } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt index eeeb1c2..a005fe4 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt @@ -9,24 +9,34 @@ import android.os.Parcel import android.os.Parcelable import androidx.annotation.Keep import com.google.firebase.firestore.Exclude +import com.otaliastudios.firestore.parcel.readValue +import com.otaliastudios.firestore.parcel.writeValue /** - * A list implementation. Delegates to a mutable list. - * - * The point of list is dirtyness. + * Creates a [FirestoreList] for the given values. + */ +@Suppress("unused") +public fun firestoreListOf(vararg values: T): FirestoreList { + return FirestoreList(values.asList()) +} + +/** + * A [FirestoreList] can be used to represent firestore lists. + * Implements list methods by delegates to a mutable list under the hood. * - * When an item is inserted, removed, or when a inner FirestoreMap/List is changed, - * this list should be marked as dirty. + * Whenever an item is inserted, removed, or when a inner Map/List is changed, + * this list will be marked as dirty. */ @Keep -open class FirestoreList @JvmOverloads constructor( +public open class FirestoreList @JvmOverloads constructor( source: List? = null -) : /* ObservableList, MutableList by data, */Iterable, Parcelable { +) : Iterable, Parcelable { + private val log = FirestoreLogger("FirestoreList") private val data: MutableList = mutableListOf() @get:Exclude - val size get() = data.size + public val size: Int get() = data.size init { if (source != null) { @@ -94,29 +104,25 @@ open class FirestoreList @JvmOverloads constructor( } private fun createFirestoreMap(): FirestoreMap { - val map = try { onCreateFirestoreMap() } catch (e: Exception) { - FirestoreMap() - } + val map = onCreateFirestoreMap() map.clearDirt() return map } private fun createFirestoreList(): FirestoreList { - val list = try { onCreateFirestoreList() } catch (e: Exception) { - FirestoreList() - } + val list = onCreateFirestoreList() list.clearDirt() return list } protected open fun onCreateFirestoreMap(): FirestoreMap { - val provider = FirestoreDocument.metadataProvider(this::class) - return provider.createInnerType>() ?: FirestoreMap() + val metadata = this::class.metadata + return metadata?.createInnerType>() ?: FirestoreMap() } protected open fun onCreateFirestoreList(): FirestoreList { - val provider = FirestoreDocument.metadataProvider(this::class) - return provider.createInnerType>() ?: FirestoreList() + val metadata = this::class.metadata + return metadata?.createInnerType>() ?: FirestoreList() } @Suppress("UNCHECKED_CAST") @@ -167,43 +173,45 @@ open class FirestoreList @JvmOverloads constructor( // Parcelable stuff. - override fun describeContents() = 0 + override fun describeContents(): Int = 0 override fun writeToParcel(parcel: Parcel, flags: Int) { val hashcode = hashCode() parcel.writeInt(hashcode) // Write class name - FirestoreLogger.i { "List $hashcode: writing class ${this::class.java.name}" } + log.i { "List $hashcode: writing class ${this::class.java.name}" } parcel.writeString(this::class.java.name) // Write size and dirtiness - FirestoreLogger.v { "List $hashcode: writing dirty $isDirty and size $size" } + log.v { "List $hashcode: writing dirty $isDirty and size $size" } parcel.writeInt(if (isDirty) 1 else 0) parcel.writeInt(size) // Write actual data for (value in data) { - FirestoreLogger.v { "List $hashcode: writing value $value" } - FirestoreParcelers.write(parcel, value, hashcode.toString()) + log.v { "List $hashcode: writing value $value" } + parcel.writeValue(value, hashcode.toString()) } // Extra bundle val bundle = Bundle() onWriteToBundle(bundle) - FirestoreLogger.v { "List $hashcode: writing extra bundle. Size is ${bundle.size()}" } + log.v { "List $hashcode: writing extra bundle. Size is ${bundle.size()}" } parcel.writeBundle(bundle) } - companion object { + public companion object { + + private val LOG = FirestoreLogger("FirestoreList") @Suppress("unused") @JvmField - public val CREATOR = object : Parcelable.ClassLoaderCreator> { + public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreList { // This should never be called by the framework. - FirestoreLogger.e { "List: received call to createFromParcel without classLoader." } + LOG.e { "List: received call to createFromParcel without classLoader." } return createFromParcel(source, FirestoreList::class.java.classLoader!!) } @@ -211,25 +219,25 @@ open class FirestoreList @JvmOverloads constructor( val hashcode = parcel.readInt() // Read class name val klass = Class.forName(parcel.readString()!!) - FirestoreLogger.i { "List $hashcode: read class ${klass.simpleName}" } + LOG.i { "List $hashcode: read class ${klass.simpleName}" } @Suppress("UNCHECKED_CAST") val dataList = klass.newInstance() as FirestoreList // Read dirtyness and size dataList.isDirty = parcel.readInt() == 1 val count = parcel.readInt() - FirestoreLogger.v { "List $hashcode: read dirtyness ${dataList.isDirty} and size $count" } + LOG.v { "List $hashcode: read dirtyness ${dataList.isDirty} and size $count" } // Read actual data repeat(count) { - FirestoreLogger.v { "List $hashcode: reading value..." } - dataList.data.add(FirestoreParcelers.read(parcel, loader, hashcode.toString())!!) + LOG.v { "List $hashcode: reading value..." } + dataList.data.add(parcel.readValue(loader, hashcode.toString())!!) } // Extra bundle - FirestoreLogger.v { "List $hashcode: reading extra bundle." } + LOG.v { "List $hashcode: reading extra bundle." } val bundle = parcel.readBundle(loader)!! - FirestoreLogger.v { "List $hashcode: read extra bundle, size ${bundle.size()}" } + LOG.v { "List $hashcode: read extra bundle, size ${bundle.size()}" } dataList.onReadFromBundle(bundle) return dataList } @@ -246,24 +254,24 @@ open class FirestoreList @JvmOverloads constructor( // Dirtyness stuff. - fun add(element: T): Boolean { + public fun add(element: T): Boolean { data.add(element) isDirty = true return true } - fun add(index: Int, element: T) { + public fun add(index: Int, element: T) { data.add(index, element) isDirty = true } - fun remove(element: T): Boolean { + public fun remove(element: T): Boolean { val result = data.remove(element) isDirty = true return result } - operator fun set(index: Int, element: T): T { + public operator fun set(index: Int, element: T): T { val value = data.set(index, element) isDirty = true return value diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt index 252fc56..5ad2d0c 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt @@ -1,50 +1,58 @@ +@file:Suppress("unused") + package com.otaliastudios.firestore import android.util.Log -import timber.log.Timber -object FirestoreLogger { +/** + * Lazy logger used across the library. + * Use [setLevel] to configure the verbosity. + */ +public class FirestoreLogger internal constructor(private val tag: String) { - const val VERBOSE = Log.VERBOSE - const val INFO = Log.INFO - const val WARN = Log.WARN - const val ERROR = Log.ERROR + public companion object { + public const val VERBOSE: Int = Log.VERBOSE + public const val INFO: Int = Log.INFO + public const val WARN: Int = Log.WARN + public const val ERROR: Int = Log.ERROR - private var level = ERROR + private var level = WARN - fun setLevel(level: Int) { - this.level = level + @JvmStatic + public fun setLevel(level: Int) { + this.level = level + } } internal fun w(message: () -> String) { - if (level <= WARN) Timber.w(message()) + if (level <= WARN) Log.w(tag, message()) } internal fun w(throwable: Throwable, message: () -> String) { - if (level <= WARN) Timber.w(throwable, message()) + if (level <= WARN) Log.w(tag, message(), throwable) } internal fun e(message: () -> String) { - if (level <= ERROR) Timber.e(message()) + if (level <= ERROR) Log.e(tag, message()) } internal fun e(throwable: Throwable, message: () -> String) { - if (level <= ERROR) Timber.e(throwable, message()) + if (level <= ERROR) Log.e(tag, message(), throwable) } internal fun i(message: () -> String) { - if (level <= INFO) Timber.i(message()) + if (level <= INFO) Log.i(tag, message()) } - internal fun i(throwable: Throwable, message: String) { - if (level <= INFO) Timber.i(throwable, message) + internal fun i(throwable: Throwable, message: () -> String) { + if (level <= INFO) Log.i(tag, message(), throwable) } internal fun v(message: () -> String) { - if (level <= VERBOSE) Timber.v(message()) + if (level <= VERBOSE) Log.v(tag, message()) } internal fun v(throwable: Throwable, message: () -> String) { - if (level <= VERBOSE) Timber.v(throwable, message()) + if (level <= VERBOSE) Log.v(tag, message(), throwable) } } \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt index d8f267d..a9e8280 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt @@ -10,26 +10,34 @@ import android.os.Parcelable import androidx.annotation.Keep import androidx.databinding.BaseObservable import com.google.firebase.firestore.Exclude -import java.util.LinkedHashMap +import com.otaliastudios.firestore.parcel.readValue +import com.otaliastudios.firestore.parcel.writeValue import kotlin.reflect.KProperty /** - * A map implementation. Delegates to a mutable map. - * Introduce dirtyness checking for childrens. + * Creates a [FirestoreMap] for the given key-value pairs. + */ +@Suppress("unused") +public fun firestoreMapOf(vararg pairs: Pair): FirestoreMap { + return FirestoreMap(pairs.toMap()) +} + +/** + * A [FirestoreMap] can be used to represent firestore JSON-like structures. + * Implements map methods by delegating to a mutable map under the hood. */ @Keep -open class FirestoreMap( - source: Map? = null -) : BaseObservable(), /*MutableMap by data,*/ Parcelable { +public open class FirestoreMap(source: Map? = null) : BaseObservable(), Parcelable { + private val log = FirestoreLogger("FirestoreMap") private val data: MutableMap = mutableMapOf() private val dirty: MutableSet = mutableSetOf() @get:Exclude - val keys get() = data.keys + public val keys: Set get() = data.keys @get:Exclude - val size get() = data.size + public val size: Int get() = data.size init { if (source != null) { @@ -82,38 +90,34 @@ open class FirestoreMap( private fun createFirestoreMap(key: String): FirestoreMap { - val map = try { onCreateFirestoreMap(key) } catch (e: Exception) { - FirestoreMap() - } + val map = onCreateFirestoreMap(key) map.clearDirt() return map } private fun createFirestoreList(key: String): FirestoreList { - val list = try { onCreateFirestoreList(key) } catch (e: Exception) { - FirestoreList() - } + val list = onCreateFirestoreList(key) list.clearDirt() return list } protected open fun onCreateFirestoreMap(key: String): FirestoreMap { - val provider = FirestoreDocument.metadataProvider(this::class) - var candidate = provider.create>(key) - candidate = candidate ?: provider.createInnerType() + val metadata = this::class.metadata + var candidate = metadata?.create>(key) + candidate = candidate ?: metadata?.createInnerType() candidate = candidate ?: FirestoreMap() return candidate } protected open fun onCreateFirestoreList(key: String): FirestoreList { - val provider = FirestoreDocument.metadataProvider(this::class) - var candidate = provider.create>(key) - candidate = candidate ?: provider.createInnerType() + val metadata = this::class.metadata + var candidate = metadata?.create>(key) + candidate = candidate ?: metadata?.createInnerType() candidate = candidate ?: FirestoreList() return candidate } - final operator fun set(key: String, value: T) { + public operator fun set(key: String, value: T) { val result = onSet(key, value) /* if (result == null) { // Do nothing. @@ -125,14 +129,14 @@ open class FirestoreMap( } else { data[key] = result dirty.add(key) - val resource = FirestoreDocument.metadataProvider(this::class).getBindableResource(key) + val resource = this::class.metadata?.getBindableResource(key) if (resource != null) notifyPropertyChanged(resource) } } internal open fun onSet(key: String, value: T): T = value - final operator fun get(key: String): T? { + public operator fun get(key: String): T? { return if (key.contains('.')) { val first = key.split('.')[0] val second = key.removePrefix("$first.") @@ -172,9 +176,9 @@ open class FirestoreMap( var what = source[property.name] as R if (what == null) { - val provider = FirestoreDocument.metadataProvider(this::class) - if (!provider.isNullable(property.name)) { - what = provider.create(property.name)!! + val metadata = this::class.metadata + if (metadata != null && !metadata.isNullable(property.name)) { + what = metadata.create(property.name)!! source[property.name] = what // We don't want this to be dirty now! It was just retrieved, not really set. // If we leave it dirty, it would not be updated on next mergeValues(). @@ -240,7 +244,7 @@ open class FirestoreMap( internal fun mergeValues(values: Map, checkChanges: Boolean, tag: String): Boolean { var changed = false for ((key, value) in values) { - FirestoreLogger.v { "$tag mergeValues: key $key with value $value, dirty: ${isDirty(key)}" } + log.v { "$tag mergeValues: key $key with value $value, dirty: ${isDirty(key)}" } if (isDirty(key)) continue if (value is Map<*, *> && value.keys.all { it is String }) { val child = get(key) ?: createFirestoreMap(key) as T // T @@ -258,7 +262,7 @@ open class FirestoreMap( changed = changed || childChanged } else { if (checkChanges && !changed) { - FirestoreLogger.v { "$tag mergeValues: key $key comparing with value ${data[key]}" } + log.v { "$tag mergeValues: key $key comparing with value ${data[key]}" } changed = changed || value != data[key] } data[key] = value @@ -283,44 +287,46 @@ open class FirestoreMap( return result } - override fun describeContents() = 0 + override fun describeContents(): Int = 0 override fun writeToParcel(parcel: Parcel, flags: Int) { val hashcode = hashCode() parcel.writeInt(hashcode) // Write class name - FirestoreLogger.i { "Map $hashcode: writing class ${this::class.java.name}" } + log.i { "Map $hashcode: writing class ${this::class.java.name}" } parcel.writeString(this::class.java.name) // Write dirty data - FirestoreLogger.v { "Map $hashcode: writing dirty count ${dirty.size} and dirty keys ${dirty.toTypedArray().joinToString()} ${dirty.toTypedArray().size}." } + log.v { "Map $hashcode: writing dirty count ${dirty.size} and dirty keys ${dirty.toTypedArray().joinToString()} ${dirty.toTypedArray().size}." } parcel.writeInt(dirty.size) parcel.writeStringArray(dirty.toTypedArray()) - FirestoreLogger.v { "Map $hashcode: writing data size. $size" } + log.v { "Map $hashcode: writing data size. $size" } parcel.writeInt(size) for ((key, value) in data) { parcel.writeString(key) - FirestoreLogger.v { "Map $hashcode: writing value for key $key..." } - FirestoreParcelers.write(parcel, value, hashcode.toString()) + log.v { "Map $hashcode: writing value for key $key..." } + parcel.writeValue(value, hashcode.toString()) } val bundle = Bundle() onWriteToBundle(bundle) - FirestoreLogger.v { "Map $hashcode: writing extra bundle. Size is ${bundle.size()}" } + log.v { "Map $hashcode: writing extra bundle. Size is ${bundle.size()}" } parcel.writeBundle(bundle) } - companion object { + public companion object { + + private val LOG = FirestoreLogger("FirestoreMap") @Suppress("unused") @JvmField - public val CREATOR = object : Parcelable.ClassLoaderCreator> { + public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreMap { // This should never be called by the framework. - FirestoreLogger.e { "Map: received call to createFromParcel without classLoader." } + LOG.e { "Map: received call to createFromParcel without classLoader." } return createFromParcel(source, FirestoreMap::class.java.classLoader!!) } @@ -330,23 +336,23 @@ open class FirestoreMap( // Read class and create the map object. val klass = Class.forName(parcel.readString()!!) - FirestoreLogger.i { "Map $hashcode: read class ${klass.simpleName}" } + LOG.i { "Map $hashcode: read class ${klass.simpleName}" } val firestoreMap = klass.newInstance() as FirestoreMap // Read dirty data val dirty = Array(parcel.readInt()) { "" } parcel.readStringArray(dirty) - FirestoreLogger.v { "Map $hashcode: read dirty count ${dirty.size} and array ${dirty.joinToString()}" } + LOG.v { "Map $hashcode: read dirty count ${dirty.size} and array ${dirty.joinToString()}" } // Read actual data val count = parcel.readInt() - FirestoreLogger.v { "Map $hashcode: read data size $count" } + LOG.v { "Map $hashcode: read data size $count" } val values = HashMap(count) repeat(count) { val key = parcel.readString()!! - FirestoreLogger.v { "Map $hashcode: reading value for key $key..." } - values[key] = FirestoreParcelers.read(parcel, loader, hashcode.toString()) + LOG.v { "Map $hashcode: reading value for key $key..." } + values[key] = parcel.readValue(loader, hashcode.toString()) } // Set both @@ -356,9 +362,9 @@ open class FirestoreMap( firestoreMap.data.putAll(values) // Read the extra bundle - FirestoreLogger.v { "Map $hashcode: reading extra bundle." } + LOG.v { "Map $hashcode: reading extra bundle." } val bundle = parcel.readBundle(loader) - FirestoreLogger.v { "Map $hashcode: read extra bundle, size ${bundle?.size()}" } + LOG.v { "Map $hashcode: read extra bundle, size ${bundle?.size()}" } firestoreMap.onReadFromBundle(bundle!!) return firestoreMap } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt deleted file mode 100644 index ca7e984..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.otaliastudios.firestore - -import android.os.Parcel - -interface FirestoreParceler { - - /** - * Writes the [T] instance state to the [parcel]. - */ - fun write(data: T, parcel: Parcel, flags: Int) - - /** - * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it. - */ - fun create(parcel: Parcel): T -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt deleted file mode 100644 index 37baecd..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. - */ - -package com.otaliastudios.firestore - -import android.os.Parcel -import com.google.firebase.Timestamp -import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.FieldValue -import com.google.firebase.firestore.FirebaseFirestore -import kotlin.reflect.KClass - -/** - * Parceling engine. - */ -object FirestoreParcelers { - - internal val MAP = mutableMapOf>() - - fun add(klass: KClass, parceler: FirestoreParceler) { - MAP[klass.java.name] = parceler - } - - private const val NULL = 0 - private const val PARCELER = 1 - private const val VALUE = 2 - - internal fun write(parcel: Parcel, value: Any?, tag: String) { - if (value == null) { - FirestoreLogger.v { "$tag writeParcel: value is null." } - parcel.writeInt(NULL) - return - } - val klass = value::class.java.name - if (MAP.containsKey(klass)) { - FirestoreLogger.v { "$tag writeParcel: value $value will be written with parceler for class $klass." } - parcel.writeInt(PARCELER) - parcel.writeString(klass) - @Suppress("UNCHECKED_CAST") - val parceler = MAP[klass] as FirestoreParceler - parceler.write(value, parcel, 0) - return - } - try { - FirestoreLogger.v { "$tag writeParcel: value $value will be written with writeValue()." } - parcel.writeInt(VALUE) - parcel.writeValue(value) - } catch (e: Exception) { - FirestoreLogger.e(e) { "Could not write value $value. You need to add a FirestoreParceler." } - throw e - } - } - - internal fun read(parcel: Parcel, loader: ClassLoader, tag: String): Any? { - val what = parcel.readInt() - if (what == NULL) { - FirestoreLogger.v { "$tag readParcel: value is null." } - return null - } - if (what == PARCELER) { - val klass = parcel.readString()!! - FirestoreLogger.v { "$tag readParcel: value will be read by parceler $klass." } - @Suppress("UNCHECKED_CAST") - val parceler = MAP[klass] as FirestoreParceler - return parceler.create(parcel) - } - if (what == VALUE) { - val read = parcel.readValue(loader) - FirestoreLogger.v { "$tag readParcel: value was read by readValue: $read." } - return read - } - val e = IllegalStateException("$tag Error while reading parcel. Unexpected control int: $what") - FirestoreLogger.e(e) { e.message!! } - throw e - } - - -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt deleted file mode 100644 index 027ea64..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.otaliastudios.firestore.batch - -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Tasks -import com.otaliastudios.firestore.FirestoreDocument - -class FirestoreBatchWrite internal constructor() { - - private val batch = FirestoreDocument.FIRESTORE.batch() - private val writer = FirestoreBatchWriter(batch) - - fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { - action(writer) - return this - } - - fun commit(): Task { - return batch.commit().addOnSuccessListener { - writer.ops.forEach { it.notifySuccess() } - }.addOnFailureListener { - writer.ops.forEach { it.notifyFailure() } - }.onSuccessTask { - Tasks.forResult(Unit) - } - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt deleted file mode 100644 index 2520c18..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.otaliastudios.firestore.batch - -import com.google.firebase.firestore.WriteBatch -import com.otaliastudios.firestore.FirestoreDocument - -class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { - - internal val ops = mutableListOf() - - fun save(document: FirestoreDocument) { - ops.add(document.save(batch)) - } - - public fun delete(document: FirestoreDocument) { - ops.add(document.delete(batch)) - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt new file mode 100644 index 0000000..00482e4 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt @@ -0,0 +1,65 @@ +package com.otaliastudios.firestore + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.firebase.firestore.WriteBatch + + +/** + * Initiates a batch write operation. + */ +@Suppress("unused") +public fun batchWrite(updates: FirestoreBatchWriter.() -> Unit): Task { + return FirestoreBatchWrite().run { + perform(updates) + commit() + } +} + +public class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { + + internal val ops = mutableListOf() + + /** + * Adds a document save to the batch operation. + */ + @Suppress("unused") + public fun save(document: FirestoreDocument) { + ops.add(document.save(batch)) + } + + + /** + * Adds a document deletion to the batch operation. + */ + @Suppress("unused") + public fun delete(document: FirestoreDocument) { + ops.add(document.delete(batch)) + } +} + +private class FirestoreBatchWrite { + + private val batch = FIRESTORE.batch() + private val writer = FirestoreBatchWriter(batch) + + fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { + action(writer) + return this + } + + fun commit(): Task { + return batch.commit().addOnSuccessListener { + writer.ops.forEach { it.notifySuccess() } + }.addOnFailureListener { + writer.ops.forEach { it.notifyFailure() } + }.onSuccessTask { + Tasks.forResult(Unit) + } + } +} + +internal interface FirestoreBatchOp { + fun notifySuccess() + fun notifyFailure() +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt new file mode 100644 index 0000000..4bd3066 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt @@ -0,0 +1,33 @@ +package com.otaliastudios.firestore + +import android.util.LruCache +import kotlin.reflect.KClass + +public object FirestoreCache { + // TODO make size configurable + private val cache = LruCache(100) + + @Suppress("UNCHECKED_CAST") + public fun get(id: String, type: KClass): T? { + val cached = cache[id] ?: return null + require(type.isInstance(cached)) { "Cached object is not of the given type: $type / ${cached::class}"} + return cached as T + } + + @Suppress("unused") + public inline operator fun get(id: String): T? { + return get(id, T::class) + } + + internal operator fun set(id: String, value: T) { + cache.put(id, value) + } + + internal fun remove(id: String) { + cache.remove(id) + } +} + +public enum class FirestoreCacheState { + FRESH, CACHED_EQUAL, CACHED_CHANGED +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt new file mode 100644 index 0000000..6101041 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt @@ -0,0 +1,19 @@ +package com.otaliastudios.firestore + +import android.util.LruCache +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import com.otaliastudios.firestore.parcel.DocumentReferenceParceler +import com.otaliastudios.firestore.parcel.FieldValueParceler +import com.otaliastudios.firestore.parcel.TimestampParceler +import com.otaliastudios.firestore.parcel.registerParceler + +// TODO make it configurable (FirebaseApp) +internal val FIRESTORE by lazy { + Firebase.firestore.also { + // One time setup + registerParceler(DocumentReferenceParceler) + registerParceler(TimestampParceler) + registerParceler(FieldValueParceler) + } +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt new file mode 100644 index 0000000..0c633ed --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. + */ + +package com.otaliastudios.firestore + +import androidx.annotation.Keep +import kotlin.reflect.KClass + +// Keep in sync with compiler! +// And also proguard rules +@Keep +public interface FirestoreMetadata { + public fun create(key: String): T? + public fun isNullable(key: String): Boolean + public fun getBindableResource(key: String): Int? + public fun createInnerType(): T? + + public companion object { + public const val SUFFIX: String = "MetadataImpl" + } +} + +private val log = FirestoreLogger("Metadata") + +private val METADATA + = mutableMapOf() + +internal val KClass<*>.metadata : FirestoreMetadata? get() { + val name = java.name + if (!METADATA.containsKey(name)) { + try { + val classPackage = java.`package`!!.name + val className = java.simpleName + val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") + METADATA[name] = metadata.newInstance() as FirestoreMetadata + } catch (e: Exception) { + log.w(e) { "Error while fetching class metadata." } + } + } + return METADATA[name] +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt similarity index 65% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt index 0e97a3d..b9e4550 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt @@ -1,14 +1,15 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore +import com.otaliastudios.firestore.parcel.FirestoreParceler /** * Parcels a possibly null DocumentReference. - * Uses FirebaseFirestore.getInstance(). + * Note that it uses [FirebaseFirestore.getInstance] to do so. */ -object DocumentReferenceParceler: FirestoreParceler { +public object DocumentReferenceParceler: FirestoreParceler { override fun create(parcel: Parcel): DocumentReference { return FirebaseFirestore.getInstance().document(parcel.readString()!!) diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt similarity index 62% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt index 6f51f65..4e969e2 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt @@ -1,18 +1,18 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.firestore.FieldValue +import com.otaliastudios.firestore.parcel.FirestoreParceler /** - * Parcels a FieldValue + * Parcels a [FieldValue]. */ -object FieldValueParceler: FirestoreParceler { +public object FieldValueParceler: FirestoreParceler { override fun create(parcel: Parcel): FieldValue { - val what = parcel.readString() - when (what) { - "delete" -> return FieldValue.delete() - "timestamp" -> return FieldValue.serverTimestamp() + return when (val what = parcel.readString()) { + "delete" -> FieldValue.delete() + "timestamp" -> FieldValue.serverTimestamp() else -> throw RuntimeException("Unknown FieldValue value: $what") } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt new file mode 100644 index 0000000..e2f4116 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt @@ -0,0 +1,35 @@ +package com.otaliastudios.firestore.parcel + +import android.os.Parcel +import kotlin.reflect.KClass + +public interface FirestoreParceler { + + /** + * Writes the [T] instance state to the [parcel]. + */ + public fun write(data: T, parcel: Parcel, flags: Int) + + /** + * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it. + */ + public fun create(parcel: Parcel): T + + public companion object { + /** + * Registers a new [FirestoreParceler]. + */ + @Suppress("unused") + public fun register(klass: KClass, parceler: FirestoreParceler) { + registerParceler(klass, parceler) + } + + /** + * Registers a new [FirestoreParceler]. + */ + @Suppress("unused") + public inline fun register(parceler: FirestoreParceler) { + registerParceler(T::class, parceler) + } + } +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt similarity index 70% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt index 614ae4c..2612ae0 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt @@ -1,12 +1,13 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.Timestamp +import com.otaliastudios.firestore.parcel.FirestoreParceler /** * Parcels a possibly null Timestamp. */ -object TimestampParceler: FirestoreParceler { +public object TimestampParceler: FirestoreParceler { override fun create(parcel: Parcel): Timestamp { return Timestamp(parcel.readLong(), parcel.readInt()) diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt new file mode 100644 index 0000000..74b9dd9 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt @@ -0,0 +1,76 @@ +package com.otaliastudios.firestore.parcel + +import android.os.Parcel +import com.otaliastudios.firestore.FirestoreLogger +import kotlin.reflect.KClass + +private val PARCELERS = mutableMapOf>() + +/** + * Registers a new [FirestoreParceler]. + */ +public fun registerParceler(klass: KClass, parceler: FirestoreParceler) { + PARCELERS[klass.java.name] = parceler +} + +/** + * Registers a new [FirestoreParceler]. + */ +@Suppress("unused") +public inline fun registerParceler(parceler: FirestoreParceler) { + registerParceler(T::class, parceler) +} + +private val log = FirestoreLogger("Parcelers") +private const val NULL = 0 +private const val PARCELER = 1 +private const val VALUE = 2 + +internal fun Parcel.writeValue(value: Any?, tag: String) { + if (value == null) { + log.v { "$tag writeParcel: value is null." } + writeInt(NULL) + return + } + val klass = value::class.java.name + if (PARCELERS.containsKey(klass)) { + log.v { "$tag writeParcel: value $value will be written with parceler for class $klass." } + writeInt(PARCELER) + writeString(klass) + @Suppress("UNCHECKED_CAST") + val parceler = PARCELERS[klass] as FirestoreParceler + parceler.write(value, this, 0) + return + } + try { + log.v { "$tag writeParcel: value $value will be written with writeValue()." } + writeInt(VALUE) + writeValue(value) + } catch (e: Exception) { + log.e(e) { "Could not write value $value. You need to add a FirestoreParceler." } + throw e + } +} + +internal fun Parcel.readValue(loader: ClassLoader, tag: String): Any? { + val what = readInt() + if (what == NULL) { + log.v { "$tag readParcel: value is null." } + return null + } + if (what == PARCELER) { + val klass = readString()!! + log.v { "$tag readParcel: value will be read by parceler $klass." } + @Suppress("UNCHECKED_CAST") + val parceler = PARCELERS[klass] as FirestoreParceler + return parceler.create(this) + } + if (what == VALUE) { + val read = readValue(loader) + log.v { "$tag readParcel: value was read by readValue: $read." } + return read + } + val e = IllegalStateException("$tag Error while reading parcel. Unexpected control int: $what") + log.e(e) { e.message!! } + throw e +} diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt new file mode 100644 index 0000000..52ec2d0 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt @@ -0,0 +1,64 @@ +package com.otaliastudios.firestore + +import com.google.firebase.firestore.DocumentSnapshot +import kotlin.reflect.KClass + +private val log = FirestoreLogger("Snapshots") + +/** + * Converts the given [DocumentSnapshot] to a [FirestoreDocument]. + * The [cache] boolean tells whether we should inspect the cache before allocating a new object. + */ +@Suppress("unused") +public inline fun DocumentSnapshot.toFirestoreDocument(cache: Boolean = true): T { + return toFirestoreDocument(T::class, cache) +} + +/** + * Converts the given [DocumentSnapshot] to a [FirestoreDocument]. + * The [cache] boolean tells whether we should inspect the cache before allocating a new object. + */ +@Suppress("UNCHECKED_CAST") +public fun DocumentSnapshot.toFirestoreDocument(type: KClass, cache: Boolean = true): T { + var needsCacheState = false + val result = if (cache) { + val cached = FirestoreCache.get(reference.id, type) + if (cached == null) { + log.i { "Id ${reference.id} asked for cache. Cache miss." } + val new = type.java.newInstance() + new.clearDirt() // Clear dirtyness from init(). + FirestoreCache[reference.id] = new + new.cacheState = FirestoreCacheState.FRESH + new + } else { + if (metadata.isFromCache) { + log.i { "Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because metadata.isFromCache." } + cached.cacheState = FirestoreCacheState.CACHED_EQUAL + } else { + log.i { "Id ${reference.id} asked for cache. Was found. We'll see if something changed." } + needsCacheState = true + } + cached + } + } else { + log.i { "Id ${reference.id} created with no cache." } + val new = type.java.newInstance() + new.clearDirt() // Clear dirtyness from init(). + FirestoreCache[reference.id] = new + new.cacheState = FirestoreCacheState.FRESH + new + } + result.id = reference.id + result.collection = reference.parent.path + val changed = result.mergeValues(data!!, needsCacheState, reference.id) + if (needsCacheState) { + result.cacheState = if (changed) { + log.v { "Id ${reference.id} Setting cache state to CACHED_CHANGED." } + FirestoreCacheState.CACHED_CHANGED + } else { + log.v { "Id ${reference.id} Setting cache state to CACHED_EQUAL." } + FirestoreCacheState.CACHED_EQUAL + } + } + return result +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b48104..4238311 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-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip