Skip to content

Commit

Permalink
Rework the sync API to make it modularized and separate from engine
Browse files Browse the repository at this point in the history
  • Loading branch information
jingtang10 committed May 18, 2021
1 parent 79c7db8 commit 8a58107
Show file tree
Hide file tree
Showing 27 changed files with 278 additions and 486 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlin_version by extra("1.4.31")
repositories {
google()
mavenCentral()
Expand All @@ -9,6 +10,7 @@ buildscript {
classpath(Plugins.androidGradlePlugin)
classpath(Plugins.kotlinGradlePlugin)
classpath(Plugins.navSafeArgsGradlePlugin)
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ object Dependencies {
const val navigation = "2.3.4"
const val recyclerView = "1.1.0"
const val room = "2.2.5"
const val workRuntimeKtx = "2.3.4"
const val workRuntimeKtx = "2.5.0"
}

object Cql {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.StringFilterModifier
import com.google.android.fhir.search.getQuery
import com.google.android.fhir.sync.FhirDataSource
import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import kotlinx.coroutines.runBlocking
Expand Down
28 changes: 11 additions & 17 deletions engine/src/main/java/com/google/android/fhir/FhirEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

package com.google.android.fhir

import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.dao.SquashedLocalChange
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.search.Search
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.Result
import com.google.android.fhir.sync.SyncConfiguration
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/** The FHIR Engine interface that handles the local storage of FHIR resources. */
interface FhirEngine {
Expand Down Expand Up @@ -55,20 +56,13 @@ interface FhirEngine {
*/
suspend fun <R : Resource> remove(clazz: Class<R>, id: String)

/**
* One time download of resources.
*
* @param syncConfiguration
* - configuration of data that needs to be synchronised
*/
suspend fun sync(syncConfiguration: SyncConfiguration): Result

/** Attempts to upload locally created and modified resources. */
suspend fun syncUpload(): Result

suspend fun periodicSync(): Result
suspend fun <R : Resource> search(search: Search): List<R>

fun updatePeriodicSyncConfiguration(syncConfig: PeriodicSyncConfiguration)
suspend fun syncDownload(download: suspend (SyncDownloadContext) -> List<Resource>)

suspend fun <R : Resource> search(search: Search): List<R>
suspend fun syncUpload(upload: (suspend (List<SquashedLocalChange>) -> List<LocalChangeToken>))
}

interface SyncDownloadContext {
suspend fun getLatestTimestamptFor(type:ResourceType):String?
}
17 changes: 2 additions & 15 deletions engine/src/main/java/com/google/android/fhir/FhirEngineBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,10 @@
package com.google.android.fhir

import android.content.Context
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.PeriodicSyncConfiguration

/** The builder for [FhirEngine] instance */
class FhirEngineBuilder constructor(dataSource: FhirDataSource, context: Context) {
private val services = FhirServices.builder(dataSource, context)

/** Sets the database file name for the FhirEngine to use. */
fun databaseName(name: String) = apply { services.databaseName(name) }

/** Instructs the FhirEngine to use an in memory database which can be useful for tests. */
internal fun inMemory() = apply { services.inMemory() }

/** Configures the FhirEngine periodic sync. */
fun periodicSyncConfiguration(config: PeriodicSyncConfiguration) = apply {
services.periodicSyncConfiguration(config)
}
class FhirEngineBuilder constructor(context: Context) {
private val services = FhirServices.builder(context)

/** Builds a new instance of the [FhirEngine]. */
fun build() = services.build().fhirEngine
Expand Down
13 changes: 2 additions & 11 deletions engine/src/main/java/com/google/android/fhir/FhirServices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,32 @@ import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.impl.DatabaseImpl
import com.google.android.fhir.impl.FhirEngineImpl
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.PeriodicSyncConfiguration

internal data class FhirServices(
val fhirEngine: FhirEngine,
val parser: IParser,
val database: Database
) {
class Builder(private val dataSource: FhirDataSource, private val context: Context) {
class Builder(private val context: Context) {
private var databaseName: String? = "fhirEngine"
private var periodicSyncConfiguration: PeriodicSyncConfiguration? = null

fun inMemory() = apply { databaseName = null }

fun databaseName(name: String) = apply { databaseName = name }

fun periodicSyncConfiguration(config: PeriodicSyncConfiguration) = apply {
periodicSyncConfiguration = config
}

fun build(): FhirServices {
val parser = FhirContext.forR4().newJsonParser()
val db = DatabaseImpl(context = context, iParser = parser, databaseName = databaseName)
val engine =
FhirEngineImpl(
database = db,
periodicSyncConfiguration = periodicSyncConfiguration,
dataSource = dataSource,
context = context
)
return FhirServices(fhirEngine = engine, parser = parser, database = db)
}
}

companion object {
fun builder(dataSource: FhirDataSource, context: Context) = Builder(dataSource, context)
fun builder(context: Context) = Builder(context)
}
}
6 changes: 3 additions & 3 deletions engine/src/main/java/com/google/android/fhir/db/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/** The interface for the FHIR resource database. */
interface Database {
internal interface Database {
/**
* Inserts a list of local `resources` into the FHIR resource database. If any of the resources
* already exists, it will be overwritten.
Expand Down Expand Up @@ -69,10 +69,10 @@ interface Database {
/**
* Insert a resource that was syncronised.
*
* @param syncedResourceEntity The synced resource
* @param syncedResources The synced resource
*/
suspend fun insertSyncedResources(
syncedResourceEntity: SyncedResourceEntity,
syncedResources: List<SyncedResourceEntity>,
resources: List<Resource>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, data

@Transaction
override suspend fun insertSyncedResources(
syncedResourceEntity: SyncedResourceEntity,
syncedResources: List<SyncedResourceEntity>,
resources: List<Resource>
) {
syncedResourceDao.insert(syncedResourceEntity)
syncedResourceDao.insertAll(syncedResources)
insertRemote(*resources.toTypedArray())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import org.hl7.fhir.r4.model.ResourceType

Expand All @@ -28,6 +29,11 @@ interface SyncedResourceDao {

@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: SyncedResourceEntity)

@Transaction
suspend fun insertAll(resources: List<SyncedResourceEntity>) {
resources.forEach { resource -> insert(resource) }
}

/**
* We will always have 1 entry for each [ResourceType] as it's the primary key, so we can limit
* the result to 1. If there is no entry for that [ResourceType] then `null` will be returned.
Expand Down
89 changes: 26 additions & 63 deletions engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,24 @@
package com.google.android.fhir.impl

import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.ResourceNotFoundException
import com.google.android.fhir.SyncDownloadContext
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.ResourceNotFoundInDbException
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.dao.SquashedLocalChange
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.execute
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.FhirSynchronizer
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.Result
import com.google.android.fhir.sync.SyncConfiguration
import com.google.android.fhir.sync.SyncWorkType
import com.google.android.fhir.toTimeZoneString
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/** Implementation of [FhirEngine]. */
class FhirEngineImpl
constructor(
private val database: Database,
private var periodicSyncConfiguration: PeriodicSyncConfiguration?,
private val dataSource: FhirDataSource,
private val context: Context
) : FhirEngine {

init {
periodicSyncConfiguration?.let { config -> triggerInitialDownload(config) }
}

internal class FhirEngineImpl
constructor(private val database: Database, private val context: Context) : FhirEngine {
override suspend fun <R : Resource> save(vararg resource: R) {
database.insert(*resource)
}
Expand All @@ -69,53 +56,29 @@ constructor(
database.delete(clazz, id)
}

override suspend fun sync(syncConfiguration: SyncConfiguration): Result {
return FhirSynchronizer(syncConfiguration, dataSource, database).sync()
}

override suspend fun syncUpload(): Result {
return FhirSynchronizer(SyncConfiguration(), dataSource, database).upload()
}

override suspend fun periodicSync(): Result {
val syncConfig =
periodicSyncConfiguration
?: throw java.lang.UnsupportedOperationException("Periodic sync configuration was not set")
val syncResult = FhirSynchronizer(syncConfig.syncConfiguration, dataSource, database).sync()
setupNextDownload(syncConfig)
return syncResult
}

override fun updatePeriodicSyncConfiguration(syncConfig: PeriodicSyncConfiguration) {
periodicSyncConfiguration = syncConfig
setupNextDownload(syncConfig)
}

override suspend fun <R : Resource> search(search: Search): List<R> {
return search.execute(database)
}

private fun setupNextDownload(syncConfig: PeriodicSyncConfiguration) {
setupDownload(syncConfig = syncConfig, withInitialDelay = true)
}

private fun triggerInitialDownload(syncConfig: PeriodicSyncConfiguration) {
setupDownload(syncConfig = syncConfig, withInitialDelay = false)
}

private fun setupDownload(syncConfig: PeriodicSyncConfiguration, withInitialDelay: Boolean) {
val workerClass = syncConfig.periodicSyncWorker
val downloadRequest =
if (withInitialDelay) {
OneTimeWorkRequest.Builder(workerClass)
.setConstraints(syncConfig.syncConstraints)
.setInitialDelay(syncConfig.repeat.interval, syncConfig.repeat.timeUnit)
.build()
} else {
OneTimeWorkRequest.Builder(workerClass).setConstraints(syncConfig.syncConstraints).build()
override suspend fun syncDownload(download: suspend (SyncDownloadContext) -> List<Resource>) {
val stuff =
download(
object : SyncDownloadContext {
override suspend fun getLatestTimestamptFor(type: ResourceType) =
database.lastUpdate(type)
}
)

val timeStamps =
stuff.groupBy { it.resourceType }.entries.map {
SyncedResourceEntity(it.key, it.value.last().meta.lastUpdated.toTimeZoneString())
}
database.insertSyncedResources(timeStamps, stuff)
}

WorkManager.getInstance(context)
.enqueueUniqueWork(SyncWorkType.DOWNLOAD.workerName, ExistingWorkPolicy.KEEP, downloadRequest)
override suspend fun syncUpload(
upload: (suspend (List<SquashedLocalChange>) -> List<LocalChangeToken>)
) {
upload(database.getAllLocalChanges()).forEach { database.deleteUpdates(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import com.google.android.fhir.db.Database
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

suspend fun <R : Resource> Search.execute(database: Database): List<R> {
internal suspend fun <R : Resource> Search.execute(database: Database): List<R> {
return database.search(getQuery())
}

Expand Down
57 changes: 57 additions & 0 deletions engine/src/main/java/com/google/android/fhir/sync/Config.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.google.android.fhir.sync

import androidx.work.Constraints
import java.net.URLEncoder
import java.util.concurrent.TimeUnit
import org.hl7.fhir.r4.model.ResourceType

/**
* Class that holds what type of resources we need to synchronise and what are the parameters of
* that type. e.g. we only want to synchronise patients that live in United States
* `ResourceSyncParams(ResourceType.Patient, mapOf("address-country" to "United States")`
*/
typealias ParamMap = Map<String, String>
typealias ResourceSyncParams = Map<ResourceType, ParamMap>

object SyncDataParams {
const val SORT_KEY = "_sort"
const val LAST_UPDATED_KEY = "_lastUpdated"
const val ADDRESS_COUNTRY_KEY = "address-country"
const val LAST_UPDATED_ASC_VALUE = "_lastUpdated"
}

/** Configuration for synchronization. */
data class SyncConfiguration(
/** Data that needs to be synchronised */
val resourceSyncParams: List<ResourceSyncParams> = emptyList(),
/**
* true if the SDK needs to retry a failed sync attempt, false otherwise If this is set to true,
* then the result of the sync will be reported after the retry.
*/
val retry: Boolean = false
)

/** Configuration for period synchronisation */
class PeriodicSyncConfiguration(
/**
* Constraints that specify the requirements needed before the synchronisation is triggered. E.g.
* network type (Wifi, 3G etc), the device should be charging etc.
*/
val syncConstraints: Constraints = Constraints.Builder().build(),

/** The interval at which the sync should be triggered in */
val repeat: RepeatInterval
)

data class RepeatInterval(
/** The interval at which the sync should be triggered in */
val interval: Long,
/** The time unit for the repeat interval */
val timeUnit: TimeUnit
)

fun ParamMap.concatParams(): String {
return this.entries.joinToString("&") { (key, value) ->
"$key=${URLEncoder.encode(value, "UTF-8")}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.hl7.fhir.r4.model.Resource
* Interface for an abstraction of retrieving static data from a network source. The data can be
* retrieved in pages and each data retrieval is an expensive operation.
*/
interface FhirDataSource {
interface DataSource {

/**
* Implement this method to load remote data based on a url [path]. A service base url is of the
Expand Down
Loading

0 comments on commit 8a58107

Please sign in to comment.