Bulk export: SearchParameters evaluation for patient compartment. If non-R4 evaluation would have failed, extract using R4 FHIR path (#5931)

* Proposed change to make DSTU3 find the same patient path as R4.

* Update logic based on conversation.

* Update logic based on conversation with R4 gating.

* Update logic again.

* Tweak logic again with an even narrower code path and add a new unit test.

* Add changelog.  Tweak javadoc.

* Code review feedback.
This commit is contained in:
Luke deGruchy 2024-05-14 16:52:30 -04:00 committed by GitHub
parent 078e87a4aa
commit 9b5673e240
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 130 additions and 20 deletions

View File

@ -21,16 +21,20 @@ package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -59,8 +63,12 @@ public class SearchParameterUtil {
* 1. Attempt to find one called 'patient' * 1. Attempt to find one called 'patient'
* 2. If that fails, find one called 'subject' * 2. If that fails, find one called 'subject'
* 3. If that fails, find one by Patient Compartment. * 3. If that fails, find one by Patient Compartment.
* 3.1 If that returns >1 result, throw an error * 3.1 If that returns exactly 1 result then return it
* 3.2 If that returns 1 result, return it * 3.2 If that doesn't return exactly 1 result and is R4, fall to 3.3, otherwise, 3.5
* 3.3 If that returns >1 result, throw an error
* 3.4 If that returns 1 result, return it
* 3.5 Find the search parameters by patient compartment using the R4 FHIR path, and return it if there is 1 result,
* otherwise, fall to 3.3
*/ */
public static Optional<RuntimeSearchParam> getOnlyPatientSearchParamForResourceType( public static Optional<RuntimeSearchParam> getOnlyPatientSearchParamForResourceType(
FhirContext theFhirContext, String theResourceType) { FhirContext theFhirContext, String theResourceType) {
@ -70,10 +78,40 @@ public class SearchParameterUtil {
if (myPatientSearchParam == null) { if (myPatientSearchParam == null) {
myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject"); myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject");
if (myPatientSearchParam == null) { if (myPatientSearchParam == null) {
myPatientSearchParam = getOnlyPatientCompartmentRuntimeSearchParam(runtimeResourceDefinition); final List<RuntimeSearchParam> searchParamsForCurrentVersion =
runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
final List<RuntimeSearchParam> searchParamsToUse;
// We want to handle a narrow code path in which attempting to process SearchParameters for a non-R4
// resource would have failed, and instead make another attempt to process them with the R4-equivalent
// FHIR path.
if (FhirVersionEnum.R4 == theFhirContext.getVersion().getVersion()
|| searchParamsForCurrentVersion.size() == 1) {
searchParamsToUse = searchParamsForCurrentVersion;
} else {
searchParamsToUse =
checkR4PatientCompartmentForMatchingSearchParam(runtimeResourceDefinition, theResourceType);
}
myPatientSearchParam =
validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, searchParamsToUse);
} }
} }
return Optional.ofNullable(myPatientSearchParam); return Optional.of(myPatientSearchParam);
}
@Nonnull
private static List<RuntimeSearchParam> checkR4PatientCompartmentForMatchingSearchParam(
RuntimeResourceDefinition theRuntimeResourceDefinition, String theResourceType) {
final RuntimeSearchParam patientSearchParamForR4 =
FhirContext.forR4Cached().getResourceDefinition(theResourceType).getSearchParam("patient");
return Optional.ofNullable(patientSearchParamForR4)
.map(patientSearchParamForR4NonNull ->
theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient").stream()
.filter(searchParam -> searchParam.getPath() != null)
.filter(searchParam ->
searchParam.getPath().equals(patientSearchParamForR4NonNull.getPath()))
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
} }
/** /**
@ -94,7 +132,7 @@ public class SearchParameterUtil {
if (mySubjectSearchParam != null) { if (mySubjectSearchParam != null) {
searchParams.add(mySubjectSearchParam); searchParams.add(mySubjectSearchParam);
} }
if (searchParams == null || searchParams.size() == 0) { if (CollectionUtils.isEmpty(searchParams)) {
String errorMessage = String.format( 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", "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()); runtimeResourceDefinition.getId());
@ -115,19 +153,34 @@ public class SearchParameterUtil {
public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam( public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
RuntimeResourceDefinition runtimeResourceDefinition) { RuntimeResourceDefinition runtimeResourceDefinition) {
RuntimeSearchParam patientSearchParam; return validateSearchParamsAndReturnOnlyOne(
List<RuntimeSearchParam> searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"); runtimeResourceDefinition, runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
if (searchParams == null || searchParams.size() == 0) { }
public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
RuntimeResourceDefinition runtimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
return validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, theSearchParams);
}
@Nonnull
private static RuntimeSearchParam validateSearchParamsAndReturnOnlyOne(
RuntimeResourceDefinition theRuntimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
final RuntimeSearchParam patientSearchParam;
if (CollectionUtils.isEmpty(theSearchParams)) {
String errorMessage = String.format( 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", "Resource type [%s] for ID [%s] and version: [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
runtimeResourceDefinition.getId()); theRuntimeResourceDefinition.getName(),
theRuntimeResourceDefinition.getId(),
theRuntimeResourceDefinition.getStructureVersion());
throw new IllegalArgumentException(Msg.code(1774) + errorMessage); throw new IllegalArgumentException(Msg.code(1774) + errorMessage);
} else if (searchParams.size() == 1) { } else if (theSearchParams.size() == 1) {
patientSearchParam = searchParams.get(0); patientSearchParam = theSearchParams.get(0);
} else { } else {
String errorMessage = String.format( String errorMessage = String.format(
"Resource type %s has more than one Search Param which references a patient compartment. We are unable to disambiguate which patient search parameter we should be searching by.", "Resource type [%s] for ID [%s] and version: [%s] has more than one Search Param which references a patient compartment. We are unable to disambiguate which patient search parameter we should be searching by.",
runtimeResourceDefinition.getId()); theRuntimeResourceDefinition.getName(),
theRuntimeResourceDefinition.getId(),
theRuntimeResourceDefinition.getStructureVersion());
throw new IllegalArgumentException(Msg.code(1775) + errorMessage); throw new IllegalArgumentException(Msg.code(1775) + errorMessage);
} }
return patientSearchParam; return patientSearchParam;
@ -148,9 +201,8 @@ public class SearchParameterUtil {
public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) { public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) {
return theFhirContext.getResourceTypes().stream() return theFhirContext.getResourceTypes().stream()
.filter(type -> getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type) .filter(type -> CollectionUtils.isNotEmpty(
.size() getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)))
> 0)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@ -165,9 +217,7 @@ public class SearchParameterUtil {
*/ */
public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) { public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) {
RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType); RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition) return CollectionUtils.isNotEmpty(getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition));
.size()
> 0;
} }
@Nullable @Nullable

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 5933
title: "Bulk export was failing with a HAPI-1775 error with the persistence module configured for DSTU3.
This has been fixed."

View File

@ -0,0 +1,55 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeSearchParam;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* We can't test this class from hapi-fhir-base due to the dependency restrictions (see the IMPORTANT NOT in the pom.xml
* That's why we're testing the class from this module.
*/
class SearchParameterUtilTest {
private static final String COVERAGE_BENEFICIARY = "Coverage.beneficiary";
private static final String PATIENT_LINK_OTHER = "Patient.link.other";
private static final String RESEARCH_SUBJECT_INDIVIDUAL = "ResearchSubject.individual";
private static final String SUPER_LONG_FHIR_PATH = "Account.subject.where(resolve() is Patient) | AdverseEvent.subject.where(resolve() is Patient) | AllergyIntolerance.patient | Appointment.participant.actor.where(resolve() is Patient) | Appointment.subject.where(resolve() is Patient) | AppointmentResponse.actor.where(resolve() is Patient) | AuditEvent.patient | Basic.subject.where(resolve() is Patient) | BodyStructure.patient | CarePlan.subject.where(resolve() is Patient) | CareTeam.subject.where(resolve() is Patient) | ChargeItem.subject.where(resolve() is Patient) | Claim.patient | ClaimResponse.patient | ClinicalImpression.subject.where(resolve() is Patient) | Communication.subject.where(resolve() is Patient) | CommunicationRequest.subject.where(resolve() is Patient) | Composition.subject.where(resolve() is Patient) | Condition.subject.where(resolve() is Patient) | Consent.subject.where(resolve() is Patient) | Contract.subject.where(resolve() is Patient) | Coverage.beneficiary | CoverageEligibilityRequest.patient | CoverageEligibilityResponse.patient | DetectedIssue.subject.where(resolve() is Patient) | DeviceRequest.subject.where(resolve() is Patient) | DeviceUsage.patient | DiagnosticReport.subject.where(resolve() is Patient) | DocumentReference.subject.where(resolve() is Patient) | Encounter.subject.where(resolve() is Patient) | EnrollmentRequest.candidate | EpisodeOfCare.patient | ExplanationOfBenefit.patient | FamilyMemberHistory.patient | Flag.subject.where(resolve() is Patient) | Goal.subject.where(resolve() is Patient) | GuidanceResponse.subject.where(resolve() is Patient) | ImagingSelection.subject.where(resolve() is Patient) | ImagingStudy.subject.where(resolve() is Patient) | Immunization.patient | ImmunizationEvaluation.patient | ImmunizationRecommendation.patient | Invoice.subject.where(resolve() is Patient) | List.subject.where(resolve() is Patient) | MeasureReport.subject.where(resolve() is Patient) | MedicationAdministration.subject.where(resolve() is Patient) | MedicationDispense.subject.where(resolve() is Patient) | MedicationRequest.subject.where(resolve() is Patient) | MedicationStatement.subject.where(resolve() is Patient) | MolecularSequence.subject.where(resolve() is Patient) | NutritionIntake.subject.where(resolve() is Patient) | NutritionOrder.subject.where(resolve() is Patient) | Observation.subject.where(resolve() is Patient) | Person.link.target.where(resolve() is Patient) | Procedure.subject.where(resolve() is Patient) | Provenance.patient | QuestionnaireResponse.subject.where(resolve() is Patient) | RelatedPerson.patient | RequestOrchestration.subject.where(resolve() is Patient) | ResearchSubject.subject.where(resolve() is Patient) | RiskAssessment.subject.where(resolve() is Patient) | ServiceRequest.subject.where(resolve() is Patient) | Specimen.subject.where(resolve() is Patient) | SupplyDelivery.patient | SupplyRequest.deliverFor | Task.for.where(resolve() is Patient) | VisionPrescription.patient";
public static Stream<Arguments> fhirVersionAndResourceType() {
return Stream.of(
Arguments.of(FhirVersionEnum.DSTU3, "Coverage", COVERAGE_BENEFICIARY),
Arguments.of(FhirVersionEnum.R4, "Coverage", COVERAGE_BENEFICIARY),
Arguments.of(FhirVersionEnum.R4B, "Coverage", COVERAGE_BENEFICIARY),
Arguments.of(FhirVersionEnum.R5, "Coverage", SUPER_LONG_FHIR_PATH),
Arguments.of(FhirVersionEnum.DSTU3, "Patient", PATIENT_LINK_OTHER),
Arguments.of(FhirVersionEnum.R4, "Patient", PATIENT_LINK_OTHER),
Arguments.of(FhirVersionEnum.R4B, "Patient", PATIENT_LINK_OTHER),
Arguments.of(FhirVersionEnum.R5, "Patient", PATIENT_LINK_OTHER),
Arguments.of(FhirVersionEnum.DSTU3, "ResearchSubject", RESEARCH_SUBJECT_INDIVIDUAL),
Arguments.of(FhirVersionEnum.R4, "ResearchSubject", RESEARCH_SUBJECT_INDIVIDUAL),
Arguments.of(FhirVersionEnum.R4B, "ResearchSubject", RESEARCH_SUBJECT_INDIVIDUAL),
Arguments.of(FhirVersionEnum.R5, "ResearchSubject", SUPER_LONG_FHIR_PATH)
);
}
@ParameterizedTest
@MethodSource("fhirVersionAndResourceType")
void getOnlyPatientSearchParamForResourceType(FhirVersionEnum theFhirVersion, String theResourceType, String theExpectedPath) {
final Optional<RuntimeSearchParam> optRuntimeSearchParam = SearchParameterUtil.getOnlyPatientSearchParamForResourceType(FhirContext.forCached(theFhirVersion), theResourceType);
assertTrue(optRuntimeSearchParam.isPresent());
final RuntimeSearchParam runtimeSearchParam = optRuntimeSearchParam.get();
assertEquals(theExpectedPath, runtimeSearchParam.getPath());
}
}