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

Quirks patches [DNM] #9

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7a45e18
New API to support saving local only resources
ndegwamartin Aug 23, 2023
0cae350
API access for data in the LocalChangeEntity table
ndegwamartin Aug 23, 2023
bb1f2eb
Add a SyncJobStatus result callback amid Sync retries
ndegwamartin Aug 23, 2023
49ca6fa
Merge branch 'master' into quirks-patches
ndegwamartin Sep 13, 2023
95633d2
Refactor SquashedLocalChanges to LocalChanges
ndegwamartin Sep 14, 2023
8de5078
Merge branch 'master' into quirks-patches
f-odhiambo Sep 14, 2023
b9ded79
Merge branch 'master' into quirks-patches
ndegwamartin Sep 20, 2023
1721a4a
Revert "New API to support saving local only resources"
ndegwamartin Sep 20, 2023
110c322
Merge branch 'master' into quirks-patches
ndegwamartin Sep 21, 2023
e325ba4
Merge branch 'master' into quirks-patches
ndegwamartin Oct 13, 2023
c9ca366
Disable eTAGs for Uploads
ndegwamartin Oct 13, 2023
136833a
Apply Spotless
ndegwamartin Oct 13, 2023
ea0ebef
Merge branch 'master' into quirks-patches
ndegwamartin Oct 30, 2023
de504eb
Merge branch 'master' into quirks-patches
Rkareko Nov 24, 2023
8c43299
Merge master into quirks-patches
Rkareko Dec 20, 2023
6ebc36d
Disable eTAGs for Uploads
Rkareko Dec 20, 2023
320ee39
Merge branch 'master' into quirks-patches
ndegwamartin Feb 7, 2024
65a342d
Merge branch 'master' into quirks-patches
ndegwamartin Feb 9, 2024
e6439e3
Update Clinical Reasoning version to PRE9
ndegwamartin Feb 9, 2024
7097dfb
Expose overloaded API for evaluateMeasure
ndegwamartin Feb 9, 2024
1942489
Merge branch 'master' into quirks-patches
ndegwamartin Mar 19, 2024
c3d476f
Merge branch 'master' into quirks-patches
ndegwamartin Mar 22, 2024
a3932bc
Merge branch 'master' into quirks-patches
ndegwamartin Mar 27, 2024
6dee127
Merge branch 'master' into quirks-patches
ndegwamartin Apr 19, 2024
7a75afb
Merge branch 'master' into quirks-patches
ndegwamartin Jun 21, 2024
6a669eb
Merge branch 'master' into quirks-patches
ndegwamartin Jul 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ object Dependencies {
object Versions {

object Cql {
const val clinicalReasoning = "3.0.0-PRE9-SNAPSHOT"
const val clinicalReasoning = "3.0.0-PRE9"
}

const val androidFhirCommon = "0.1.0-alpha05"
Expand Down
2 changes: 2 additions & 0 deletions engine/src/main/java/com/google/android/fhir/FhirEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ interface FhirEngine {
*/
suspend fun getLocalChanges(type: ResourceType, id: String): List<LocalChange>

suspend fun getUnsyncedLocalChanges(): List<LocalChange>

/**
* Purges a resource from the database without deleting data from the server.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ internal class FhirEngineImpl(private val database: Database, private val contex
return database.getLocalChanges(type, id)
}

// FhirEngineImpl.kt
override suspend fun getUnsyncedLocalChanges(): List<LocalChange> = database.getAllLocalChanges()

override suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean) {
database.purge(type, setOf(id), forcePurge)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -135,6 +140,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
Expand All @@ -155,6 +162,33 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
}
}

open fun onFailedSyncJobResult(failedSyncJobStatus: SyncJobStatus.Failed) {
try {
CoroutineScope(Dispatchers.IO).launch {
val jsonParser = FhirContext.forR4().newJsonParser()

(failedSyncJobStatus).exceptions.filterIsInstance<HttpException>().forEach {
resourceSyncHTTPException ->
val operationOutcome =
jsonParser.parseResource(
IOUtils.toString(
resourceSyncHTTPException.response()?.errorBody()?.byteStream(),
StandardCharsets.UTF_8,
),
) as OperationOutcome

operationOutcome.issue.forEach { operationOutcome ->
Timber.e(
"SERVER ${operationOutcome.severity} - HTTP ${resourceSyncHTTPException.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ internal object UploadRequestGeneratorFactory {
mode.httpVerbToUseForCreate,
mode.httpVerbToUseForUpdate,
mode.bundleSize,
useETagForUpload = false
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ internal object TestFhirEngineImpl : FhirEngine {
)
}

override suspend fun getUnsyncedLocalChanges(): List<LocalChange> {
TODO("Not yet implemented")
}

override suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean) {}

override suspend fun purge(type: ResourceType, ids: Set<String>, forcePurge: Boolean) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.google.android.fhir.workflow;

import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType;
import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider;
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor;
import org.opencds.cqf.fhir.cr.measure.r4.R4RepositorySubjectProvider;
import org.opencds.cqf.fhir.utility.repository.FederatedRepository;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;

import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;

public class FhirEngineR4MeasureProcessor extends R4MeasureProcessor {
private static String FIELD_SUBJECT_PROVIDER="subjectProvider";
private Repository repository;

public FhirEngineR4MeasureProcessor(Repository repository, MeasureEvaluationOptions measureEvaluationOptions) {
super(repository, measureEvaluationOptions);
this.repository = repository;
}

public FhirEngineR4MeasureProcessor(Repository repository, MeasureEvaluationOptions measureEvaluationOptions, SubjectProvider subjectProvider) {
super(repository, measureEvaluationOptions, subjectProvider);
this.repository = repository;
}

@Override
public MeasureReport evaluateMeasure(Measure measure, String periodStart, String periodEnd, String reportType, List<String> subjectIds, IBaseBundle additionalData, MeasureEvalType evalType) {
var actualRepo = this.repository;
if (additionalData != null) {
actualRepo = new FederatedRepository(
this.repository, new InMemoryFhirRepository(this.repository.fhirContext(), additionalData));
}

SubjectProvider subjectProvider = getSubjectProvider();
var subjects = subjectProvider.getSubjects(actualRepo, evalType, subjectIds).collect(Collectors.toList());
return super.evaluateMeasure( measure, periodStart, periodEnd, reportType, subjects, additionalData, evalType) ;
}


/***
* We have two constructors that could result in different subject providers. So for this field we will use reflection
* @return [SubjectProvider] the SubjectProvider
*/
public SubjectProvider getSubjectProvider(){
SubjectProvider subjectProvider;
try {
Field field = this.getClass().getSuperclass().getDeclaredField(FIELD_SUBJECT_PROVIDER);
field.setAccessible(true);
subjectProvider = (SubjectProvider) field.get(this);
}catch (Exception e){
subjectProvider = null;
}
return subjectProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ import org.hl7.fhir.instance.model.api.IBaseParameters
import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.model.CanonicalType
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.Measure
import org.hl7.fhir.r4.model.MeasureReport
import org.hl7.fhir.r4.model.Parameters
import org.hl7.fhir.r4.model.PlanDefinition
import org.hl7.fhir.r4.model.Reference
import org.opencds.cqf.fhir.cql.EvaluationSettings
import org.opencds.cqf.fhir.cql.LibraryEngine
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions
import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType
import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType
import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor
import org.opencds.cqf.fhir.cr.plandefinition.r4.PlanDefinitionProcessor
import org.opencds.cqf.fhir.utility.monad.Eithers
import org.opencds.cqf.fhir.utility.repository.ProxyRepository
Expand Down Expand Up @@ -68,7 +69,8 @@ internal constructor(

private val libraryProcessor = LibraryEngine(repository, evaluationSettings)

private val measureProcessor = R4MeasureProcessor(repository, measureEvaluationOptions)
private val measureProcessor: FhirEngineR4MeasureProcessor =
FhirEngineR4MeasureProcessor(repository, measureEvaluationOptions)
private val planDefinitionProcessor = PlanDefinitionProcessor(repository, evaluationSettings)

/**
Expand Down Expand Up @@ -148,6 +150,55 @@ internal constructor(
return report
}

/**
* Generates a [MeasureReport] based on the provided inputs.
*
* NOTE: The API may internally result in a blocking IO operation. The user should call the API
* from a worker thread or it may throw [BlockingMainThreadException] exception.
*/
@WorkerThread
fun evaluateMeasure(
measure: Measure,
start: String,
end: String,
reportType: String,
subjectId: String? = null,
practitioner: String? = null,
additionalData: IBaseBundle? = null,
): MeasureReport {
val subject =
if (!practitioner.isNullOrBlank()) {
checkAndAddType(practitioner, "Practitioner")
} else if (!subjectId.isNullOrBlank()) {
checkAndAddType(subjectId, "Patient")
} else {
// List of null is required to run population-level measures
null
}
val evalType =
MeasureEvalType.fromCode(reportType)
.orElse(
if (!subjectId.isNullOrEmpty()) MeasureEvalType.SUBJECT else MeasureEvalType.POPULATION,
) as MeasureEvalType

val report =
measureProcessor.evaluateMeasure(
/* measure = */ measure,
/* periodStart = */ start,
/* periodEnd = */ end,
/* reportType = */ reportType,
/* subjectIds = */ if (subject.isNullOrEmpty()) listOf() else listOf(subject),
/* additionalData = */ additionalData,
/* evalType = */ evalType,
)

// add subject reference for non-individual reportTypes
if (report.type.name == MeasureReportType.SUMMARY.name && !subject.isNullOrBlank()) {
report.setSubject(Reference(subject))
}
return report
}

/**
* Generates a [CarePlan] based on the provided inputs.
*
Expand Down
Loading