diff --git a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt index 4b7ff2cb45..cdcfbc3af8 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -178,6 +178,8 @@ interface FhirEngine { */ suspend fun getLocalChanges(type: ResourceType, id: String): List + suspend fun getUnsyncedLocalChanges(): List + /** * Purges a resource from the database without deleting data from the server. * diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt index c6b3d0ad71..73bf852d87 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt @@ -44,6 +44,9 @@ import com.google.android.fhir.updateMeta import java.time.Instant import java.util.UUID import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -445,6 +448,11 @@ internal class DatabaseImpl( } } + /** Implementation of a parallelized map */ + suspend fun Iterable.pmap(f: suspend (A) -> B): List = coroutineScope { + map { async { f(it) } }.awaitAll() + } + companion object { /** * The name for unencrypted database. diff --git a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt index 595f433d9d..564637f1c4 100644 --- a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,8 @@ internal class FhirEngineImpl(private val database: Database, private val contex override suspend fun getLocalChanges(type: ResourceType, id: String) = withContext(Dispatchers.IO) { database.getLocalChanges(type, id) } + override suspend fun getUnsyncedLocalChanges(): List = database.getAllLocalChanges() + override suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean) = withContext(Dispatchers.IO) { database.purge(type, setOf(id), forcePurge) } diff --git a/engine/src/main/java/com/google/android/fhir/sync/FhirSyncWorker.kt b/engine/src/main/java/com/google/android/fhir/sync/FhirSyncWorker.kt index 45320ce810..88df417efe 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/FhirSyncWorker.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/FhirSyncWorker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.WorkerParameters import androidx.work.workDataOf +import ca.uhn.fhir.context.FhirContext import com.google.android.fhir.FhirEngine import com.google.android.fhir.FhirEngineProvider import com.google.android.fhir.OffsetDateTimeTypeAdapter @@ -32,11 +33,15 @@ import com.google.android.fhir.sync.upload.request.UploadRequestGeneratorFactory import com.google.gson.ExclusionStrategy import com.google.gson.FieldAttributes import com.google.gson.GsonBuilder +import java.nio.charset.StandardCharsets import java.time.OffsetDateTime import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import org.apache.commons.io.IOUtils +import org.hl7.fhir.r4.model.OperationOutcome +import retrofit2.HttpException import timber.log.Timber /** @@ -131,6 +136,8 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter } val result = synchronizer.synchronize() + if (result is SyncJobStatus.Failed) onFailedSyncJobResult(result) + val output = buildWorkData(result) // await/join is needed to collect states completely @@ -151,6 +158,34 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter } } + open fun onFailedSyncJobResult(failedSyncJobStatus: SyncJobStatus.Failed) { + try { + val jsonParser = FhirContext.forR4().newJsonParser() + val exceptions = (failedSyncJobStatus).exceptions + + exceptions.forEach { resourceSyncException -> + val operationOutcome = + jsonParser.parseResource( + IOUtils.toString( + (resourceSyncException.exception as HttpException) + .response() + ?.errorBody() + ?.byteStream(), + StandardCharsets.UTF_8, + ), + ) as OperationOutcome + + operationOutcome.issue.forEach { operationOutcome -> + Timber.e( + "SERVER ${operationOutcome.severity} - HTTP ${resourceSyncException.exception.code()} | Code - ${operationOutcome.code} | Diagnostics - ${operationOutcome.diagnostics}", + ) + } + } + } catch (e: Exception) { + Timber.e(e) + } + } + private fun buildWorkData(state: SyncJobStatus): Data { return workDataOf( // send serialized state and type so that consumer can convert it back diff --git a/engine/src/main/java/com/google/android/fhir/sync/upload/request/UploadRequestGenerator.kt b/engine/src/main/java/com/google/android/fhir/sync/upload/request/UploadRequestGenerator.kt index 86af7a4b64..d1962aa180 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/upload/request/UploadRequestGenerator.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/upload/request/UploadRequestGenerator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,7 @@ internal object UploadRequestGeneratorFactory { mode.httpVerbToUseForCreate, mode.httpVerbToUseForUpdate, mode.bundleSize, + useETagForUpload = false, ) } } diff --git a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt index 1b85a71382..7ceb038538 100644 --- a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt +++ b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,6 +203,10 @@ internal object TestFhirEngineImpl : FhirEngine { ) } + override suspend fun getUnsyncedLocalChanges(): List { + TODO("Not yet implemented") + } + override suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean) {} override suspend fun purge(type: ResourceType, ids: Set, forcePurge: Boolean) {}