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.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
|
||||||
|
|
|
@ -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