diff --git a/java/arcs/android/crdt/BUILD b/java/arcs/android/crdt/BUILD index fb20a3e7488..bce9b916a3d 100644 --- a/java/arcs/android/crdt/BUILD +++ b/java/arcs/android/crdt/BUILD @@ -21,6 +21,8 @@ kt_android_library( "//java/arcs/core/crdt", "//java/arcs/core/data:rawentity", "//java/arcs/core/data/util:data-util", + "//java/arcs/core/storage:reference", + "//java/arcs/core/storage:storage_key", "//third_party/java/jsr305_annotations", ], ) diff --git a/java/arcs/android/crdt/ParcelableReferencable.kt b/java/arcs/android/crdt/ParcelableReferencable.kt index 54304773ac7..22727f42aa1 100644 --- a/java/arcs/android/crdt/ParcelableReferencable.kt +++ b/java/arcs/android/crdt/ParcelableReferencable.kt @@ -13,11 +13,12 @@ package arcs.android.crdt import android.os.Parcel import android.os.Parcelable +import arcs.android.crdt.ParcelableReferencable.Type import arcs.core.common.Referencable import arcs.core.crdt.CrdtEntity import arcs.core.data.RawEntity import arcs.core.data.util.ReferencablePrimitive -import java.lang.IllegalArgumentException +import arcs.core.storage.Reference import javax.annotation.OverridingMethodsMustInvokeSuper /** @@ -34,6 +35,7 @@ interface ParcelableReferencable : Parcelable { // TODO: Add other ParcelableReferencable subclasses. RawEntity(ParcelableRawEntity.CREATOR), CrdtEntityReferenceImpl(ParcelableCrdtEntity.ReferenceImpl), + StorageReferenceImpl(ParcelableReference.CREATOR), Primitive(ParcelableReferencablePrimitive.CREATOR); override fun writeToParcel(parcel: Parcel, flags: Int) { @@ -56,6 +58,7 @@ interface ParcelableReferencable : Parcelable { // TODO: Add other ParcelableReferencable subclasses. is ParcelableRawEntity -> Type.RawEntity is ParcelableCrdtEntity.ReferenceImpl -> Type.CrdtEntityReferenceImpl + is ParcelableReference -> Type.StorageReferenceImpl is ParcelableReferencablePrimitive -> Type.Primitive else -> throw IllegalArgumentException( "Unsupported Referencable type: ${this.javaClass}" @@ -71,6 +74,7 @@ interface ParcelableReferencable : Parcelable { operator fun invoke(actual: Referencable): ParcelableReferencable = when (actual) { // TODO: Add other ParcelableReferencable subclasses. is RawEntity -> ParcelableRawEntity(actual) + is Reference -> ParcelableReference(actual) is CrdtEntity.ReferenceImpl -> ParcelableCrdtEntity.ReferenceImpl(actual) is ReferencablePrimitive<*> -> ParcelableReferencablePrimitive(actual) else -> diff --git a/java/arcs/android/crdt/ParcelableReference.kt b/java/arcs/android/crdt/ParcelableReference.kt new file mode 100644 index 00000000000..158b3526964 --- /dev/null +++ b/java/arcs/android/crdt/ParcelableReference.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Google LLC. + * + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * + * Code distributed by Google as part of this project is also subject to an additional IP rights + * grant found at + * http://polymer.github.io/PATENTS.txt + */ + +package arcs.android.crdt + +import android.os.Parcel +import android.os.Parcelable +import arcs.android.util.writeProto +import arcs.core.storage.Reference +import arcs.core.storage.StorageKeyParser + +/** Parcelable version of [Reference]. */ +data class ParcelableReference(override val actual: Reference) : ParcelableReferencable { + override fun writeToParcel(parcel: Parcel, flags: Int) { + super.writeToParcel(parcel, flags) + parcel.writeString(actual.id) + parcel.writeString(actual.storageKey.toString()) + actual.version?.let { + parcel.writeProto(it.toProto()) + } ?: { + parcel.writeTypedObject(null, flags) + }() + } + + override fun describeContents(): Int = 0 + + /* Don't use this directly, instead use ParcelableReferencable. */ + internal companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelableReference { + val id = requireNotNull(parcel.readString()) { + "Required id not found in parcel for ParcelableReference" + } + val storageKeyString = requireNotNull(parcel.readString()) { + "Required storageKey not found in parcel for ParcelableReference" + } + val versionMap = parcel.readVersionMap()?.takeIf { it.isNotEmpty() } + + return ParcelableReference( + Reference(id, StorageKeyParser.parse(storageKeyString), versionMap) + ) + } + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } +} + +/** Writes the [Reference] to the receiving [Parcel]. */ +fun Parcel.writeReference(reference: Reference, flags: Int) = + writeTypedObject(ParcelableReference(reference), flags) diff --git a/java/arcs/core/crdt/extension/ConversionExtensions.kt b/java/arcs/core/crdt/extension/ConversionExtensions.kt index 32eec3fa26a..d172436158a 100644 --- a/java/arcs/core/crdt/extension/ConversionExtensions.kt +++ b/java/arcs/core/crdt/extension/ConversionExtensions.kt @@ -19,8 +19,10 @@ import arcs.core.data.util.ReferencablePrimitive import arcs.core.util.Base64 /** Converts the [RawEntity] into a [CrdtEntity.Data] model, at the given version. */ -fun RawEntity.toCrdtEntityData(versionMap: VersionMap): CrdtEntity.Data = - CrdtEntity.Data(versionMap.copy(), this) { CrdtEntity.ReferenceImpl(it.id) } +fun RawEntity.toCrdtEntityData( + versionMap: VersionMap, + referenceBuilder: (Referencable) -> CrdtEntity.Reference = { CrdtEntity.ReferenceImpl(it.id) } +): CrdtEntity.Data = CrdtEntity.Data(versionMap.copy(), this, referenceBuilder) private fun Any?.toReferencable(): Referencable { requireNotNull(this) { "Cannot create a referencable from a null value." } diff --git a/java/arcs/core/data/Reference.kt b/java/arcs/core/data/Reference.kt index ed73952b0ca..0950460127f 100644 --- a/java/arcs/core/data/Reference.kt +++ b/java/arcs/core/data/Reference.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.Dispatchers * * Developers can check the liveness of a [Reference] using either [isAlive] or [isDead]. */ -interface Reference { +interface Reference : arcs.core.crdt.CrdtEntity.Reference { /** * Fetches the actual [Entity] value being referenced from storage. * diff --git a/java/arcs/core/storage/DirectStore.kt b/java/arcs/core/storage/DirectStore.kt index 1af5afd7441..09561478736 100644 --- a/java/arcs/core/storage/DirectStore.kt +++ b/java/arcs/core/storage/DirectStore.kt @@ -92,7 +92,7 @@ class DirectStore /* internal */ constru return when (message) { is ProxyMessage.SyncRequest -> { callbacks.value[message.id]?.invoke( - ProxyMessage.ModelUpdate(localModel.data, message.id) + ProxyMessage.ModelUpdate(getLocalData(), message.id) ) true } diff --git a/java/arcs/core/storage/ReferenceModeStore.kt b/java/arcs/core/storage/ReferenceModeStore.kt index 3942b27e6ed..e8568d5cd94 100644 --- a/java/arcs/core/storage/ReferenceModeStore.kt +++ b/java/arcs/core/storage/ReferenceModeStore.kt @@ -566,7 +566,10 @@ class ReferenceModeStore private constructor( entity, VersionMap(crdtKey to maxVersion), fieldVersionProvider - ) { CrdtEntity.ReferenceImpl(it.id) } + ) { + if (it is Reference) it + else CrdtEntity.Reference.buildReference(it) + } } companion object { diff --git a/java/arcs/core/storage/driver/Database.kt b/java/arcs/core/storage/driver/Database.kt index edf3252de5d..efdedc5acd7 100644 --- a/java/arcs/core/storage/driver/Database.kt +++ b/java/arcs/core/storage/driver/Database.kt @@ -271,7 +271,11 @@ class DatabaseDriver( )?.also { dataAndVersion = when (it) { is DatabaseData.Entity -> - it.rawEntity.toCrdtEntityData(it.versionMap) + it.rawEntity.toCrdtEntityData(it.versionMap) { refable -> + // Use the storage reference if it is one. + if (refable is Reference) refable + else CrdtEntity.Reference.buildReference(refable) + } is DatabaseData.Singleton -> it.reference.toCrdtSingletonData(it.versionMap) is DatabaseData.Collection -> @@ -361,7 +365,10 @@ class DatabaseDriver( val actualData = when (data) { is DatabaseData.Singleton -> data.reference.toCrdtSingletonData(data.versionMap) is DatabaseData.Collection -> data.values.toCrdtSetData(data.versionMap) - is DatabaseData.Entity -> data.rawEntity.toCrdtEntityData(data.versionMap) + is DatabaseData.Entity -> data.rawEntity.toCrdtEntityData(data.versionMap) { + if (it is Reference) it + else CrdtEntity.Reference.buildReference(it) + } } as Data // Stash it locally. diff --git a/java/arcs/core/storage/handle/RawEntityDereferencer.kt b/java/arcs/core/storage/handle/RawEntityDereferencer.kt index d6182dd7f52..f2ef761b5ac 100644 --- a/java/arcs/core/storage/handle/RawEntityDereferencer.kt +++ b/java/arcs/core/storage/handle/RawEntityDereferencer.kt @@ -73,17 +73,18 @@ class RawEntityDereferencer( launch { store.onProxyMessage(ProxyMessage.SyncRequest(token)) } // Only return the item if we've actually managed to pull it out of the database. - deferred.await().takeIf { it matches schema } + deferred.await().takeIf { it matches schema }?.copy(id = reference.id) } +} - private infix fun RawEntity.matches(schema: Schema): Boolean { - // Only allow empty to match if the Schema is also empty. - // TODO: Is this a correct assumption? - if (singletons.isEmpty() && collections.isEmpty()) - return schema.fields.singletons.isEmpty() && schema.fields.collections.isEmpty() +/* internal */ +infix fun RawEntity.matches(schema: Schema): Boolean { + // Only allow empty to match if the Schema is also empty. + // TODO: Is this a correct assumption? + if (singletons.isEmpty() && collections.isEmpty()) + return schema.fields.singletons.isEmpty() && schema.fields.collections.isEmpty() - // Return true if any of the RawEntity's fields are part of the Schema. - return (singletons.isEmpty() || singletons.keys.any { it in schema.fields.singletons }) && - (collections.isEmpty() || collections.keys.any { it in schema.fields.collections }) - } + // Return true if any of the RawEntity's fields are part of the Schema. + return (singletons.isEmpty() || singletons.keys.any { it in schema.fields.singletons }) && + (collections.isEmpty() || collections.keys.any { it in schema.fields.collections }) } diff --git a/javatests/arcs/android/storage/ParcelableReferenceTest.kt b/javatests/arcs/android/storage/ParcelableReferenceTest.kt new file mode 100644 index 00000000000..dbbd1cda60d --- /dev/null +++ b/javatests/arcs/android/storage/ParcelableReferenceTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2020 Google LLC. + * + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * + * Code distributed by Google as part of this project is also subject to an additional IP rights + * grant found at + * http://polymer.github.io/PATENTS.txt + */ + +package arcs.android.storage + +import android.os.Parcel +import androidx.test.ext.junit.runners.AndroidJUnit4 +import arcs.android.crdt.ParcelableRawEntity +import arcs.android.crdt.readReferencable +import arcs.android.crdt.writeReference +import arcs.core.crdt.VersionMap +import arcs.core.data.RawEntity +import arcs.core.storage.Reference +import arcs.core.storage.driver.RamDiskStorageKey +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ParcelableReferenceTest { + @Before + fun setUp() { + RamDiskStorageKey.registerParser() + } + + @Test + fun parcelableRoundtrip_works_withNullVersionMap() { + val expected = Reference("myId", RamDiskStorageKey("backingKey"), null) + + // Create a parcel and populate it with a ParcelableOperations object. + val marshalled = with(Parcel.obtain()) { + writeReference(expected, 0) + marshall() + } + + // Now unmarshall the parcel, so we can verify the contents. + val unmarshalled = with(Parcel.obtain()) { + unmarshall(marshalled, 0, marshalled.size) + setDataPosition(0) + readReferencable() + } + assertThat(unmarshalled).isEqualTo(expected) + } + + @Test + fun parcelableRoundtrip_works_withNonNullVersionMap() { + val expected = Reference( + "myId", + RamDiskStorageKey("backingKey"), + VersionMap("foo" to 1) + ) + + // Create a parcel and populate it with a ParcelableOperations object. + val marshalled = with(Parcel.obtain()) { + writeReference(expected, 0) + marshall() + } + + // Now unmarshall the parcel, so we can verify the contents. + val unmarshalled = with(Parcel.obtain()) { + unmarshall(marshalled, 0, marshalled.size) + setDataPosition(0) + readReferencable() + } + assertThat(unmarshalled).isEqualTo(expected) + } + + @Test + fun parcelableRoundtripWorks_whenReference_isPartOfRawEntity() { + val expectedReference = Reference( + "myId", + RamDiskStorageKey("backingKey"), + VersionMap("foo" to 1) + ) + val expected = RawEntity( + "myId", + singletons = mapOf("foo" to expectedReference), + collections = emptyMap() + ) + + // Create a parcel and populate it with a ParcelableOperations object. + val marshalled = with(Parcel.obtain()) { + writeTypedObject(ParcelableRawEntity(expected), 0) + marshall() + } + + // Now unmarshall the parcel, so we can verify the contents. + val unmarshalled = with(Parcel.obtain()) { + unmarshall(marshalled, 0, marshalled.size) + setDataPosition(0) + readReferencable() + } + assertThat(unmarshalled).isEqualTo(expected) + } +} diff --git a/javatests/arcs/core/storage/driver/DatabaseDriverTest.kt b/javatests/arcs/core/storage/driver/DatabaseDriverTest.kt index 32fa6ebdc0d..e9da1846db1 100644 --- a/javatests/arcs/core/storage/driver/DatabaseDriverTest.kt +++ b/javatests/arcs/core/storage/driver/DatabaseDriverTest.kt @@ -95,7 +95,12 @@ class DatabaseDriverTest { calledWithVersion = version } - assertThat(calledWithData).isEqualTo(entity.toCrdtEntityData(VersionMap())) + assertThat(calledWithData).isEqualTo( + entity.toCrdtEntityData(VersionMap()) { + if (it is Reference) it + else buildReference(it) + } + ) assertThat(calledWithVersion).isEqualTo(1) } diff --git a/javatests/arcs/core/storage/handle/RawEntityDereferencerTest.kt b/javatests/arcs/core/storage/handle/RawEntityDereferencerTest.kt new file mode 100644 index 00000000000..fe877161072 --- /dev/null +++ b/javatests/arcs/core/storage/handle/RawEntityDereferencerTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2020 Google LLC. + * + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * + * Code distributed by Google as part of this project is also subject to an additional IP rights + * grant found at + * http://polymer.github.io/PATENTS.txt + */ + +package arcs.core.storage.handle + +import arcs.core.common.Referencable +import arcs.core.crdt.CrdtEntity +import arcs.core.crdt.CrdtEntity.Reference.Companion.buildReference +import arcs.core.crdt.VersionMap +import arcs.core.data.FieldType +import arcs.core.data.RawEntity +import arcs.core.data.Schema +import arcs.core.data.SchemaFields +import arcs.core.data.util.toReferencable +import arcs.core.storage.Driver +import arcs.core.storage.DriverFactory +import arcs.core.storage.Reference +import arcs.core.storage.driver.RamDisk +import arcs.core.storage.driver.RamDiskDriverProvider +import arcs.core.storage.driver.RamDiskStorageKey +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@Suppress("EXPERIMENTAL_API_USAGE", "MapGetWithNotNullAssertionOperator") +@RunWith(JUnit4::class) +class RawEntityDereferencerTest { + // Self-referential schema. + private val schema = Schema( + emptyList(), + SchemaFields( + singletons = mapOf( + "name" to FieldType.Text, + "sibling" to FieldType.EntityRef("abc") + ), + collections = emptyMap() + ), + "abc" + ) + private val backingKey = RamDiskStorageKey("people") + private lateinit var aliceDriver: Driver + private lateinit var bobDriver: Driver + // TODO: Test with an activation factory in android-specific tests. + private val dereferencer = RawEntityDereferencer(schema, entityActivationFactory = null) + private val referenceBuilder = { refable: Referencable -> + if (refable is Reference) refable + else buildReference(refable) + } + + private val alice = RawEntity( + "aliceId", + singletons = mapOf( + "name" to "Alice Entity".toReferencable(), + "sibling" to Reference("bobId", backingKey, VersionMap()) + .also { it.dereferencer = this.dereferencer } + ), + collections = emptyMap() + ) + private val bob = RawEntity( + "bobId", + singletons = mapOf( + "name" to "Bob Entity".toReferencable(), + "sibling" to Reference("aliceId", backingKey, VersionMap()) + .also { it.dereferencer = this.dereferencer } + ), + collections = emptyMap() + ) + + @Before + fun setUp() = runBlocking { + RamDiskDriverProvider() + RamDisk.clear() + + aliceDriver = DriverFactory.getDriver(backingKey.childKeyWithComponent("aliceId"))!! + bobDriver = DriverFactory.getDriver(backingKey.childKeyWithComponent("bobId"))!! + + aliceDriver.send(CrdtEntity.Data(VersionMap("alice" to 1), alice, referenceBuilder), 1) + bobDriver.send(CrdtEntity.Data(VersionMap("bob" to 1), bob, referenceBuilder), 1) + Unit + } + + @Test + fun dereference_canDereference_friend() = runBlockingTest { + val dereferencedBob = (alice.singletons["sibling"] as Reference) + .dereference(this.coroutineContext) + assertThat(dereferencedBob!!.id).isEqualTo(bob.id) + assertThat(dereferencedBob.singletons["name"]!!.unwrap()) + .isEqualTo(bob.singletons["name"]!!.unwrap()) + assertThat(dereferencedBob.singletons["sibling"]!!.unwrap()) + .isEqualTo(bob.singletons["sibling"]!!.unwrap()) + } + + @Test + fun dereference_canDereference_sibling_of_sibling_of_sibling() = runBlockingTest { + val dereferencedBob = + (alice.singletons["sibling"] as Reference).dereference(this.coroutineContext)!! + val dereferencedAliceFromBob = + (dereferencedBob.singletons["sibling"] as Reference) + .also { it.dereferencer = dereferencer } + .dereference(this.coroutineContext)!! + val dereferencedBobFromAliceFromBob = + (dereferencedAliceFromBob.singletons["sibling"] as Reference) + .also { it.dereferencer = dereferencer } + .dereference(this.coroutineContext)!! + + assertThat(dereferencedAliceFromBob.id).isEqualTo(alice.id) + assertThat(dereferencedAliceFromBob.singletons["name"]!!.unwrap()) + .isEqualTo(alice.singletons["name"]!!.unwrap()) + assertThat(dereferencedAliceFromBob.singletons["sibling"]!!.unwrap()) + .isEqualTo(alice.singletons["sibling"]!!.unwrap()) + + assertThat(dereferencedBobFromAliceFromBob.id).isEqualTo(bob.id) + assertThat(dereferencedBobFromAliceFromBob.singletons["name"]!!.unwrap()) + .isEqualTo(bob.singletons["name"]!!.unwrap()) + assertThat(dereferencedBobFromAliceFromBob.singletons["sibling"]!!.unwrap()) + .isEqualTo(bob.singletons["sibling"]!!.unwrap()) + } + + @Test + fun rawEntity_matches_schema_isTrue_whenEntityIsEmpty_andSchemaIsEmpty() { + val entity = RawEntity(singletons = emptyMap(), collections = emptyMap()) + val schema = Schema( + emptyList(), + SchemaFields( + singletons = emptyMap(), + collections = emptyMap() + ), + "abc" + ) + + assertThat(entity matches schema).isTrue() + } + + @Test + fun rawEntity_matches_schema_isFalse_whenEntityIsEmpty_butSchemaIsNot() { + val entity = RawEntity(singletons = emptyMap(), collections = emptyMap()) + val schemaOne = Schema( + emptyList(), + SchemaFields( + singletons = mapOf("name" to FieldType.Text), + collections = emptyMap() + ), + "abc" + ) + val schemaTwo = Schema( + emptyList(), + SchemaFields( + singletons = emptyMap(), + collections = mapOf("friends" to FieldType.EntityRef("def")) + ), + "abc" + ) + + assertThat(entity matches schemaOne).isFalse() + assertThat(entity matches schemaTwo).isFalse() + } + + @Test + fun rawEntity_matches_schema_isTrue_ifSingletonIsFound_inSchema() { + val entity = RawEntity( + singletons = mapOf("name" to "Sundar".toReferencable()), + collections = emptyMap() + ) + val schema = Schema( + emptyList(), + SchemaFields( + singletons = mapOf( + "name" to FieldType.Text, + "age" to FieldType.Number + ), + collections = emptyMap() + ), + "abc" + ) + + assertThat(entity matches schema).isTrue() + } + + @Test + fun rawEntity_matches_schema_isFalse_ifNoSingletonsFound_inSchema() { + val entity = RawEntity( + singletons = mapOf("foo" to "bar".toReferencable()), + collections = emptyMap() + ) + val schema = Schema( + emptyList(), + SchemaFields( + singletons = mapOf( + "name" to FieldType.Text, + "age" to FieldType.Number + ), + collections = emptyMap() + ), + "abc" + ) + + assertThat(entity matches schema).isFalse() + } + + @Test + fun rawEntity_matches_schema_isTrue_ifCollectionIsFound_inSchema() { + val entity = RawEntity( + singletons = emptyMap(), + collections = mapOf( + "friends" to setOf( + Reference("Susan", RamDiskStorageKey("susan"), null) + ) + ) + ) + val schema = Schema( + emptyList(), + SchemaFields( + singletons = emptyMap(), + collections = mapOf("friends" to FieldType.EntityRef("def")) + ), + "abc" + ) + + assertThat(entity matches schema).isTrue() + } + + @Test + fun rawEntity_matches_schema_isTrue_ifNoCollectionsAreFound_inSchema() { + val entity = RawEntity( + singletons = emptyMap(), + collections = mapOf( + "not_friends" to setOf( + Reference("Susan", RamDiskStorageKey("susan"), null) + ) + ) + ) + val schema = Schema( + emptyList(), + SchemaFields( + singletons = emptyMap(), + collections = mapOf("friends" to FieldType.EntityRef("def")) + ), + "abc" + ) + + assertThat(entity matches schema).isFalse() + } +}