-
Notifications
You must be signed in to change notification settings - Fork 293
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add ResourceConsolidator * Add ResourceConsolidator only with no upload mode * only keep one consolidator * spotless * add kdocs * update docos
- Loading branch information
1 parent
f949093
commit 0bbdeb9
Showing
2 changed files
with
129 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
engine/src/main/java/com/google/android/fhir/sync/upload/ResourceConsolidator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* Copyright 2023 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.google.android.fhir.sync.upload | ||
|
||
import com.google.android.fhir.LocalChangeToken | ||
import com.google.android.fhir.db.Database | ||
import org.hl7.fhir.r4.model.Bundle | ||
import org.hl7.fhir.r4.model.Resource | ||
import org.hl7.fhir.r4.model.ResourceType | ||
import timber.log.Timber | ||
|
||
/** | ||
* Represents a mechanism to consolidate resources after they are uploaded. | ||
* | ||
* INTERNAL ONLY. This interface should NEVER been exposed as an external API because it works | ||
* together with other components in the upload package to fulfill a specific upload strategy. After | ||
* a resource is uploaded to a remote FHIR server and a response is returned, we need to consolidate | ||
* any changes in the database, Examples of this would be, updating the lastUpdated timestamp field, | ||
* or deleting the local change from the database, or updating the resource IDs and payloads to | ||
* correspond with the server’s feedback. | ||
*/ | ||
internal fun interface ResourceConsolidator { | ||
|
||
/** Consolidates the local change token with the provided response from the FHIR server. */ | ||
suspend fun consolidate(localChangeToken: LocalChangeToken, response: Resource) | ||
} | ||
|
||
/** Default implementation of [ResourceConsolidator] that uses the database to aid consolidation. */ | ||
internal class DefaultResourceConsolidator(private val database: Database) : ResourceConsolidator { | ||
|
||
override suspend fun consolidate(localChangeToken: LocalChangeToken, response: Resource) { | ||
database.deleteUpdates(localChangeToken) | ||
when (response) { | ||
is Bundle -> updateVersionIdAndLastUpdated(response) | ||
else -> updateVersionIdAndLastUpdated(response) | ||
} | ||
} | ||
|
||
private suspend fun updateVersionIdAndLastUpdated(bundle: Bundle) { | ||
when (bundle.type) { | ||
Bundle.BundleType.TRANSACTIONRESPONSE -> { | ||
bundle.entry.forEach { | ||
when { | ||
it.hasResource() -> updateVersionIdAndLastUpdated(it.resource) | ||
it.hasResponse() -> updateVersionIdAndLastUpdated(it.response) | ||
} | ||
} | ||
} | ||
else -> { | ||
// Leave it for now. | ||
Timber.i("Received request to update meta values for ${bundle.type}") | ||
} | ||
} | ||
} | ||
|
||
private suspend fun updateVersionIdAndLastUpdated(response: Bundle.BundleEntryResponseComponent) { | ||
if (response.hasEtag() && response.hasLastModified() && response.hasLocation()) { | ||
response.resourceIdAndType?.let { (id, type) -> | ||
database.updateVersionIdAndLastUpdated( | ||
id, | ||
type, | ||
getVersionFromETag(response.etag), | ||
response.lastModified.toInstant(), | ||
) | ||
} | ||
} | ||
} | ||
|
||
private suspend fun updateVersionIdAndLastUpdated(resource: Resource) { | ||
if (resource.hasMeta() && resource.meta.hasVersionId() && resource.meta.hasLastUpdated()) { | ||
database.updateVersionIdAndLastUpdated( | ||
resource.id, | ||
resource.resourceType, | ||
resource.meta.versionId, | ||
resource.meta.lastUpdated.toInstant(), | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* FHIR uses weak ETag that look something like W/"MTY4NDMyODE2OTg3NDUyNTAwMA", so we need to | ||
* extract version from it. See https://hl7.org/fhir/http.html#Http-Headers. | ||
*/ | ||
private fun getVersionFromETag(eTag: String) = | ||
// The server should always return a weak etag that starts with W, but if it server returns a | ||
// strong tag, we store it as-is. The http-headers for conditional upload like if-match will | ||
// always add value as a weak tag. | ||
if (eTag.startsWith("W/")) { | ||
eTag.split("\"")[1] | ||
} else { | ||
eTag | ||
} | ||
|
||
/** | ||
* May return a Pair of versionId and resource type extracted from the | ||
* [Bundle.BundleEntryResponseComponent.location]. | ||
* | ||
* [Bundle.BundleEntryResponseComponent.location] may be: | ||
* 1. absolute path: `<server-path>/<resource-type>/<resource-id>/_history/<version>` | ||
* 2. relative path: `<resource-type>/<resource-id>/_history/<version>` | ||
*/ | ||
private val Bundle.BundleEntryResponseComponent.resourceIdAndType: Pair<String, ResourceType>? | ||
get() = | ||
location | ||
?.split("/") | ||
?.takeIf { it.size > 3 } | ||
?.let { it[it.size - 3] to ResourceType.fromCode(it[it.size - 4]) } | ||
} |