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:
parent
078e87a4aa
commit
9b5673e240
|
@ -21,16 +21,20 @@ package ca.uhn.fhir.util;
|
|||
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
@ -59,8 +63,12 @@ public class SearchParameterUtil {
|
|||
* 1. Attempt to find one called 'patient'
|
||||
* 2. If that fails, find one called 'subject'
|
||||
* 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
|
||||
* 3.1 If that returns exactly 1 result then 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(
|
||||
FhirContext theFhirContext, String theResourceType) {
|
||||
|
@ -70,10 +78,40 @@ public class SearchParameterUtil {
|
|||
if (myPatientSearchParam == null) {
|
||||
myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject");
|
||||
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) {
|
||||
searchParams.add(mySubjectSearchParam);
|
||||
}
|
||||
if (searchParams == null || searchParams.size() == 0) {
|
||||
if (CollectionUtils.isEmpty(searchParams)) {
|
||||
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());
|
||||
|
@ -115,19 +153,34 @@ public class SearchParameterUtil {
|
|||
|
||||
public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
|
||||
RuntimeResourceDefinition runtimeResourceDefinition) {
|
||||
RuntimeSearchParam patientSearchParam;
|
||||
List<RuntimeSearchParam> searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
|
||||
if (searchParams == null || searchParams.size() == 0) {
|
||||
return validateSearchParamsAndReturnOnlyOne(
|
||||
runtimeResourceDefinition, runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
|
||||
}
|
||||
|
||||
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(
|
||||
"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());
|
||||
"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",
|
||||
theRuntimeResourceDefinition.getName(),
|
||||
theRuntimeResourceDefinition.getId(),
|
||||
theRuntimeResourceDefinition.getStructureVersion());
|
||||
throw new IllegalArgumentException(Msg.code(1774) + errorMessage);
|
||||
} else if (searchParams.size() == 1) {
|
||||
patientSearchParam = searchParams.get(0);
|
||||
} else if (theSearchParams.size() == 1) {
|
||||
patientSearchParam = theSearchParams.get(0);
|
||||
} else {
|
||||
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.",
|
||||
runtimeResourceDefinition.getId());
|
||||
"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.",
|
||||
theRuntimeResourceDefinition.getName(),
|
||||
theRuntimeResourceDefinition.getId(),
|
||||
theRuntimeResourceDefinition.getStructureVersion());
|
||||
throw new IllegalArgumentException(Msg.code(1775) + errorMessage);
|
||||
}
|
||||
return patientSearchParam;
|
||||
|
@ -148,9 +201,8 @@ public class SearchParameterUtil {
|
|||
|
||||
public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) {
|
||||
return theFhirContext.getResourceTypes().stream()
|
||||
.filter(type -> getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)
|
||||
.size()
|
||||
> 0)
|
||||
.filter(type -> CollectionUtils.isNotEmpty(
|
||||
getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
@ -165,9 +217,7 @@ public class SearchParameterUtil {
|
|||
*/
|
||||
public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) {
|
||||
RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
|
||||
return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition)
|
||||
.size()
|
||||
> 0;
|
||||
return CollectionUtils.isNotEmpty(getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
@ -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."
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue