Refactor prefetch generation out of discovery service. Add support for CRMI extensions.

This commit is contained in:
Brenin Rhodes 2024-04-24 14:26:25 -06:00
parent 90201095c4
commit 09eb56e2db
15 changed files with 1823 additions and 1118 deletions

View File

@ -24,6 +24,11 @@ public class CdsCrConstants {
public static final String CDS_CR_MODULE_ID = "CR"; public static final String CDS_CR_MODULE_ID = "CR";
public static final String CRMI_EFFECTIVE_DATA_REQUIREMENTS =
"http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-effectiveDataRequirements";
public static final String CQF_FHIR_QUERY_PATTERN = "http://hl7.org/fhir/StructureDefinition/cqf-fhirQueryPattern";
// CDS Hook field names // CDS Hook field names
public static final String CDS_PARAMETER_USER_ID = "userId"; public static final String CDS_PARAMETER_USER_ID = "userId";
public static final String CDS_PARAMETER_PATIENT_ID = "patientId"; public static final String CDS_PARAMETER_PATIENT_ID = "patientId";

View File

@ -0,0 +1,35 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import org.opencds.cqf.fhir.api.Repository;
public abstract class BasePrefetchTemplateBuilder {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected final Repository myRepository;
public BasePrefetchTemplateBuilder(Repository theRepository) {
myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH;
}
}

View File

@ -23,32 +23,24 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson; import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.DataRequirement;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.dstu3.model.PlanDefinition; import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.TriggerDefinition;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.api.Repository; import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.dstu3.SearchHelper;
import java.util.ArrayList; import java.util.stream.Collectors;
import java.util.List;
public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService { public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}"; protected final Repository myRepository;
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected Repository myRepository;
protected final IIdType myPlanDefinitionId; protected final IIdType myPlanDefinitionId;
protected final PrefetchTemplateBuilderDstu3 myPrefetchTemplateBuilder;
public CrDiscoveryServiceDstu3(IIdType thePlanDefinitionId, Repository theRepository) { public CrDiscoveryServiceDstu3(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId; myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository; myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH; myPrefetchTemplateBuilder = new PrefetchTemplateBuilderDstu3(myRepository);
} }
public CdsServiceJson resolveService() { public CdsServiceJson resolveService() {
@ -59,12 +51,36 @@ public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService {
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition; PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementDstu3(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); String triggerEvent = getTriggerEvent(planDef);
if (triggerEvent != null) {
PrefetchUrlList prefetchUrlList =
isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList();
return new CrDiscoveryElementDstu3(planDef, prefetchUrlList).getCdsServiceJson();
}
} }
return null; return null;
} }
public boolean isEca(PlanDefinition thePlanDefinition) { protected String getTriggerEvent(PlanDefinition thePlanDefinition) {
if (thePlanDefinition == null
|| !thePlanDefinition.hasAction()
|| thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTriggerDefinition())) {
return null;
}
var triggerDefs = thePlanDefinition.getAction().stream()
.filter(a -> a.hasTriggerDefinition())
.flatMap(a -> a.getTriggerDefinition().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
return triggerDefs.get(0).getEventName();
}
protected boolean isEca(PlanDefinition thePlanDefinition) {
if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) {
for (Coding coding : thePlanDefinition.getType().getCoding()) { for (Coding coding : thePlanDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) { if (coding.getCode().equals("eca-rule")) {
@ -74,372 +90,4 @@ public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService {
} }
return false; return false;
} }
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// Assuming 1 library
// TODO: enhance to handle multiple libraries - need a way to identify primary
// library
Library library = null;
if (thePlanDefinition.hasLibrary()
&& thePlanDefinition.getLibraryFirstRep().hasReference()) {
library = myRepository.read(
Library.class, thePlanDefinition.getLibraryFirstRep().getReferenceElement());
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(StringType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
StringType codeFilterComponentString = null;
if (codeFilterComponent.hasValueSetStringType()) {
codeFilterComponentString = codeFilterComponent.getValueSetStringType();
} else if (codeFilterComponent.hasValueSetReference()) {
codeFilterComponentString = new StringType(
codeFilterComponent.getValueSetReference().getReference());
} else if (codeFilterComponent.hasValueCoding()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getValueCoding();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
if (codeFilterComponentString != null) {
for (String codes : resolveValueSetCodes(codeFilterComponentString)) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (!library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
case "ProcedureRequest":
if (thePath.equals("bodySite")) return "body-site";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodySite":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "EligibilityRequest":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingManifest":
case "ImagingStudy":
case "Immunization":
case "ImmunizationRecommendation":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "ProcedureRequest":
case "Provenance":
case "QuestionnaireResponse":
case "ReferralRequest":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodySite":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "patient";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "EligibilityRequest":
return "patient";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingManifest":
return "patient";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "ProcedureRequest":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "ReferralRequest":
return "patient";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
} }

View File

@ -24,31 +24,23 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DataRequirement;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.PlanDefinition; import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.TriggerDefinition;
import org.opencds.cqf.fhir.api.Repository; import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.r4.SearchHelper;
import java.util.ArrayList; import java.util.stream.Collectors;
import java.util.List;
public class CrDiscoveryServiceR4 implements ICrDiscoveryService { public class CrDiscoveryServiceR4 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected final Repository myRepository; protected final Repository myRepository;
protected final IIdType myPlanDefinitionId; protected final IIdType myPlanDefinitionId;
protected final PrefetchTemplateBuilderR4 myPrefetchTemplateBuilder;
public CrDiscoveryServiceR4(IIdType thePlanDefinitionId, Repository theRepository) { public CrDiscoveryServiceR4(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId; myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository; myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH; myPrefetchTemplateBuilder = new PrefetchTemplateBuilderR4(myRepository);
} }
public CdsServiceJson resolveService() { public CdsServiceJson resolveService() {
@ -59,12 +51,36 @@ public class CrDiscoveryServiceR4 implements ICrDiscoveryService {
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition; PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementR4(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); String triggerEvent = getTriggerEvent(planDef);
if (triggerEvent != null) {
PrefetchUrlList prefetchUrlList =
isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList();
return new CrDiscoveryElementR4(planDef, prefetchUrlList).getCdsServiceJson();
}
} }
return null; return null;
} }
public boolean isEca(PlanDefinition planDefinition) { protected String getTriggerEvent(PlanDefinition thePlanDefinition) {
if (thePlanDefinition == null
|| !thePlanDefinition.hasAction()
|| thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) {
return null;
}
var triggerDefs = thePlanDefinition.getAction().stream()
.filter(a -> a.hasTrigger())
.flatMap(a -> a.getTrigger().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
return triggerDefs.get(0).getName();
}
protected boolean isEca(PlanDefinition planDefinition) {
if (planDefinition.hasType() && planDefinition.getType().hasCoding()) { if (planDefinition.hasType() && planDefinition.getType().hasCoding()) {
for (Coding coding : planDefinition.getType().getCoding()) { for (Coding coding : planDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) { if (coding.getCode().equals("eca-rule")) {
@ -74,356 +90,4 @@ public class CrDiscoveryServiceR4 implements ICrDiscoveryService {
} }
return false; return false;
} }
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
Library library = null;
if (thePlanDefinition.hasLibrary() && !thePlanDefinition.getLibrary().isEmpty()) {
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> valueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : valueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(CanonicalType valueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(List<String> ret, StringBuilder codes, String system, String code) {
String codeToken = system + "|" + code;
int postAppendLength = codes.length() + codeToken.length();
if (codes.length() > 0 && postAppendLength < myMaxUriLength) {
codes.append(",");
} else if (postAppendLength > myMaxUriLength) {
ret.add(codes.toString());
codes = new StringBuilder();
}
codes.append(codeToken);
return codes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "policy-holder";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
} }

View File

@ -24,31 +24,23 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DataRequirement;
import org.hl7.fhir.r5.model.Library;
import org.hl7.fhir.r5.model.PlanDefinition; import org.hl7.fhir.r5.model.PlanDefinition;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.TriggerDefinition;
import org.opencds.cqf.fhir.api.Repository; import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.r5.SearchHelper;
import java.util.ArrayList; import java.util.stream.Collectors;
import java.util.List;
public class CrDiscoveryServiceR5 implements ICrDiscoveryService { public class CrDiscoveryServiceR5 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected final Repository myRepository; protected final Repository myRepository;
protected final IIdType myPlanDefinitionId; protected final IIdType myPlanDefinitionId;
protected final PrefetchTemplateBuilderR5 myPrefetchTemplateBuilder;
public CrDiscoveryServiceR5(IIdType thePlanDefinitionId, Repository theRepository) { public CrDiscoveryServiceR5(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId; myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository; myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH; myPrefetchTemplateBuilder = new PrefetchTemplateBuilderR5(myRepository);
} }
public CdsServiceJson resolveService() { public CdsServiceJson resolveService() {
@ -59,12 +51,36 @@ public class CrDiscoveryServiceR5 implements ICrDiscoveryService {
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition; PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementR5(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); String triggerEvent = getTriggerEvent(planDef);
if (triggerEvent != null) {
PrefetchUrlList prefetchUrlList =
isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList();
return new CrDiscoveryElementR5(planDef, prefetchUrlList).getCdsServiceJson();
}
} }
return null; return null;
} }
public boolean isEca(PlanDefinition thePlanDefinition) { protected String getTriggerEvent(PlanDefinition thePlanDefinition) {
if (thePlanDefinition == null
|| !thePlanDefinition.hasAction()
|| thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) {
return null;
}
var triggerDefs = thePlanDefinition.getAction().stream()
.filter(a -> a.hasTrigger())
.flatMap(a -> a.getTrigger().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
return triggerDefs.get(0).getName();
}
protected boolean isEca(PlanDefinition thePlanDefinition) {
if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) {
for (Coding coding : thePlanDefinition.getType().getCoding()) { for (Coding coding : thePlanDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) { if (coding.getCode().equals("eca-rule")) {
@ -74,358 +90,4 @@ public class CrDiscoveryServiceR5 implements ICrDiscoveryService {
} }
return false; return false;
} }
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
Library library = null;
if (thePlanDefinition.hasLibrary() && !thePlanDefinition.getLibrary().isEmpty()) {
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(CanonicalType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType().toCode())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType().toCode())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path =
mapCodePathToSearchParam(theDataRequirement.getType().toCode(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "policy-holder";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
} }

View File

@ -0,0 +1,434 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.DataRequirement;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.dstu3.SearchHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CQF_FHIR_QUERY_PATTERN;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CRMI_EFFECTIVE_DATA_REQUIREMENTS;
public class PrefetchTemplateBuilderDstu3 extends BasePrefetchTemplateBuilder {
public PrefetchTemplateBuilderDstu3(Repository theRepository) {
super(theRepository);
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (!library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
Library library = null;
Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS);
// Use a Module Definition Library with Effective Data Requirements for the Plan Definition if it exists
if (dataReqExt != null && dataReqExt.hasValue()) {
StringType moduleDefCanonical = (StringType) dataReqExt.getValue();
library = (Library) SearchHelper.searchRepositoryByCanonical(myRepository, moduleDefCanonical);
}
// Otherwise use the primary Library
if (library == null && thePlanDefinition.hasLibrary()) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
StringType canonical =
new StringType(thePlanDefinition.getLibrary().get(0).getReference());
library = (Library) SearchHelper.searchRepositoryByCanonical(myRepository, canonical);
}
return library;
}
protected List<String> createRequestUrl(DataRequirement theDataRequirement) {
List<String> urlList = new ArrayList<>();
// if we have a fhirQueryPattern extensions, use them
List<Extension> fhirQueryExtList = theDataRequirement.getExtension().stream()
.filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue())
.collect(Collectors.toList());
if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) {
for (Extension fhirQueryExt : fhirQueryExtList) {
urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString());
}
return urlList;
}
// else build the query
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String baseQuery = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
// TODO: Add valueFilter extension resolution
if (theDataRequirement.hasCodeFilter()) {
resolveCodeFilter(theDataRequirement, urlList, baseQuery);
} else {
urlList.add(baseQuery);
}
return urlList;
}
protected void resolveCodeFilter(DataRequirement theDataRequirement, List<String> theUrlList, String theBaseQuery) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
StringType codeFilterComponentString = null;
if (codeFilterComponent.hasValueSetStringType()) {
codeFilterComponentString = codeFilterComponent.getValueSetStringType();
} else if (codeFilterComponent.hasValueSetReference()) {
codeFilterComponentString = new StringType(
codeFilterComponent.getValueSetReference().getReference());
} else if (codeFilterComponent.hasValueCoding()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getValueCoding();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
theUrlList.add(theBaseQuery + "&" + path + "=" + code);
} else {
theUrlList.add("," + code);
}
isFirstCodingInFilter = false;
}
}
if (codeFilterComponentString != null) {
for (String codes : resolveValueSetCodes(codeFilterComponentString)) {
theUrlList.add(theBaseQuery + "&" + path + "=" + codes);
}
}
}
}
protected List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
protected List<String> resolveValueSetCodes(StringType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
case "ProcedureRequest":
if (thePath.equals("bodySite")) return "body-site";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
protected static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodySite":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "EligibilityRequest":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingManifest":
case "ImagingStudy":
case "Immunization":
case "ImmunizationRecommendation":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "ProcedureRequest":
case "Provenance":
case "QuestionnaireResponse":
case "ReferralRequest":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
protected String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodySite":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "beneficiary";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "EligibilityRequest":
return "patient";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingManifest":
return "patient";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "ProcedureRequest":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "ReferralRequest":
return "patient";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -0,0 +1,413 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DataRequirement;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.SearchHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CQF_FHIR_QUERY_PATTERN;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CRMI_EFFECTIVE_DATA_REQUIREMENTS;
public class PrefetchTemplateBuilderR4 extends BasePrefetchTemplateBuilder {
public PrefetchTemplateBuilderR4(Repository theRepository) {
super(theRepository);
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
if (thePlanDefinition == null) return null;
PrefetchUrlList prefetchList = new PrefetchUrlList();
Library library = resolvePrimaryLibrary(thePlanDefinition);
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
Library library = null;
Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS);
// Use a Module Definition Library with Effective Data Requirements for the Plan Definition if it exists
if (dataReqExt != null && dataReqExt.hasValue()) {
CanonicalType moduleDefCanonical = (CanonicalType) dataReqExt.getValue();
library = (Library) SearchHelper.searchRepositoryByCanonical(myRepository, moduleDefCanonical);
}
// Otherwise use the primary Library
if (library == null && thePlanDefinition.hasLibrary()) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
protected List<String> createRequestUrl(DataRequirement theDataRequirement) {
List<String> urlList = new ArrayList<>();
// if we have a fhirQueryPattern extensions, use them
List<Extension> fhirQueryExtList = theDataRequirement.getExtension().stream()
.filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue())
.collect(Collectors.toList());
if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) {
for (Extension fhirQueryExt : fhirQueryExtList) {
urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString());
}
return urlList;
}
// else build the query
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String baseQuery = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
// TODO: Add valueFilter extension resolution
if (theDataRequirement.hasCodeFilter()) {
resolveCodeFilter(theDataRequirement, urlList, baseQuery);
} else {
urlList.add(baseQuery);
}
return urlList;
}
protected void resolveCodeFilter(DataRequirement theDataRequirement, List<String> theUrlList, String theBaseQuery) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
theUrlList.add(theBaseQuery + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
theUrlList.add(theBaseQuery + "&" + path + "=" + code);
} else {
theUrlList.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
}
protected List<String> resolveValueCodingCodes(List<Coding> valueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : valueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
protected List<String> resolveValueSetCodes(CanonicalType valueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(List<String> ret, StringBuilder codes, String system, String code) {
String codeToken = system + "|" + code;
int postAppendLength = codes.length() + codeToken.length();
if (codes.length() > 0 && postAppendLength < myMaxUriLength) {
codes.append(",");
} else if (postAppendLength > myMaxUriLength) {
ret.add(codes.toString());
codes = new StringBuilder();
}
codes.append(codeToken);
return codes;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
protected static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
protected String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "beneficiary";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -0,0 +1,421 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DataRequirement;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.Library;
import org.hl7.fhir.r5.model.PlanDefinition;
import org.hl7.fhir.r5.model.ValueSet;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.SearchHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CQF_FHIR_QUERY_PATTERN;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CRMI_EFFECTIVE_DATA_REQUIREMENTS;
public class PrefetchTemplateBuilderR5 extends BasePrefetchTemplateBuilder {
public PrefetchTemplateBuilderR5(Repository theRepository) {
super(theRepository);
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
if (thePlanDefinition == null) return null;
PrefetchUrlList prefetchList = new PrefetchUrlList();
Library library = resolvePrimaryLibrary(thePlanDefinition);
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
Library library = null;
Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS);
// Use a Module Definition Library with Effective Data Requirements for the Plan Definition if it exists
if (dataReqExt != null && dataReqExt.hasValue()) {
CanonicalType moduleDefCanonical = (CanonicalType) dataReqExt.getValue();
library = (Library) SearchHelper.searchRepositoryByCanonical(myRepository, moduleDefCanonical);
}
// Otherwise use the primary Library
if (library == null && thePlanDefinition.hasLibrary()) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
protected List<String> createRequestUrl(DataRequirement theDataRequirement) {
List<String> urlList = new ArrayList<>();
// if we have a fhirQueryPattern extensions, use them
List<Extension> fhirQueryExtList = theDataRequirement.getExtension().stream()
.filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue())
.collect(Collectors.toList());
if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) {
for (Extension fhirQueryExt : fhirQueryExtList) {
urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString());
}
return urlList;
}
// else build the query
if (!isPatientCompartment(theDataRequirement.getType().toCode())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType().toCode())
+ "=Patient/" + PATIENT_ID_CONTEXT;
// TODO: Add valueFilter extension resolution
if (theDataRequirement.hasCodeFilter()) {
resolveCodeFilter(theDataRequirement, urlList, patientRelatedResource);
} else {
urlList.add(patientRelatedResource);
}
return urlList;
}
protected void resolveCodeFilter(DataRequirement theDataRequirement, List<String> theUrlList, String theBaseQuery) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path =
mapCodePathToSearchParam(theDataRequirement.getType().toCode(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
theUrlList.add(theBaseQuery + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
theUrlList.add(theBaseQuery + "&" + path + "=" + code);
} else {
theUrlList.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
}
protected List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
protected List<String> resolveValueSetCodes(CanonicalType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "beneficiary";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -40,5 +40,4 @@ public class CrDiscoveryServiceR4Test extends BaseCrTest {
"}"; "}";
assertEquals(expected, actual); assertEquals(expected, actual);
} }
} }

View File

@ -0,0 +1,50 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.BaseCrTest;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opencds.cqf.fhir.api.Repository;
import java.util.Arrays;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CRMI_EFFECTIVE_DATA_REQUIREMENTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
class PrefetchTemplateBuilderR4Test extends BaseCrTest {
@Mock
Repository myRepository;
@InjectMocks
@Spy
PrefetchTemplateBuilderR4 myFixture;
@Test
public void testR4DiscoveryServiceWithEffectiveDataRequirements() {
PlanDefinition planDefinition = new PlanDefinition();
planDefinition.addExtension(CRMI_EFFECTIVE_DATA_REQUIREMENTS,
new CanonicalType("http://hl7.org/fhir/uv/crmi/Library/moduledefinition-example"));
planDefinition.setId("ModuleDefinitionTest");
Library library = ClasspathUtil.loadResource(myFhirContext, Library.class, "ModuleDefinitionExample.json");
doReturn(library).when(myFixture).resolvePrimaryLibrary(planDefinition);
PrefetchUrlList actual = myFixture.getPrefetchUrlList(planDefinition);
assertNotNull(actual);
PrefetchUrlList expected = new PrefetchUrlList();
expected.addAll(Arrays.asList("Patient?_id={{context.patientId}}",
"Encounter?status=finished&subject=Patient/{{context.patientId}}&type:in=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.292",
"Coverage?policy-holder=Patient/{{context.patientId}}&type:in=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591"));
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,298 @@
{
"resourceType": "Library",
"id": "moduledefinition-example",
"meta": {
"profile": [
"http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-moduledefinitionlibrary"
]
},
"url": "http://hl7.org/fhir/uv/crmi/Library/moduledefinition-example",
"identifier": [
{
"use": "official",
"system": "http://example.org/fhir/cqi/ecqm/Library/Identifier",
"value": "EXMLogic"
},
{
"system": "urn:ietf:rfc:3986",
"value": "urn:oid:2.16.840.1.113883.4.642.40.38.28.7"
}
],
"version": "1.0.0-snapshot",
"name": "EXMLogicModuleDefinition",
"title": "Example Logic Library - Module Definition",
"status": "active",
"experimental": true,
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/library-type",
"code": "module-definition"
}
]
},
"date": "2019-09-03",
"publisher": "HL7 International / Clinical Decision Support",
"contact": [
{
"telecom": [
{
"system": "url",
"value": "http://www.hl7.org/Special/committees/dss"
}
]
}
],
"description": "This library is used as an example module definition in the FHIR Quality Measure Implementation Guide",
"jurisdiction": [
{
"coding": [
{
"system": "http://unstats.un.org/unsd/methods/m49/m49.htm",
"code": "001",
"display": "World"
}
]
}
],
"relatedArtifact": [
{
"type": "depends-on",
"display": "FHIR model information",
"resource": "http://fhir.org/guides/cqf/common/Library/FHIR-ModelInfo|4.0.1"
},
{
"type": "depends-on",
"display": "Library FHIRHelpers",
"resource": "http://fhir.org/guides/cqf/common/Library/FHIRHelpers|4.0.1"
},
{
"type": "depends-on",
"display": "Code system Diagnosis Role",
"resource": "http://terminology.hl7.org/CodeSystem/diagnosis-role"
},
{
"type": "depends-on",
"display": "Value set Emergency Department Visit",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.292"
},
{
"type": "depends-on",
"display": "Value set Psychiatric/Mental Health Patient",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.299"
},
{
"type": "depends-on",
"display": "Value set Hospital Settings",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1111.126"
},
{
"type": "depends-on",
"display": "Value set ONC Administrative Sex",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1"
},
{
"type": "depends-on",
"display": "Value set Race",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.836"
},
{
"type": "depends-on",
"display": "Value set Ethnicity",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.837"
},
{
"type": "depends-on",
"display": "Value set Payer",
"resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591"
}
],
"parameter": [
{
"name": "Measurement Period",
"use": "in",
"min": 0,
"max": "1",
"type": "Period"
},
{
"name": "Patient",
"use": "out",
"min": 0,
"max": "1",
"type": "Patient"
},
{
"name": "Inpatient Encounter",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Initial Population",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Measure Population",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Stratifier 1",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Stratifier 2",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Stratifier 3",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "Stratifier 4",
"use": "out",
"min": 0,
"max": "*",
"type": "Encounter"
},
{
"name": "SDE Ethnicity",
"use": "out",
"min": 0,
"max": "*",
"type": "Coding"
},
{
"name": "SDE Payer",
"use": "out",
"min": 0,
"max": "*",
"type": "Resource"
},
{
"name": "SDE Race",
"use": "out",
"min": 0,
"max": "*",
"type": "Coding"
},
{
"name": "SDE Sex",
"use": "out",
"min": 0,
"max": "1",
"type": "Coding"
}
],
"dataRequirement": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-fhirQueryPattern",
"valueString": "Patient?_id={{context.patientId}}"
}
],
"type": "Patient",
"profile": [
"http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient"
],
"mustSupport": [
"extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity')"
],
"_mustSupport": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/rendered-value",
"valueString": "ethnicity"
}
]
}
]
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-fhirQueryPattern",
"valueString": "Encounter?status=finished&subject=Patient/{{context.patientId}}&type:in=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.292"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-isSelective",
"valueBoolean": true
},
{
"extension": [
{
"url": "path",
"valueString": "status"
},
{
"url": "comparator",
"valueCode": "eq"
},
{
"url": "value",
"valueString": "finished"
}
],
"url": "http://hl7.org/fhir/StructureDefinition/cqf-valueFilter"
}
],
"type": "Encounter",
"profile": [
"http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter"
],
"codeFilter": [
{
"path": "type",
"valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.292"
}
]
},
{
"type": "Condition",
"profile": [
"http://hl7.org/fhir/StructureDefinition/Condition"
],
"codeFilter": [
{
"path": "id"
}
]
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-fhirQueryPattern",
"valueString": "Coverage?policy-holder=Patient/{{context.patientId}}&type:in=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591"
}
],
"type": "Coverage",
"profile": [
"http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage"
],
"codeFilter": [
{
"path": "type",
"valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591"
}
]
}
]
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4; package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4; package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure; package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.r4.ICollectDataServiceFactory; import ca.uhn.fhir.cr.r4.ICollectDataServiceFactory;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure; package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.r4.IDataRequirementsServiceFactory; import ca.uhn.fhir.cr.r4.IDataRequirementsServiceFactory;