Repository: google/android-fhir
Issue: Insert unqualified (logical) ids for resources instead of fully qualified ids

**Describe the bug**
Upon sync, fully qualified IDs are being inserted into the database (in Resource, Index and Local change tables). This leads to crashes when deserialised resources are updated.

**Component**
Core library

**To Reproduce**
Steps to reproduce the behavior:
1. Sync resources from the HAPI FHIR server (happening automatically in the reference app.
2. Modify any resource property e.g. `birthDate`.
3. Call `FhirEngine.update(resource)`
4. Crash while selecting old resource for update.

**Root cause**

Calling `resource.id` on synced resources read from a `Bundle` yields fully qualified id, e.g. `http://hapi.fhir.org/baseR4/Patient/1915026`. 
Upon serialisation the base URL is removed and only the logical part is stored in the JSON representation of the resource. E.g.:
```
{
  "resourceType": "Patient",
  "id": "1915026",
 ...
``` 
After deserialising this JSON, calling `resource.id` would yield `1915026`. This will not match with the stored, fully qualified id leading to crashes and the database being essentially unusable.

**Expected behavior**
Synced resources should be update-able via `FhirEngine.update(resource)`

**Screenshots**
N/A

**Smartphone (please complete the following information):**
 - Device:  Pixel4a 
 - Android version: 11

**Additional context**
None.


Changed files:
core/src/main/java/com/google/android/fhir/Util.kt, core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt, core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt, core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt, core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt, core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt

diff --git a/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
index 2671118ee..c7ea7ff2d 100644
--- a/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
+++ b/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
@@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.fhir.FhirServices
 import com.google.android.fhir.db.ResourceNotFoundInDbException
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.resource.TestingUtils
 import com.google.android.fhir.sync.FhirDataSource
 import com.google.common.truth.Truth.assertThat
@@ -158,9 +159,9 @@ class DatabaseImplTest {
     database.update(patient)
     val patientString = services.parser.encodeResourceToString(patient)
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.INSERT)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     assertThat(payload).isEqualTo(patientString)
   }
@@ -196,9 +197,11 @@ class DatabaseImplTest {
     database.update(patient)
     services.parser.encodeResourceToString(patient)
     val (token, _) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }
     database.deleteUpdates(token)
-    assertThat(database.getAllLocalChanges().none { it.second.resourceId.equals(patient.id) })
+    assertThat(
+        database.getAllLocalChanges().none { it.second.resourceId.equals(patient.logicalId) }
+      )
       .isTrue()
   }
 
@@ -207,7 +210,9 @@ class DatabaseImplTest {
     val patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json")
     database.insertRemote(patient)
     assertThat(
-        database.getAllLocalChanges().map { it.second }.none { it.resourceId.equals(patient.id) }
+        database.getAllLocalChanges().map { it.second }.none {
+          it.resourceId.equals(patient.logicalId)
+        }
       )
       .isTrue()
   }
@@ -218,7 +223,7 @@ class DatabaseImplTest {
     database.insertAllRemote(listOf(patient, TEST_PATIENT_2))
     assertThat(
         database.getAllLocalChanges().map { it.second }.none {
-          it.resourceId in listOf(patient.id, TEST_PATIENT_2_ID)
+          it.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID)
         }
       )
       .isTrue()
@@ -233,9 +238,9 @@ class DatabaseImplTest {
     val updatePatch = testingUtils.readJsonArrayFromFile("/update_patch_1.json")
     database.update(updatedPatient)
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.UPDATE)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     testingUtils.assertJsonArrayEqualsIgnoringOrder(JSONArray(payload), updatePatch)
   }
@@ -250,9 +255,9 @@ class DatabaseImplTest {
     database.update(patient)
     val updatePatch = testingUtils.readJsonArrayFromFile("/update_patch_2.json")
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.UPDATE)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     testingUtils.assertJsonArrayEqualsIgnoringOrder(JSONArray(payload), updatePatch)
   }
diff --git a/core/src/main/java/com/google/android/fhir/Util.kt b/core/src/main/java/com/google/android/fhir/Util.kt
index 1ee5c7bf2..495231428 100644
--- a/core/src/main/java/com/google/android/fhir/Util.kt
+++ b/core/src/main/java/com/google/android/fhir/Util.kt
@@ -21,6 +21,7 @@ import java.time.ZoneId
 import java.time.format.DateTimeFormatter
 import java.util.Date
 import java.util.Locale
+import org.hl7.fhir.r4.model.Resource
 
 /** Utility function to format a [Date] object using the system's default locale. */
 @SuppressLint("NewApi")
@@ -30,3 +31,10 @@ internal fun Date.toTimeZoneString(): String {
       .withZone(ZoneId.systemDefault())
   return simpleDateFormat.format(this.toInstant())
 }
+
+/**
+ * The logical (unqualified) part of the ID. For example, if the ID is
+ * "http://example.com/fhir/Patient/123/_history/456", then this value would be "123".
+ */
+internal val Resource.logicalId: String
+  get() = this.idElement.idPart
diff --git a/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
index 6486e339c..2ed49e583 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
@@ -25,6 +25,7 @@ import com.google.android.fhir.db.impl.dao.LocalChangeToken
 import com.google.android.fhir.db.impl.dao.LocalChangeUtils
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
 import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.resource.getResourceType
 import com.google.android.fhir.search.impl.Query
 import org.hl7.fhir.r4.model.Resource
@@ -79,7 +80,7 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, data
 
   @Transaction
   override fun <R : Resource> update(resource: R) {
-    val oldResource = select(resource.javaClass, resource.id)
+    val oldResource = select(resource.javaClass, resource.logicalId)
     resourceDao.update(resource)
     localChangeDao.addUpdate(oldResource, resource)
   }
@@ -157,8 +158,8 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, data
     codeSystem: String,
     codeValue: String
   ): List<R> {
-    val refs = searchByReference(clazz, reference, referenceValue).map { it.id }
-    return searchByCode(clazz, code, codeSystem, codeValue).filter { refs.contains(it.id) }
+    val refs = searchByReference(clazz, reference, referenceValue).map { it.logicalId }
+    return searchByCode(clazz, code, codeSystem, codeValue).filter { refs.contains(it.logicalId) }
   }
 
   override fun <R : Resource> search(query: Query): List<R> =
diff --git a/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt b/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
index fac940d6b..5c4c42956 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
@@ -24,6 +24,7 @@ import androidx.room.Transaction
 import ca.uhn.fhir.parser.IParser
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity.Type
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.toTimeZoneString
 import java.util.Date
 import org.hl7.fhir.r4.model.Resource
@@ -48,7 +49,7 @@ internal abstract class LocalChangeDao {
   }
 
   fun addInsert(resource: Resource) {
-    val resourceId = resource.id
+    val resourceId = resource.logicalId
     val resourceType = resource.resourceType
     val timestamp = Date().toTimeZoneString()
     val resourceString = iParser.encodeResourceToString(resource)
@@ -66,7 +67,7 @@ internal abstract class LocalChangeDao {
   }
 
   fun addUpdate(oldResource: Resource, resource: Resource) {
-    val resourceId = resource.id
+    val resourceId = resource.logicalId
     val resourceType = resource.resourceType
     val timestamp = Date().toTimeZoneString()
 
diff --git a/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt b/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
index 68f55f53f..34aa78b86 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
@@ -34,6 +34,7 @@ import com.google.android.fhir.db.impl.entities.TokenIndexEntity
 import com.google.android.fhir.db.impl.entities.UriIndexEntity
 import com.google.android.fhir.index.ResourceIndexer
 import com.google.android.fhir.index.ResourceIndices
+import com.google.android.fhir.logicalId
 import org.hl7.fhir.r4.model.Resource
 import org.hl7.fhir.r4.model.ResourceType
 
@@ -45,12 +46,16 @@ internal abstract class ResourceDao {
 
   @Transaction
   open fun update(resource: Resource) {
-    updateResource(resource.id, resource.resourceType, iParser.encodeResourceToString(resource))
+    updateResource(
+      resource.logicalId,
+      resource.resourceType,
+      iParser.encodeResourceToString(resource)
+    )
     val entity =
       ResourceEntity(
         id = 0,
         resourceType = resource.resourceType,
-        resourceId = resource.id,
+        resourceId = resource.logicalId,
         serializedResource = iParser.encodeResourceToString(resource)
       )
     val index = ResourceIndexer.index(resource)
@@ -180,7 +185,7 @@ internal abstract class ResourceDao {
       ResourceEntity(
         id = 0,
         resourceType = resource.resourceType,
-        resourceId = resource.id,
+        resourceId = resource.logicalId,
         serializedResource = iParser.encodeResourceToString(resource)
       )
     insertResource(entity)
diff --git a/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt b/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
index 964eb6168..085eb5c94 100644
--- a/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
+++ b/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
@@ -26,6 +26,7 @@ import com.google.android.fhir.index.entities.ReferenceIndex
 import com.google.android.fhir.index.entities.StringIndex
 import com.google.android.fhir.index.entities.TokenIndex
 import com.google.android.fhir.index.entities.UriIndex
+import com.google.android.fhir.logicalId
 import java.math.BigDecimal
 import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
 import org.hl7.fhir.r4.model.Base
@@ -55,7 +56,7 @@ internal object ResourceIndexer {
   fun <R : Resource> index(resource: R) = extractIndexValues(resource)
 
   private fun <R : Resource> extractIndexValues(resource: R): ResourceIndices {
-    val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.id)
+    val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.logicalId)
     resource
       .javaClass
       .fields