From 29ebb950e81e5ee1b3f2f63e8387f7ec46ffeded Mon Sep 17 00:00:00 2001 From: StevenXLi Date: Wed, 14 Dec 2022 18:43:11 -0500 Subject: [PATCH] 4360 bulk export questionnaireresponses should get picked when author is not empty (#4361) * added failing test * implemented solution * added more tests * added changelog * changed the implementation, now extended to get all the patient based search params for a given resource instead of 2 in a fixed list of resources * added test for patient bulk export for resources not in patient compartment, fixed implementation to pass test Co-authored-by: Steven Li --- .../ca/uhn/fhir/util/SearchParameterUtil.java | 25 +++- ...d-get-picked-when-author-is-not-empty.yaml | 5 + .../export/svc/JpaBulkExportProcessor.java | 24 ++-- .../uhn/fhir/jpa/bulk/BulkDataExportTest.java | 113 ++++++++++++++++++ 4 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4360-bulk-export-questionnaireresponses-should-get-picked-when-author-is-not-empty.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java index 4e5314704fe..4e0358f4fca 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SearchParameterUtil.java @@ -59,7 +59,7 @@ public class SearchParameterUtil { * Given the resource type, fetch its patient-based search parameter name * 1. Attempt to find one called 'patient' * 2. If that fails, find one called 'subject' - * 3. If that fails, find find by Patient Compartment. + * 3. If that fails, find one by Patient Compartment. * 3.1 If that returns >1 result, throw an error * 3.2 If that returns 1 result, return it */ @@ -76,6 +76,29 @@ public class SearchParameterUtil { return Optional.ofNullable(myPatientSearchParam); } + /** + * Given the resource type, fetch all its patient-based search parameter name that's available + */ + public static Set getPatientSearchParamsForResourceType(FhirContext theFhirContext, String theResourceType) { + RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType); + + List searchParams = new ArrayList<>(runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient")); + // add patient search parameter for resources that's not in the compartment + RuntimeSearchParam myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient"); + if (myPatientSearchParam != null) { + searchParams.add(myPatientSearchParam); + } + RuntimeSearchParam mySubjectSearchParam = runtimeResourceDefinition.getSearchParam("subject"); + if (mySubjectSearchParam != null) { + searchParams.add(mySubjectSearchParam); + } + if (searchParams == null || searchParams.size() == 0) { + String errorMessage = String.format("Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter", runtimeResourceDefinition.getId()); + throw new IllegalArgumentException(Msg.code(2222) + errorMessage); + } + // deduplicate list of searchParams and get their names + return searchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.toSet()); + } /** * Search the resource definition for a compartment named 'patient' and return its related Search Parameter. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4360-bulk-export-questionnaireresponses-should-get-picked-when-author-is-not-empty.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4360-bulk-export-questionnaireresponses-should-get-picked-when-author-is-not-empty.yaml new file mode 100644 index 00000000000..fc576d8ee33 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4360-bulk-export-questionnaireresponses-should-get-picked-when-author-is-not-empty.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 4360 +title: "Previously, Patient Bulk Export on certain resources with reference to patient only in author or performer did not get exported. +This has been fixed." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java index 393753e1c50..95a44129858 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java @@ -146,21 +146,23 @@ public class JpaBulkExportProcessor implements IBulkExportProcessor { throw new IllegalStateException(Msg.code(797) + errorMessage); } - List maps = myBulkExportHelperSvc.createSearchParameterMapsForResourceType(def, theParams); - String patientSearchParam = getPatientSearchParamForCurrentResourceType(theParams.getResourceType()).getName(); + Set patientSearchParams = SearchParameterUtil.getPatientSearchParamsForResourceType(myContext, theParams.getResourceType()); - for (SearchParameterMap map : maps) { - //Ensure users did not monkey with the patient compartment search parameter. - validateSearchParametersForPatient(map, theParams); + for (String patientSearchParam : patientSearchParams) { + List maps = myBulkExportHelperSvc.createSearchParameterMapsForResourceType(def, theParams); + for (SearchParameterMap map : maps) { + //Ensure users did not monkey with the patient compartment search parameter. + validateSearchParametersForPatient(map, theParams); - ISearchBuilder searchBuilder = getSearchBuilderForResourceType(theParams.getResourceType()); + ISearchBuilder searchBuilder = getSearchBuilderForResourceType(theParams.getResourceType()); - filterBySpecificPatient(theParams, resourceType, patientSearchParam, map); + filterBySpecificPatient(theParams, resourceType, patientSearchParam, map); - SearchRuntimeDetails searchRuntime = new SearchRuntimeDetails(null, jobId); - IResultIterator resultIterator = searchBuilder.createQuery(map, searchRuntime, null, RequestPartitionId.allPartitions()); - while (resultIterator.hasNext()) { - pids.add(resultIterator.next()); + SearchRuntimeDetails searchRuntime = new SearchRuntimeDetails(null, jobId); + IResultIterator resultIterator = searchBuilder.createQuery(map, searchRuntime, null, RequestPartitionId.allPartitions()); + while (resultIterator.hasNext()) { + pids.add(resultIterator.next()); + } } } return pids; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportTest.java index 597c4f87381..226c612e668 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportTest.java @@ -13,19 +13,25 @@ import com.google.common.collect.Sets; import org.apache.commons.io.LineIterator; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Basic; import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.MedicationAdministration; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ServiceRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -460,6 +466,113 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test { verifyBulkExportResults(options, List.of("Patient/P1", obsId, provId, devId, devId2), List.of("Patient/P2", provId2, devId3)); } + @Test + public void testPatientBulkExportWithReferenceToAuthor_ShouldShowUp() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); + // Create some resources + Patient patient = new Patient(); + patient.setId("P1"); + patient.setActive(true); + myClient.update().resource(patient).execute(); + + Basic basic = new Basic(); + basic.setAuthor(new Reference("Patient/P1")); + String basicId = myClient.create().resource(basic).execute().getId().toUnqualifiedVersionless().getValue(); + + DocumentReference documentReference = new DocumentReference(); + documentReference.setStatus(Enumerations.DocumentReferenceStatus.CURRENT); + documentReference.addAuthor(new Reference("Patient/P1")); + String docRefId = myClient.create().resource(documentReference).execute().getId().toUnqualifiedVersionless().getValue(); + + QuestionnaireResponse questionnaireResponseSub = new QuestionnaireResponse(); + questionnaireResponseSub.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED); + questionnaireResponseSub.setSubject(new Reference("Patient/P1")); + String questRespSubId = myClient.create().resource(questionnaireResponseSub).execute().getId().toUnqualifiedVersionless().getValue(); + + QuestionnaireResponse questionnaireResponseAuth = new QuestionnaireResponse(); + questionnaireResponseAuth.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED); + questionnaireResponseAuth.setAuthor(new Reference("Patient/P1")); + String questRespAuthId = myClient.create().resource(questionnaireResponseAuth).execute().getId().toUnqualifiedVersionless().getValue(); + + // set the export options + BulkDataExportOptions options = new BulkDataExportOptions(); + options.setResourceTypes(Sets.newHashSet("Patient", "Basic", "DocumentReference", "QuestionnaireResponse")); + options.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT); + options.setOutputFormat(Constants.CT_FHIR_NDJSON); + verifyBulkExportResults(options, List.of("Patient/P1", basicId, docRefId, questRespAuthId, questRespSubId), Collections.emptyList()); + } + + @Test + public void testPatientBulkExportWithReferenceToPerformer_ShouldShowUp() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); + // Create some resources + Patient patient = new Patient(); + patient.setId("P1"); + patient.setActive(true); + myClient.update().resource(patient).execute(); + + CarePlan carePlan = new CarePlan(); + carePlan.setStatus(CarePlan.CarePlanStatus.COMPLETED); + CarePlan.CarePlanActivityComponent carePlanActivityComponent = new CarePlan.CarePlanActivityComponent(); + CarePlan.CarePlanActivityDetailComponent carePlanActivityDetailComponent = new CarePlan.CarePlanActivityDetailComponent(); + carePlanActivityDetailComponent.addPerformer(new Reference("Patient/P1")); + carePlanActivityComponent.setDetail(carePlanActivityDetailComponent); + carePlan.addActivity(carePlanActivityComponent); + String carePlanId = myClient.create().resource(carePlan).execute().getId().toUnqualifiedVersionless().getValue(); + + MedicationAdministration medicationAdministration = new MedicationAdministration(); + medicationAdministration.setStatus(MedicationAdministration.MedicationAdministrationStatus.COMPLETED); + MedicationAdministration.MedicationAdministrationPerformerComponent medicationAdministrationPerformerComponent = new MedicationAdministration.MedicationAdministrationPerformerComponent(); + medicationAdministrationPerformerComponent.setActor(new Reference("Patient/P1")); + medicationAdministration.addPerformer(medicationAdministrationPerformerComponent); + String medAdminId = myClient.create().resource(medicationAdministration).execute().getId().toUnqualifiedVersionless().getValue(); + + ServiceRequest serviceRequest = new ServiceRequest(); + serviceRequest.setStatus(ServiceRequest.ServiceRequestStatus.COMPLETED); + serviceRequest.addPerformer(new Reference("Patient/P1")); + String sevReqId = myClient.create().resource(serviceRequest).execute().getId().toUnqualifiedVersionless().getValue(); + + Observation observationSub = new Observation(); + observationSub.setStatus(Observation.ObservationStatus.AMENDED); + observationSub.setSubject(new Reference("Patient/P1")); + String obsSubId = myClient.create().resource(observationSub).execute().getId().toUnqualifiedVersionless().getValue(); + + Observation observationPer = new Observation(); + observationPer.setStatus(Observation.ObservationStatus.AMENDED); + observationPer.addPerformer(new Reference("Patient/P1")); + String obsPerId = myClient.create().resource(observationPer).execute().getId().toUnqualifiedVersionless().getValue(); + + // set the export options + BulkDataExportOptions options = new BulkDataExportOptions(); + options.setResourceTypes(Sets.newHashSet("Patient", "Observation", "CarePlan", "MedicationAdministration", "ServiceRequest")); + options.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT); + options.setOutputFormat(Constants.CT_FHIR_NDJSON); + verifyBulkExportResults(options, List.of("Patient/P1", carePlanId, medAdminId, sevReqId, obsSubId, obsPerId), Collections.emptyList()); + } + + @Test + public void testPatientBulkExportWithResourceNotInCompartment_ShouldShowUp() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); + // Create some resources + Patient patient = new Patient(); + patient.setId("P1"); + patient.setActive(true); + myClient.update().resource(patient).execute(); + + Device device = new Device(); + device.setStatus(Device.FHIRDeviceStatus.ACTIVE); + device.setPatient(new Reference("Patient/P1")); + String deviceId = myClient.create().resource(device).execute().getId().toUnqualifiedVersionless().getValue(); + + + // set the export options + BulkDataExportOptions options = new BulkDataExportOptions(); + options.setResourceTypes(Sets.newHashSet("Patient", "Device")); + options.setExportStyle(BulkDataExportOptions.ExportStyle.PATIENT); + options.setOutputFormat(Constants.CT_FHIR_NDJSON); + verifyBulkExportResults(options, List.of("Patient/P1", deviceId), Collections.emptyList()); + } + private void verifyBulkExportResults(BulkDataExportOptions theOptions, List theContainedList, List theExcludedList) { Batch2JobStartResponse startResponse = myJobRunner.startNewJob(BulkExportUtils.createBulkExportJobParametersFromExportOptions(theOptions));