Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Patches: changes to be uploaded to the server #2156

Merged
merged 13 commits into from
Sep 12, 2023
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest {
}

@Test
fun `shouldHideErrorTextviewInHeader`() {
fun shouldHideErrorTextviewInHeader() {
val questionnaireItem = answerOptions(true, "Coding 1")
questionnaireItem.addExtension(openChoiceType)
val questionnaireViewItem =
Expand Down
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class FhirApplication : Application(), DataCaptureConfig.Provider {
dataCaptureConfig =
DataCaptureConfig().apply {
urlResolver = ReferenceUrlResolver(this@FhirApplication as Context)
xFhirQueryResolver = XFhirQueryResolver { fhirEngine.search(it).map { it.resource } }
xFhirQueryResolver = XFhirQueryResolver { it -> fhirEngine.search(it).map { it.resource } }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.DateProvider
import com.google.android.fhir.FhirServices
import com.google.android.fhir.LocalChange
import com.google.android.fhir.LocalChangeToken
import com.google.android.fhir.SearchResult
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.LOCAL_LAST_UPDATED_PARAM
import com.google.android.fhir.search.Operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.google.android.fhir

import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.search.Search
import com.google.android.fhir.sync.ConflictResolver
import java.time.OffsetDateTime
Expand Down
19 changes: 16 additions & 3 deletions engine/src/main/java/com/google/android/fhir/LocalChange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.google.android.fhir

import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import java.time.Instant
import org.hl7.fhir.r4.model.Resource

Expand All @@ -43,11 +43,24 @@ data class LocalChange(
enum class Type(val value: Int) {
INSERT(1), // create a new resource. payload is the entire resource json.
UPDATE(2), // patch. payload is the json patch.
DELETE(3), // delete. payload is empty string.
NO_OP(4); // no-op. Discard
DELETE(3); // delete. payload is empty string.

companion object {
fun from(input: Int): Type = values().first { it.value == input }
}
}
}

/** Method to convert LocalChangeEntity to LocalChange instance. */
internal fun LocalChangeEntity.toLocalChange(): LocalChange =
LocalChange(
resourceType,
resourceId,
versionId,
timestamp,
LocalChange.Type.from(type.value),
payload,
LocalChangeToken(listOf(id))
)

data class LocalChangeToken(val ids: List<Long>)
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
package com.google.android.fhir.db

import com.google.android.fhir.LocalChange
import com.google.android.fhir.LocalChangeToken
import com.google.android.fhir.db.impl.dao.IndexedIdAndResource
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.ResourceEntity
import com.google.android.fhir.search.SearchQuery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.DatabaseErrorStrategy
import com.google.android.fhir.LocalChange
import com.google.android.fhir.LocalChangeToken
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABASE_NAME
import com.google.android.fhir.db.impl.dao.IndexedIdAndResource
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.dao.toLocalChange
import com.google.android.fhir.db.impl.entities.ResourceEntity
import com.google.android.fhir.index.ResourceIndexer
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.SearchQuery
import com.google.android.fhir.toLocalChange
import java.time.Instant
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import ca.uhn.fhir.parser.IParser
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonpatch.diff.JsonDiff
import com.google.android.fhir.LocalChangeToken
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.LocalChangeEntity.Type
import com.google.android.fhir.db.impl.entities.ResourceEntity
Expand All @@ -30,6 +34,7 @@ import java.time.Instant
import java.util.Date
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.json.JSONArray
import timber.log.Timber

/**
Expand Down Expand Up @@ -76,11 +81,7 @@ internal abstract class LocalChangeDao {
)
}
val jsonDiff =
LocalChangeUtils.diff(
iParser,
iParser.parseResource(oldEntity.serializedResource) as Resource,
resource
)
diff(iParser, iParser.parseResource(oldEntity.serializedResource) as Resource, resource)
if (jsonDiff.length() == 0) {
Timber.i(
"New resource ${resource.resourceType}/${resource.id} is same as old resource. " +
Expand Down Expand Up @@ -189,3 +190,42 @@ internal abstract class LocalChangeDao {

class InvalidLocalChangeException(message: String?) : Exception(message)
}

/** Calculates the JSON patch between two [Resource] s. */
internal fun diff(parser: IParser, source: Resource, target: Resource): JSONArray {
val objectMapper = ObjectMapper()
return getFilteredJSONArray(
JsonDiff.asJson(
objectMapper.readValue(parser.encodeResourceToString(source), JsonNode::class.java),
objectMapper.readValue(parser.encodeResourceToString(target), JsonNode::class.java)
)
)
}

/**
* This function returns the json diff as a json array of operation objects. We remove the "/meta"
* and "/text" paths as they cause path not found issue when we update the resource. They are
* usually present in the downloaded resource object but are missing in the edited object as these
* aren't supposed to be edited. Thus, the Json diff creates a DELETE- OP for "/meta" and "/text"
* and causes the issue with server update.
*
* An unfiltered JSON Array for family name update looks like
* ```
* [{"op":"remove","path":"/meta"}, {"op":"remove","path":"/text"},
* {"op":"replace","path":"/name/0/family","value":"Nucleus"}]
* ```
*
* A filtered JSON Array for family name update looks like
* ```
* [{"op":"replace","path":"/name/0/family","value":"Nucleus"}]
* ```
*/
private fun getFilteredJSONArray(jsonDiff: JsonNode) =
with(JSONArray(jsonDiff.toString())) {
val ignorePaths = setOf("/meta", "/text")
return@with JSONArray(
(0 until length())
.map { optJSONObject(it) }
.filterNot { jsonObject -> ignorePaths.any { jsonObject.optString("path").startsWith(it) } }
)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import android.content.Context
import com.google.android.fhir.DatastoreUtil
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.LocalChange
import com.google.android.fhir.LocalChangeToken
import com.google.android.fhir.SearchResult
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.count
Expand All @@ -31,7 +31,6 @@ import com.google.android.fhir.sync.ConflictResolver
import com.google.android.fhir.sync.Resolved
import java.time.OffsetDateTime
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.OffsetDateTimeTypeAdapter
import com.google.android.fhir.sync.download.DownloaderImpl
import com.google.android.fhir.sync.upload.SquashedChangesUploadWorkManager
import com.google.android.fhir.sync.upload.UploadWorkManager
import com.google.android.fhir.sync.upload.UploaderImpl
import com.google.android.fhir.sync.upload.patch.SquashedChangesUploadWorkManager
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.GsonBuilder
Expand Down
Loading