From 09eb56e2dbfa355b59c8d70b09c485b573d7e1f9 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Wed, 24 Apr 2024 14:26:25 -0600 Subject: [PATCH 1/8] Refactor prefetch generation out of discovery service. Add support for CRMI extensions. --- .../fhir/cdshooks/svc/cr/CdsCrConstants.java | 5 + .../BasePrefetchTemplateBuilder.java | 35 ++ .../cr/discovery/CrDiscoveryServiceDstu3.java | 412 ++--------------- .../cr/discovery/CrDiscoveryServiceR4.java | 394 ++-------------- .../cr/discovery/CrDiscoveryServiceR5.java | 396 ++-------------- .../PrefetchTemplateBuilderDstu3.java | 434 ++++++++++++++++++ .../discovery/PrefetchTemplateBuilderR4.java | 413 +++++++++++++++++ .../discovery/PrefetchTemplateBuilderR5.java | 421 +++++++++++++++++ .../discovery/CrDiscoveryServiceR4Test.java | 1 - .../PrefetchTemplateBuilderR4Test.java | 50 ++ .../resources/ModuleDefinitionExample.json | 298 ++++++++++++ .../cr/r4/ICollectDataServiceFactory.java | 19 + .../r4/IDataRequirementsServiceFactory.java | 19 + .../measure/CollectDataOperationProvider.java | 19 + .../DataRequirementsOperationProvider.java | 19 + 15 files changed, 1820 insertions(+), 1115 deletions(-) create mode 100644 hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/BasePrefetchTemplateBuilder.java create mode 100644 hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java create mode 100644 hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java create mode 100644 hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java create mode 100644 hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java create mode 100644 hapi-fhir-server-cds-hooks/src/test/resources/ModuleDefinitionExample.json diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java index 49f21eba79b7..b7829490b61e 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java @@ -24,6 +24,11 @@ private CdsCrConstants() {} 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 public static final String CDS_PARAMETER_USER_ID = "userId"; public static final String CDS_PARAMETER_PATIENT_ID = "patientId"; diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/BasePrefetchTemplateBuilder.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/BasePrefetchTemplateBuilder.java new file mode 100644 index 000000000000..dc879029115d --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/BasePrefetchTemplateBuilder.java @@ -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; + } +} diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceDstu3.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceDstu3.java index 5eb44690494e..fef10d7da939 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceDstu3.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceDstu3.java @@ -23,32 +23,24 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson; import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; 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.StringType; -import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.dstu3.model.TriggerDefinition; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; 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; public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService { - protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}"; - protected final int DEFAULT_MAX_URI_LENGTH = 8000; - protected int myMaxUriLength; - - protected Repository myRepository; + protected final Repository myRepository; protected final IIdType myPlanDefinitionId; + protected final PrefetchTemplateBuilderDstu3 myPrefetchTemplateBuilder; public CrDiscoveryServiceDstu3(IIdType thePlanDefinitionId, Repository theRepository) { myPlanDefinitionId = thePlanDefinitionId; myRepository = theRepository; - myMaxUriLength = DEFAULT_MAX_URI_LENGTH; + myPrefetchTemplateBuilder = new PrefetchTemplateBuilderDstu3(myRepository); } public CdsServiceJson resolveService() { @@ -59,387 +51,43 @@ public CdsServiceJson resolveService() { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) { PlanDefinition planDef = (PlanDefinition) thePlanDefinition; - return new CrDiscoveryElementDstu3(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); - } - return null; - } - - public boolean isEca(PlanDefinition thePlanDefinition) { - if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { - for (Coding coding : thePlanDefinition.getType().getCoding()) { - if (coding.getCode().equals("eca-rule")) { - return true; - } + String triggerEvent = getTriggerEvent(planDef); + if (triggerEvent != null) { + PrefetchUrlList prefetchUrlList = + isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList(); + return new CrDiscoveryElementDstu3(planDef, prefetchUrlList).getCdsServiceJson(); } } - 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; + return null; } - public List resolveValueCodingCodes(List theValueCodings) { - List 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); - } + protected String getTriggerEvent(PlanDefinition thePlanDefinition) { + if (thePlanDefinition == null + || !thePlanDefinition.hasAction() + || thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTriggerDefinition())) { + return null; } - result.add(codes.toString()); - return result; - } - - public List resolveValueSetCodes(StringType theValueSetId) { - ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); - List 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); - } - } - } + 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; } - result.add(codes.toString()); - return result; - } - - protected StringBuilder getCodesStringBuilder( - List 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; + return triggerDefs.get(0).getEventName(); } - public List createRequestUrl(DataRequirement theDataRequirement) { - if (!isPatientCompartment(theDataRequirement.getType())) return null; - String patientRelatedResource = theDataRequirement.getType() + "?" - + getPatientSearchParam(theDataRequirement.getType()) - + "=Patient/" + PATIENT_ID_CONTEXT; - List 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 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); - } + protected boolean isEca(PlanDefinition thePlanDefinition) { + if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { + for (Coding coding : thePlanDefinition.getType().getCoding()) { + if (coding.getCode().equals("eca-rule")) { + return true; } } - 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 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; + return false; } } diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4.java index 54c7a3e88f69..437ce355b9ee 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4.java @@ -24,31 +24,23 @@ import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; import org.hl7.fhir.instance.model.api.IBaseResource; 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.DataRequirement; -import org.hl7.fhir.r4.model.Library; 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.utility.r4.SearchHelper; -import java.util.ArrayList; -import java.util.List; +import java.util.stream.Collectors; 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 IIdType myPlanDefinitionId; + protected final PrefetchTemplateBuilderR4 myPrefetchTemplateBuilder; public CrDiscoveryServiceR4(IIdType thePlanDefinitionId, Repository theRepository) { myPlanDefinitionId = thePlanDefinitionId; myRepository = theRepository; - myMaxUriLength = DEFAULT_MAX_URI_LENGTH; + myPrefetchTemplateBuilder = new PrefetchTemplateBuilderR4(myRepository); } public CdsServiceJson resolveService() { @@ -59,371 +51,43 @@ public CdsServiceJson resolveService() { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) { PlanDefinition planDef = (PlanDefinition) thePlanDefinition; - return new CrDiscoveryElementR4(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); - } - return null; - } - - public boolean isEca(PlanDefinition planDefinition) { - if (planDefinition.hasType() && planDefinition.getType().hasCoding()) { - for (Coding coding : planDefinition.getType().getCoding()) { - if (coding.getCode().equals("eca-rule")) { - return true; - } + String triggerEvent = getTriggerEvent(planDef); + if (triggerEvent != null) { + PrefetchUrlList prefetchUrlList = + isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList(); + return new CrDiscoveryElementR4(planDef, prefetchUrlList).getCdsServiceJson(); } } - 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; + return null; } - public List resolveValueCodingCodes(List valueCodings) { - List 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); - } + protected String getTriggerEvent(PlanDefinition thePlanDefinition) { + if (thePlanDefinition == null + || !thePlanDefinition.hasAction() + || thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) { + return null; } - result.add(codes.toString()); - return result; - } - - public List resolveValueSetCodes(CanonicalType valueSetId) { - ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId); - List 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); - } - } - } + 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; } - result.add(codes.toString()); - return result; - } - - protected StringBuilder getCodesStringBuilder(List 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; + return triggerDefs.get(0).getName(); } - public List createRequestUrl(DataRequirement theDataRequirement) { - if (!isPatientCompartment(theDataRequirement.getType())) return null; - String patientRelatedResource = theDataRequirement.getType() + "?" - + getPatientSearchParam(theDataRequirement.getType()) - + "=Patient/" + PATIENT_ID_CONTEXT; - List 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 codeFilterValueCodings = codeFilterComponent.getCode(); - boolean isFirstCodingInFilter = true; - for (String code : resolveValueCodingCodes(codeFilterValueCodings)) { - if (isFirstCodingInFilter) { - ret.add(patientRelatedResource + "&" + path + "=" + code); - } else { - ret.add("," + code); - } - - isFirstCodingInFilter = false; - } + protected boolean isEca(PlanDefinition planDefinition) { + if (planDefinition.hasType() && planDefinition.getType().hasCoding()) { + for (Coding coding : planDefinition.getType().getCoding()) { + if (coding.getCode().equals("eca-rule")) { + return true; } } - 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 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; + return false; } } diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR5.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR5.java index 7562531364d2..c38439994daf 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR5.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR5.java @@ -24,31 +24,23 @@ import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils; import org.hl7.fhir.instance.model.api.IBaseResource; 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.DataRequirement; -import org.hl7.fhir.r5.model.Library; 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.utility.r5.SearchHelper; -import java.util.ArrayList; -import java.util.List; +import java.util.stream.Collectors; 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 IIdType myPlanDefinitionId; + protected final PrefetchTemplateBuilderR5 myPrefetchTemplateBuilder; public CrDiscoveryServiceR5(IIdType thePlanDefinitionId, Repository theRepository) { myPlanDefinitionId = thePlanDefinitionId; myRepository = theRepository; - myMaxUriLength = DEFAULT_MAX_URI_LENGTH; + myPrefetchTemplateBuilder = new PrefetchTemplateBuilderR5(myRepository); } public CdsServiceJson resolveService() { @@ -59,373 +51,43 @@ public CdsServiceJson resolveService() { protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) { if (thePlanDefinition instanceof PlanDefinition) { PlanDefinition planDef = (PlanDefinition) thePlanDefinition; - return new CrDiscoveryElementR5(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson(); - } - return null; - } - - public boolean isEca(PlanDefinition thePlanDefinition) { - if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { - for (Coding coding : thePlanDefinition.getType().getCoding()) { - if (coding.getCode().equals("eca-rule")) { - return true; - } + String triggerEvent = getTriggerEvent(planDef); + if (triggerEvent != null) { + PrefetchUrlList prefetchUrlList = + isEca(planDef) ? myPrefetchTemplateBuilder.getPrefetchUrlList(planDef) : new PrefetchUrlList(); + return new CrDiscoveryElementR5(planDef, prefetchUrlList).getCdsServiceJson(); } } - 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; + return null; } - public List resolveValueCodingCodes(List theValueCodings) { - List 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); - } + protected String getTriggerEvent(PlanDefinition thePlanDefinition) { + if (thePlanDefinition == null + || !thePlanDefinition.hasAction() + || thePlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) { + return null; } - result.add(codes.toString()); - return result; - } - - public List resolveValueSetCodes(CanonicalType theValueSetId) { - ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); - List 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); - } - } - } + 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; } - result.add(codes.toString()); - return result; - } - - protected StringBuilder getCodesStringBuilder( - List 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; + return triggerDefs.get(0).getName(); } - public List createRequestUrl(DataRequirement theDataRequirement) { - if (!isPatientCompartment(theDataRequirement.getType().toCode())) return null; - String patientRelatedResource = theDataRequirement.getType() + "?" - + getPatientSearchParam(theDataRequirement.getType().toCode()) - + "=Patient/" + PATIENT_ID_CONTEXT; - List 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 codeFilterValueCodings = codeFilterComponent.getCode(); - boolean isFirstCodingInFilter = true; - for (String code : resolveValueCodingCodes(codeFilterValueCodings)) { - if (isFirstCodingInFilter) { - ret.add(patientRelatedResource + "&" + path + "=" + code); - } else { - ret.add("," + code); - } - - isFirstCodingInFilter = false; - } + protected boolean isEca(PlanDefinition thePlanDefinition) { + if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) { + for (Coding coding : thePlanDefinition.getType().getCoding()) { + if (coding.getCode().equals("eca-rule")) { + return true; } } - 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 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; + return false; } } diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java new file mode 100644 index 000000000000..3cd917b2e6c5 --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java @@ -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 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 createRequestUrl(DataRequirement theDataRequirement) { + List urlList = new ArrayList<>(); + // if we have a fhirQueryPattern extensions, use them + List 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 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 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 resolveValueCodingCodes(List theValueCodings) { + List 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 resolveValueSetCodes(StringType theValueSetId) { + ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); + List 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 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; + } +} diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java new file mode 100644 index 000000000000..13ea110453e5 --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java @@ -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 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 createRequestUrl(DataRequirement theDataRequirement) { + List urlList = new ArrayList<>(); + // if we have a fhirQueryPattern extensions, use them + List 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 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 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 resolveValueCodingCodes(List valueCodings) { + List 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 resolveValueSetCodes(CanonicalType valueSetId) { + ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId); + List 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 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; + } +} diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java new file mode 100644 index 000000000000..2fdec3514d28 --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java @@ -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 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 createRequestUrl(DataRequirement theDataRequirement) { + List urlList = new ArrayList<>(); + // if we have a fhirQueryPattern extensions, use them + List 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 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 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 resolveValueCodingCodes(List theValueCodings) { + List 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 resolveValueSetCodes(CanonicalType theValueSetId) { + ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); + List 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 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; + } +} diff --git a/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4Test.java b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4Test.java index e8b5eee22aa5..9115657f1aff 100644 --- a/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4Test.java +++ b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/CrDiscoveryServiceR4Test.java @@ -40,5 +40,4 @@ public void testR4DiscoveryService() throws JsonProcessingException { "}"; assertEquals(expected, actual); } - } diff --git a/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java new file mode 100644 index 000000000000..0fd6a13d9295 --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java @@ -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); + } +} diff --git a/hapi-fhir-server-cds-hooks/src/test/resources/ModuleDefinitionExample.json b/hapi-fhir-server-cds-hooks/src/test/resources/ModuleDefinitionExample.json new file mode 100644 index 000000000000..a70755783892 --- /dev/null +++ b/hapi-fhir-server-cds-hooks/src/test/resources/ModuleDefinitionExample.json @@ -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" + } + ] + } + ] +} diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/ICollectDataServiceFactory.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/ICollectDataServiceFactory.java index fcd883576bfb..e94facf199f9 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/ICollectDataServiceFactory.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/ICollectDataServiceFactory.java @@ -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; import ca.uhn.fhir.rest.api.server.RequestDetails; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/IDataRequirementsServiceFactory.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/IDataRequirementsServiceFactory.java index f5de302cd97e..483945ba1b50 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/IDataRequirementsServiceFactory.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/IDataRequirementsServiceFactory.java @@ -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; import ca.uhn.fhir.rest.api.server.RequestDetails; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java index bdaec6cc8ac7..09313ca21ff1 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CollectDataOperationProvider.java @@ -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; import ca.uhn.fhir.cr.r4.ICollectDataServiceFactory; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/DataRequirementsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/DataRequirementsOperationProvider.java index 9c801d91d39b..da7ff8506471 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/DataRequirementsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/DataRequirementsOperationProvider.java @@ -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; import ca.uhn.fhir.cr.r4.IDataRequirementsServiceFactory; From 3e762cced8fae3c485f0960cc6cce2cd6563f817 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Wed, 24 Apr 2024 14:32:05 -0600 Subject: [PATCH 2/8] Add changelog --- .../fhir/changelog/7_4_0/5873-support-crmi-extensions.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5873-support-crmi-extensions.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5873-support-crmi-extensions.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5873-support-crmi-extensions.yaml new file mode 100644 index 000000000000..1dc033206bc1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5873-support-crmi-extensions.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 5873 +title: "Added support for CDS on FHIR PlanDefinitions that use the CRMI Effective Data Requirements extension when + creating the JSON for the Service Discovery. Also added support for use of the FHIR Query Pattern extension when + generating Prefetch Templates for Service Discovery. If a DataRequirement has FHIR Query Pattern extensions they + will be used, otherwise Prefetch Templates will be generated." From a49b133a39e2c7dfb611c9fd5e1d89b465d3ba41 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Wed, 24 Apr 2024 14:39:02 -0600 Subject: [PATCH 3/8] Add to doc --- .../ca/uhn/hapi/fhir/docs/cds_hooks/cds_hooks_intro.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/cds_hooks/cds_hooks_intro.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/cds_hooks/cds_hooks_intro.md index fec9449175eb..2d2c8e50653c 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/cds_hooks/cds_hooks_intro.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/cds_hooks/cds_hooks_intro.md @@ -136,6 +136,8 @@ To create CDS Services from PlanDefinitions the dependencies for a FHIR Storage Any PlanDefinition resource with an action that has a trigger of type [named-event](http://hl7.org/fhir/R4/codesystem-trigger-type.html#trigger-type-named-event) will have a CDS Service created using the PlanDefinition.id as the service id and the name of the trigger as the hook that the service is created for per the [CDS on FHIR Specification](https://hl7.org/fhir/clinicalreasoning-cds-on-fhir.html#surfacing-clinical-decision-support). +The [CRMI Effective Data Requirements](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/StructureDefinition-crmi-effectiveDataRequirements.html) extension, if present, will be used to determine the library to use when creating the prefetch templates. Otherwise, the primary library of the PlanDefinition will be used. + CDS Services created this way will show up as registered services and can be called just as other services are called. The CDS Service request will be converted into parameters for the [$apply operation](/docs/clinical_reasoning/plan_definitions.html#apply), the results of which are then converted into a CDS Response per the [CDS on FHIR Specification](https://hl7.org/fhir/clinicalreasoning-cds-on-fhir.html#consuming-decision-support). These CDS Services will take advantage of the [Auto Prefetch](/docs/cds_hooks/#auto-prefetch) feature. Prefetch data is included as a Bundle in the `data` parameter of the $apply call. From a881ce620c564317b559f34825f46311555908b7 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Wed, 24 Apr 2024 14:44:41 -0600 Subject: [PATCH 4/8] cleanup unused imports --- .../svc/cr/discovery/PrefetchTemplateBuilderR4Test.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java index 0fd6a13d9295..07217291bee9 100644 --- a/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java +++ b/hapi-fhir-server-cds-hooks/src/test/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4Test.java @@ -5,8 +5,6 @@ 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; From 4f48bce9593eb2a45c853601a79aaf4646e9b66d Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Wed, 24 Apr 2024 15:02:49 -0600 Subject: [PATCH 5/8] cleanup --- .../fhir/cdshooks/svc/cr/CdsCrConstants.java | 2 -- .../PrefetchTemplateBuilderDstu3.java | 9 +++--- .../discovery/PrefetchTemplateBuilderR4.java | 32 ++++++++++--------- .../discovery/PrefetchTemplateBuilderR5.java | 9 +++--- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java index b7829490b61e..ccb6c2a3b3ca 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/CdsCrConstants.java @@ -20,8 +20,6 @@ package ca.uhn.hapi.fhir.cdshooks.svc.cr; public class CdsCrConstants { - private CdsCrConstants() {} - public static final String CDS_CR_MODULE_ID = "CR"; public static final String CRMI_EFFECTIVE_DATA_REQUIREMENTS = diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java index 3cd917b2e6c5..446a0bd2b983 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java @@ -185,14 +185,14 @@ protected List resolveValueSetCodes(StringType theValueSetId) { } protected StringBuilder getCodesStringBuilder( - List theList, StringBuilder theCodes, String theSystem, String theCode) { + List theStrings, 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()); + theStrings.add(theCodes.toString()); theCodes = new StringBuilder(); } theCodes.append(codeToken); @@ -427,8 +427,9 @@ protected String getPatientSearchParam(String theDataType) { return "subject"; case "VisionPrescription": return "patient"; - } - return null; + default: + return null; + } } } diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java index 13ea110453e5..301fc9631932 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java @@ -127,11 +127,11 @@ protected void resolveCodeFilter(DataRequirement theDataRequirement, List resolveValueCodingCodes(List valueCodings) { + protected List resolveValueCodingCodes(List theValueCodings) { List result = new ArrayList<>(); StringBuilder codes = new StringBuilder(); - for (Coding coding : valueCodings) { + for (Coding coding : theValueCodings) { if (coding.hasCode()) { String system = coding.getSystem(); String code = coding.getCode(); @@ -144,8 +144,8 @@ protected List resolveValueCodingCodes(List valueCodings) { return result; } - protected List resolveValueSetCodes(CanonicalType valueSetId) { - ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId); + protected List resolveValueSetCodes(CanonicalType theValueSetId) { + ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); List result = new ArrayList<>(); StringBuilder codes = new StringBuilder(); if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) { @@ -172,18 +172,19 @@ protected List resolveValueSetCodes(CanonicalType valueSetId) { return result; } - protected StringBuilder getCodesStringBuilder(List ret, StringBuilder codes, String system, String code) { - String codeToken = system + "|" + code; - int postAppendLength = codes.length() + codeToken.length(); + protected StringBuilder getCodesStringBuilder( + List theStrings, StringBuilder theCodes, String system, String theCode) { + String codeToken = system + "|" + theCode; + int postAppendLength = theCodes.length() + codeToken.length(); - if (codes.length() > 0 && postAppendLength < myMaxUriLength) { - codes.append(","); + if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) { + theCodes.append(","); } else if (postAppendLength > myMaxUriLength) { - ret.add(codes.toString()); - codes = new StringBuilder(); + theStrings.add(theCodes.toString()); + theCodes = new StringBuilder(); } - codes.append(codeToken); - return codes; + theCodes.append(codeToken); + return theCodes; } protected String mapCodePathToSearchParam(String theDataType, String thePath) { @@ -406,8 +407,9 @@ protected String getPatientSearchParam(String theDataType) { return "subject"; case "VisionPrescription": return "patient"; - } - return null; + default: + return null; + } } } diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java index 2fdec3514d28..accb87a5b549 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java @@ -174,14 +174,14 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { } protected StringBuilder getCodesStringBuilder( - List theList, StringBuilder theCodes, String theSystem, String theCode) { + List theStrings, 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()); + theStrings.add(theCodes.toString()); theCodes = new StringBuilder(); } theCodes.append(codeToken); @@ -414,8 +414,9 @@ public String getPatientSearchParam(String theDataType) { return "subject"; case "VisionPrescription": return "patient"; - } - return null; + default: + return null; + } } } From e8d49fedaf1fe4b3fbd463c712bb2386a80f0dc9 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Thu, 25 Apr 2024 08:10:46 -0600 Subject: [PATCH 6/8] cleanup --- .../cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java | 1 + .../cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java | 1 + .../cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java | 1 + 3 files changed, 3 insertions(+) diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java index 446a0bd2b983..0d754b3df99e 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java @@ -184,6 +184,7 @@ protected List resolveValueSetCodes(StringType theValueSetId) { return result; } + @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( List theStrings, StringBuilder theCodes, String theSystem, String theCode) { String codeToken = theSystem + "|" + theCode; diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java index 301fc9631932..3d2068215945 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java @@ -172,6 +172,7 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { return result; } + @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( List theStrings, StringBuilder theCodes, String system, String theCode) { String codeToken = system + "|" + theCode; diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java index accb87a5b549..8d5c20575c24 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java @@ -173,6 +173,7 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { return result; } + @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( List theStrings, StringBuilder theCodes, String theSystem, String theCode) { String codeToken = theSystem + "|" + theCode; From 3cb57324be86dd96becbf4eb9a39e589852941c8 Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Thu, 25 Apr 2024 08:38:49 -0600 Subject: [PATCH 7/8] cleanup --- .../discovery/PrefetchTemplateBuilderR4.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java index 3d2068215945..78108dafe86a 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java @@ -79,7 +79,7 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { List fhirQueryExtList = theDataRequirement.getExtension().stream() .filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue()) .collect(Collectors.toList()); - if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) { + if (!fhirQueryExtList.isEmpty()) { for (Extension fhirQueryExt : fhirQueryExtList) { urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString()); } @@ -102,6 +102,7 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { return urlList; } + @SuppressWarnings("ReassignedVariable") protected void resolveCodeFilter(DataRequirement theDataRequirement, List theUrlList, String theBaseQuery) { for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent : theDataRequirement.getCodeFilter()) { @@ -127,6 +128,7 @@ protected void resolveCodeFilter(DataRequirement theDataRequirement, List resolveValueCodingCodes(List theValueCodings) { List result = new ArrayList<>(); @@ -144,6 +146,7 @@ protected List resolveValueCodingCodes(List theValueCodings) { return result; } + @SuppressWarnings("ReassignedVariable") protected List resolveValueSetCodes(CanonicalType theValueSetId) { ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); List result = new ArrayList<>(); @@ -174,18 +177,19 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( - List theStrings, StringBuilder theCodes, String system, String theCode) { - String codeToken = system + "|" + theCode; - int postAppendLength = theCodes.length() + codeToken.length(); + List theStrings, StringBuilder theCodes, String theSystem, String theCode) { + StringBuilder codes = theCodes; + String codeToken = theSystem + "|" + theCode; + int postAppendLength = codes.length() + codeToken.length(); - if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) { - theCodes.append(","); + if (codes.length() > 0 && postAppendLength < myMaxUriLength) { + codes.append(","); } else if (postAppendLength > myMaxUriLength) { - theStrings.add(theCodes.toString()); - theCodes = new StringBuilder(); + theStrings.add(codes.toString()); + codes = new StringBuilder(); } - theCodes.append(codeToken); - return theCodes; + codes.append(codeToken); + return codes; } protected String mapCodePathToSearchParam(String theDataType, String thePath) { From e81827fe9db27ad0fa903ce5f5201885efd04dcb Mon Sep 17 00:00:00 2001 From: Brenin Rhodes Date: Thu, 25 Apr 2024 09:04:50 -0600 Subject: [PATCH 8/8] cleanup --- .../PrefetchTemplateBuilderDstu3.java | 28 +++++++++---------- .../discovery/PrefetchTemplateBuilderR4.java | 8 +----- .../discovery/PrefetchTemplateBuilderR5.java | 25 ++++++++--------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java index 0d754b3df99e..9e994b509b0a 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderDstu3.java @@ -46,7 +46,6 @@ 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 requestUrls = createRequestUrl(dataRequirement); @@ -58,6 +57,7 @@ public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) { return prefetchList; } + @SuppressWarnings("ReassignedVariable") protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) { Library library = null; Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS); @@ -82,7 +82,7 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { List fhirQueryExtList = theDataRequirement.getExtension().stream() .filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue()) .collect(Collectors.toList()); - if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) { + if (!fhirQueryExtList.isEmpty()) { for (Extension fhirQueryExt : fhirQueryExtList) { urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString()); } @@ -105,12 +105,12 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { return urlList; } + @SuppressWarnings("ReassignedVariable") protected void resolveCodeFilter(DataRequirement theDataRequirement, List 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(); @@ -126,7 +126,6 @@ protected void resolveCodeFilter(DataRequirement theDataRequirement, List resolveValueCodingCodes(List theValueCodings) { List 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; } + @SuppressWarnings("ReassignedVariable") protected List resolveValueSetCodes(StringType theValueSetId) { ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId); List result = new ArrayList<>(); @@ -187,17 +185,17 @@ protected List resolveValueSetCodes(StringType theValueSetId) { @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( List theStrings, StringBuilder theCodes, String theSystem, String theCode) { + StringBuilder codes = theCodes; String codeToken = theSystem + "|" + theCode; - int postAppendLength = theCodes.length() + codeToken.length(); - - if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) { - theCodes.append(","); + int postAppendLength = codes.length() + codeToken.length(); + if (codes.length() > 0 && postAppendLength < myMaxUriLength) { + codes.append(","); } else if (postAppendLength > myMaxUriLength) { - theStrings.add(theCodes.toString()); - theCodes = new StringBuilder(); + theStrings.add(codes.toString()); + codes = new StringBuilder(); } - theCodes.append(codeToken); - return theCodes; + codes.append(codeToken); + return codes; } protected String mapCodePathToSearchParam(String theDataType, String thePath) { diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java index 78108dafe86a..bc4047bc7c22 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR4.java @@ -56,6 +56,7 @@ public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) { return prefetchList; } + @SuppressWarnings("ReassignedVariable") protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) { Library library = null; Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS); @@ -121,7 +122,6 @@ protected void resolveCodeFilter(DataRequirement theDataRequirement, List resolveValueCodingCodes(List theValueCodings) { List 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; } @@ -156,7 +153,6 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { valueSet.getExpansion().getContains()) { String system = contains.getSystem(); String code = contains.getCode(); - codes = getCodesStringBuilder(result, codes, system, code); } } else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) { @@ -165,7 +161,6 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { if (concepts.hasConcept()) { for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) { String code = concept.getCode(); - codes = getCodesStringBuilder(result, codes, system, code); } } @@ -181,7 +176,6 @@ protected StringBuilder getCodesStringBuilder( StringBuilder codes = theCodes; String codeToken = theSystem + "|" + theCode; int postAppendLength = codes.length() + codeToken.length(); - if (codes.length() > 0 && postAppendLength < myMaxUriLength) { codes.append(","); } else if (postAppendLength > myMaxUriLength) { diff --git a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java index 8d5c20575c24..5e81cca8d260 100644 --- a/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java +++ b/hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/svc/cr/discovery/PrefetchTemplateBuilderR5.java @@ -56,6 +56,7 @@ public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) { return prefetchList; } + @SuppressWarnings("ReassignedVariable") protected Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) { Library library = null; Extension dataReqExt = thePlanDefinition.getExtensionByUrl(CRMI_EFFECTIVE_DATA_REQUIREMENTS); @@ -79,7 +80,7 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { List fhirQueryExtList = theDataRequirement.getExtension().stream() .filter(e -> e.getUrl().equals(CQF_FHIR_QUERY_PATTERN) && e.hasValue()) .collect(Collectors.toList()); - if (fhirQueryExtList != null && !fhirQueryExtList.isEmpty()) { + if (!fhirQueryExtList.isEmpty()) { for (Extension fhirQueryExt : fhirQueryExtList) { urlList.add(fhirQueryExt.getValueAsPrimitive().getValueAsString()); } @@ -102,6 +103,7 @@ protected List createRequestUrl(DataRequirement theDataRequirement) { return urlList; } + @SuppressWarnings("ReassignedVariable") protected void resolveCodeFilter(DataRequirement theDataRequirement, List theUrlList, String theBaseQuery) { for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent : theDataRequirement.getCodeFilter()) { @@ -121,26 +123,23 @@ protected void resolveCodeFilter(DataRequirement theDataRequirement, List resolveValueCodingCodes(List theValueCodings) { List 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; } @@ -176,17 +175,17 @@ protected List resolveValueSetCodes(CanonicalType theValueSetId) { @SuppressWarnings("ReassignedVariable") protected StringBuilder getCodesStringBuilder( List theStrings, StringBuilder theCodes, String theSystem, String theCode) { + StringBuilder codes = theCodes; String codeToken = theSystem + "|" + theCode; - int postAppendLength = theCodes.length() + codeToken.length(); - - if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) { - theCodes.append(","); + int postAppendLength = codes.length() + codeToken.length(); + if (codes.length() > 0 && postAppendLength < myMaxUriLength) { + codes.append(","); } else if (postAppendLength > myMaxUriLength) { - theStrings.add(theCodes.toString()); - theCodes = new StringBuilder(); + theStrings.add(codes.toString()); + codes = new StringBuilder(); } - theCodes.append(codeToken); - return theCodes; + codes.append(codeToken); + return codes; } protected String mapCodePathToSearchParam(String theDataType, String thePath) {