diff --git a/README.md b/README.md index 08fdcdbb1..287bb55b2 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,4 @@ Questions about the project can be asked in the [Da Vinci CRD stream on the FHIR This project welcomes Pull Requests. Any issues identified with the RI should be submitted via the [GitHub issue tracker](https://github.com/HL7-DaVinci/CRD/issues). + diff --git a/resources/src/main/java/org/cdshooks/CdsRequest.java b/resources/src/main/java/org/cdshooks/CdsRequest.java index e6654b0e3..06a708b8d 100755 --- a/resources/src/main/java/org/cdshooks/CdsRequest.java +++ b/resources/src/main/java/org/cdshooks/CdsRequest.java @@ -1,7 +1,5 @@ package org.cdshooks; -import com.fasterxml.jackson.annotation.JsonGetter; - import javax.validation.constraints.NotNull; import org.hl7.davinci.EncounterBasedServiceContext; @@ -83,5 +81,18 @@ public void setContext(serviceContextTypeT context) { */ public abstract Object getDataForPrefetchToken(); + @Override + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append("[hook: " + hook + "]"); + sb.append("[hookInstance: " + hookInstance + "]"); + sb.append("[fhirServer: " + fhirServer + "]"); + sb.append("[fhirAuthorization: " + fhirAuthorization + "]"); + sb.append("[context: " + context + "]"); + sb.append("[extension: " + extension + "]"); + sb.append("[prefetch: " + prefetch + "]"); + return sb.toString(); + } + } diff --git a/resources/src/main/java/org/cdshooks/Coding.java b/resources/src/main/java/org/cdshooks/Coding.java new file mode 100644 index 000000000..ced8d63ad --- /dev/null +++ b/resources/src/main/java/org/cdshooks/Coding.java @@ -0,0 +1,19 @@ +package org.cdshooks; + +public class Coding { + + private String code; + + private String system = null; + + private String display = null; + + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + + public String getSystem() { return system; } + public void setSystem(String system) { this.system = system; } + + public String getDisplay() { return display; } + public void setDisplay(String display) { this.display = display; } +} diff --git a/resources/src/main/java/org/cdshooks/Extension.java b/resources/src/main/java/org/cdshooks/Extension.java index 3e9c17db8..672900eb8 100644 --- a/resources/src/main/java/org/cdshooks/Extension.java +++ b/resources/src/main/java/org/cdshooks/Extension.java @@ -10,4 +10,8 @@ public class Extension { public Configuration getConfiguration() { return configuration; } public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + + public String toString(){ + return "Extension configuration: " + configuration; + } } diff --git a/resources/src/main/java/org/cdshooks/Source.java b/resources/src/main/java/org/cdshooks/Source.java index 05b138906..22b63acd6 100755 --- a/resources/src/main/java/org/cdshooks/Source.java +++ b/resources/src/main/java/org/cdshooks/Source.java @@ -5,13 +5,15 @@ public class Source { private String url = null; + private String icon = null; + + private Coding topic = null; + public String getLabel() { return label; } - public void setLabel(String label) { - this.label = label; - } + public void setLabel(String label) { this.label = label; } public String getUrl() { return url; @@ -20,4 +22,12 @@ public String getUrl() { public void setUrl(String url) { this.url = url; } + + public String getIcon() { return icon; } + + public void setIcon(String icon) { this.icon = icon; } + + public Coding getTopic() { return topic; } + + public void setTopic(Coding topic) { this.topic = topic; } } diff --git a/resources/src/main/java/org/hl7/davinci/EncounterBasedServiceContext.java b/resources/src/main/java/org/hl7/davinci/EncounterBasedServiceContext.java index ddc1e1458..d217ce3dd 100644 --- a/resources/src/main/java/org/hl7/davinci/EncounterBasedServiceContext.java +++ b/resources/src/main/java/org/hl7/davinci/EncounterBasedServiceContext.java @@ -1,7 +1,10 @@ package org.hl7.davinci; +import org.hl7.fhir.r4.model.Bundle; + public interface EncounterBasedServiceContext { String getUserId(); String getPatientId(); String getEncounterId(); + Bundle getDraftOrders(); } diff --git a/resources/src/main/java/org/hl7/davinci/PrefetchTemplateElement.java b/resources/src/main/java/org/hl7/davinci/PrefetchTemplateElement.java index 4f36920f3..d7cfbc6ad 100644 --- a/resources/src/main/java/org/hl7/davinci/PrefetchTemplateElement.java +++ b/resources/src/main/java/org/hl7/davinci/PrefetchTemplateElement.java @@ -30,4 +30,9 @@ public String getQuery() { public Class getReturnType() { return returnType; } + + @Override + public String toString(){ + return "[" + key + ", " + query + ", " + returnType + "]"; + } } \ No newline at end of file diff --git a/resources/src/main/java/org/hl7/davinci/r4/CardTypes.java b/resources/src/main/java/org/hl7/davinci/r4/CardTypes.java new file mode 100644 index 000000000..e1284a707 --- /dev/null +++ b/resources/src/main/java/org/hl7/davinci/r4/CardTypes.java @@ -0,0 +1,45 @@ +package org.hl7.davinci.r4; + +import org.cdshooks.Coding; + +public enum CardTypes { + COVERAGE("coverage", "Coverage"), + DOCUMENTATION("documentation", "Documentation"), + PRIOR_AUTH("prior-auth", "Prior Authorization"), + DTR_CLIN("dtr-clin", "DTR Clin"), + DTR_ADMIN("dtr-admin", "DTR Admin"), + CLAIM("claim", "Claim"), + INSURANCE("insurance", "Insurance"), + LIMITS("limits", "Limits"), + NETWORK("network", "Network"), + APPROPRIATE_USE("appropriate-use", "Appropriate Use"), + COST("cost", "Cost"), + THERAPY_ALTERNATIVES_OPT("therapy-alternatives-opt", "Therapy Alternatives Opt"), + THERAPY_ALTERNATIVES_REG("therapy-alternatives-req", "Therapy Alternatives Req"), + CLINICAL_REMINDER("clinical-reminder", "Clinical Reminder"), + DUPLICATE_THERAPY("duplicate-therapy", "Duplicate Therapy"), + CONTRAINDICATION("contraindication", "Contraindication"), + GUIDELINE("guideline", "Guideline"), + OFF_GUIDELINE("off-guideline", "Off Guideline"); + + private String code; + private String display; + private static String codeSystem = "http://hl7.org/fhir/us/davinci-crd/CodeSystem/cardType"; + + CardTypes(String code, String display) { + this.code = code; + this.display = display; + } + + public String getCode() { return code; } + public String getDisplay() { return display; } + public String getCodeSystem() { return codeSystem; } + + public Coding getCoding() { + Coding coding = new Coding(); + coding.setSystem(codeSystem); + coding.setCode(code); + coding.setDisplay(display); + return coding; + } +} diff --git a/resources/src/main/java/org/hl7/davinci/r4/CoverageGuidance.java b/resources/src/main/java/org/hl7/davinci/r4/CoverageGuidance.java new file mode 100644 index 000000000..94b808fa7 --- /dev/null +++ b/resources/src/main/java/org/hl7/davinci/r4/CoverageGuidance.java @@ -0,0 +1,33 @@ +package org.hl7.davinci.r4; + +import org.hl7.fhir.r4.model.Coding; + +public enum CoverageGuidance { + NOT_COVERED("not-covered", "Not Covered"), + COVERED("covered", "Covered"), + PRIOR_AUTH("prior-auth", "Prior authorization"), + CLINICAL("clinical", "Clinical"), + ADMIN("admin", "Admin"); + + + private String code; + private String display; + private static String codeSystem = "http://hl7.org/fhir/us/davinci-crd/CodeSystem/coverageGuidance"; + + CoverageGuidance(String code, String display) { + this.code = code; + this.display = display; + } + + public String getCode() { return code; } + public String getDisplay() { return display; } + public String getCodeSystem() { return codeSystem; } + + public Coding getCoding() { + Coding coding = new Coding(); + coding.setSystem(codeSystem); + coding.setCode(code); + coding.setDisplay(display); + return coding; + } +} diff --git a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java index e1821bc71..222269bff 100644 --- a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java +++ b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; + import org.hl7.davinci.*; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; @@ -169,9 +170,13 @@ public static PractitionerRoleInfo getPractitionerRoleInfo( public static List getPayors(List coverages) { List payors = new ArrayList<>(); for (Coverage coverage: coverages){ - for (Reference ref: coverage.getPayor()){ - Organization organization = (Organization) ref.getResource(); - payors.add(organization); + if (coverage != null) { + for (Reference ref: coverage.getPayor()){ + Organization organization = (Organization) ref.getResource(); + if (organization != null) { + payors.add(organization); + } + } } } return payors; diff --git a/resources/src/main/java/org/hl7/davinci/r4/crdhook/CrdPrefetch.java b/resources/src/main/java/org/hl7/davinci/r4/crdhook/CrdPrefetch.java index 79623b6c8..528c1406c 100644 --- a/resources/src/main/java/org/hl7/davinci/r4/crdhook/CrdPrefetch.java +++ b/resources/src/main/java/org/hl7/davinci/r4/crdhook/CrdPrefetch.java @@ -1,10 +1,14 @@ package org.hl7.davinci.r4.crdhook; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.hl7.davinci.r4.JacksonBundleDeserializer; import org.hl7.davinci.r4.JacksonHapiSerializer; import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; /** * Class that supports the representation of prefetch information in a CDS Hook request. @@ -103,4 +107,74 @@ public void setSupplyRequestBundle(Bundle supplyRequestBundle) { public Bundle getMedicationStatementBundle() { return medicationStatementBundle; } public void setMedicationStatementBundle(Bundle medicationStatementBundle) { this.medicationStatementBundle = medicationStatementBundle; } + + /** + * Checks whether the given resource exists in the requested resource type. + * @param id + * @return + */ + public boolean containsRequestResourceId(String id) { + return this.bundleContainsResourceId(this.deviceRequestBundle, id) + || this.bundleContainsResourceId(this.medicationRequestBundle, id) + || this.bundleContainsResourceId(this.nutritionOrderBundle, id) + || this.bundleContainsResourceId(this.serviceRequestBundle, id) + || this.bundleContainsResourceId(this.supplyRequestBundle, id) + || this.bundleContainsResourceId(this.appointmentBundle, id) + || this.bundleContainsResourceId(this.encounterBundle, id) + || this.bundleContainsResourceId(this.medicationDispenseBundle, id) + || this.bundleContainsResourceId(this.medicationStatementBundle, id); + } + + /** + * Returns whether the given bundle contains the given resource. + * @param bundle + * @param id + * @return + */ + private boolean bundleContainsResourceId(Bundle bundle, String id) { + if (bundle == null) { + return false; + } + if (id.contains("/")) { + String[] splitId = id.split("/"); + id = splitId[splitId.length-1]; + } + final String idToCheck = id; + return bundle.getEntry().stream().anyMatch(entry -> entry.getResource().getId().contains(idToCheck)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + List entries = new ArrayList<>(); + if(this.deviceRequestBundle != null){ + entries = this.deviceRequestBundle.getEntry(); + } else if(this.nutritionOrderBundle != null){ + entries = this.nutritionOrderBundle.getEntry(); + } else if(this.serviceRequestBundle != null){ + entries = this.serviceRequestBundle.getEntry(); + } else if(this.medicationDispenseBundle != null){ + entries = this.medicationDispenseBundle.getEntry(); + } else if(this.medicationStatementBundle != null){ + entries = this.medicationStatementBundle.getEntry(); + } else if(this.encounterBundle != null){ + entries = this.encounterBundle.getEntry(); + } else if(this.appointmentBundle != null){ + entries = this.appointmentBundle.getEntry(); + } else if(this.medicationRequestBundle != null){ + entries = this.medicationRequestBundle.getEntry(); + } else if(this.supplyRequestBundle != null){ + entries = this.supplyRequestBundle.getEntry(); + } + sb.append("["); + for(BundleEntryComponent entry : entries) { + sb.append(entry.getResource()); + sb.append("-"); + sb.append(entry.getResource().getId()); + sb.append(","); + } + sb.setLength(sb.length()-1); + sb.append("]"); + return sb.toString(); + } } diff --git a/resources/src/main/java/org/hl7/davinci/r4/crdhook/ordersign/OrderSignRequest.java b/resources/src/main/java/org/hl7/davinci/r4/crdhook/ordersign/OrderSignRequest.java index 61a82c0d8..8ff435f65 100644 --- a/resources/src/main/java/org/hl7/davinci/r4/crdhook/ordersign/OrderSignRequest.java +++ b/resources/src/main/java/org/hl7/davinci/r4/crdhook/ordersign/OrderSignRequest.java @@ -32,5 +32,10 @@ public Object getDataForPrefetchToken() { return mapForPrefetchTemplates; } + @Override + public String toString() { + return "Super: " + super.toString() + " OrderSignRequest: " + getDataForPrefetchToken().toString(); + } + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index 66c8de4f6..aa7b8abed 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -1,19 +1,28 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd; +import com.google.gson.Gson; + +import com.google.gson.Gson; + import org.apache.commons.lang.StringUtils; import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.RequestIncompleteException; import org.hl7.davinci.endpoint.components.CardBuilder; -import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.hl7.davinci.endpoint.components.PrefetchHydrator; +import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; +import org.hl7.davinci.endpoint.components.QueryBatchRequest; import org.hl7.davinci.endpoint.database.FhirResourceRepository; import org.hl7.davinci.endpoint.database.RequestLog; import org.hl7.davinci.endpoint.database.RequestService; import org.hl7.davinci.endpoint.files.FileStore; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; +import org.hl7.davinci.r4.CardTypes; +import org.hl7.davinci.r4.CoverageGuidance; +import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; +import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; import org.hl7.davinci.endpoint.database.FhirResourceRepository; @@ -39,17 +48,17 @@ public abstract class CdsService> extends @Autowired FileStore fileStore; - + @Autowired private FhirResourceRepository fhirResourceRepository; - + protected FhirComponentsT fhirComponents; public CdsService(String id, Hook hook, String title, String description, List prefetchElements, FhirComponentsT fhirComponents, DiscoveryExtension extension) { - + super(id, hook, title, description, prefetchElements, fhirComponents, extension); this.fhirComponents = fhirComponents; } @@ -74,18 +83,29 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // hydrated requestLog.advanceTimeline(requestService); - // logger.info("***** ***** request from requestLog: "+requestLog.toString() ); + // Attempt a Query Batch Request to backfill missing attributes. + if (myConfig.isQueryBatchRequest()) { + QueryBatchRequest qbr = new QueryBatchRequest(this.fhirComponents); + this.attempQueryBatchRequest(request, qbr); + } + + logger.info("***** ***** request from requestLog: " + requestLog.toString() ); CdsResponse response = new CdsResponse(); + Gson gson = new Gson(); + final String jsonObject = gson.toJson(request.getPrefetch()); + logger.info("Final populated CRDPrefetch: " + jsonObject); + // CQL Fetched List lookupResults; try { lookupResults = this.createCqlExecutionContexts(request, fileStore, applicationBaseUrl.toString() + "/"); requestLog.advanceTimeline(requestService); } catch (RequestIncompleteException e) { + logger.warn("RequestIncompleteException " + request); logger.warn(e.getMessage() + "; summary card sent to client"); - response.addCard(CardBuilder.summaryCard(e.getMessage())); + response.addCard(CardBuilder.summaryCard(CardTypes.COVERAGE, e.getMessage())); requestLog.setCardListFromCards(response.getCards()); requestLog.setResults(e.getMessage()); requestService.edit(requestLog); @@ -133,7 +153,20 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); - response.addCard(CardBuilder.transform(results, smartAppLinks)); + + if (coverageRequirements.isPriorAuthRequired()) { + Card card = CardBuilder.transform(CardTypes.PRIOR_AUTH, results, smartAppLinks); + card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", + true, CoverageGuidance.ADMIN)); + response.addCard(card); + } else if (coverageRequirements.isDocumentationRequired()) { + Card card = CardBuilder.transform(CardTypes.DTR_CLIN, results, smartAppLinks); + card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", + true, CoverageGuidance.CLINICAL)); + response.addCard(card); + } // add a card for an alternative therapy if there is one if (results.getAlternativeTherapy().getApplies() && hookConfiguration.getAlternativeTherapy()) { @@ -146,15 +179,15 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a } } else { logger.warn("Unspecified Questionnaire URI; summary card sent to client"); - response.addCard(CardBuilder.transform(results)); + response.addCard(CardBuilder.transform(CardTypes.COVERAGE, results)); } } else { // no prior auth or documentation required logger.info("Add the no doc or prior auth required card"); - Card card = CardBuilder.transform(results); + Card card = CardBuilder.transform(CardTypes.COVERAGE, results); card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", - true)); + true, CoverageGuidance.COVERED)); card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); response.addCard(card); } @@ -174,9 +207,9 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (!foundApplicableRule) { String msg = "No documentation rules found"; logger.warn(msg + "; summary card sent to client"); - response.addCard(CardBuilder.summaryCard(msg)); + response.addCard(CardBuilder.summaryCard(CardTypes.COVERAGE, msg)); } - CardBuilder.errorCardIfNonePresent(response); + CardBuilder.errorCardIfNonePresent(CardTypes.COVERAGE, response); } // Adding card to requestLog @@ -257,4 +290,10 @@ public abstract List createCqlExecutionContexts(r FileStore fileStore, String baseUrl) throws RequestIncompleteException; protected abstract CqlResultsForCard executeCqlAndGetRelevantResults(Context context, String topic); + + /** + * Delegates query batch request to child classes based on their prefetch types. + */ + protected abstract void attempQueryBatchRequest(requestTypeT request, QueryBatchRequest qbr); + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java index 738373737..51055319e 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java @@ -7,6 +7,7 @@ import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.endpoint.components.CardBuilder; +import org.hl7.davinci.r4.CardTypes; import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.hl7.fhir.r4.model.Coding; import org.slf4j.Logger; @@ -61,7 +62,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a CdsResponse response = new CdsResponse(); List medications = getMedications(request); for (Coding medication : medications) { - Card card = CardBuilder.summaryCard(""); + Card card = CardBuilder.summaryCard(CardTypes.COVERAGE, ""); if (isRemsDrug(medication)) { card.setSummary(String.format("%s has REMS", medication.getDisplay())); } else { diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java index a5be23692..ffd498f6c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java @@ -8,7 +8,6 @@ import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; import org.hl7.davinci.r4.Utilities; -import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,30 +23,27 @@ public class FhirBundleProcessor { static final Logger logger = LoggerFactory.getLogger(FhirBundleProcessor.class); private FileStore fileStore; - private CrdPrefetch prefetch; private String baseUrl; private List selections; private List results = new ArrayList<>(); - public FhirBundleProcessor(CrdPrefetch prefetch, FileStore fileStore, String baseUrl, List selections) { - this.prefetch = prefetch; + public FhirBundleProcessor(FileStore fileStore, String baseUrl, List selections) { this.fileStore = fileStore; this.baseUrl = baseUrl; this.selections = selections; } - public FhirBundleProcessor(CrdPrefetch prefetch, FileStore fileStore, String baseUrl) { - this(prefetch, fileStore, baseUrl, new ArrayList<>()); + public FhirBundleProcessor(FileStore fileStore, String baseUrl) { + this(fileStore, baseUrl, new ArrayList<>()); } public List getResults() { return results; } - public void processDeviceRequests() { - Bundle deviceRequestBundle = prefetch.getDeviceRequestBundle(); + public void processDeviceRequests(Bundle deviceRequestBundle) { List deviceRequestList = Utilities.getResourcesOfTypeFromBundle(DeviceRequest.class, deviceRequestBundle); if (!deviceRequestList.isEmpty()) { - logger.info("r4/FhirBundleProcessor::getAndProcessDeviceRequests: DeviceRequest(s) found"); + logger.info("r4/FhirBundleProcessor::processDeviceRequests: DeviceRequest(s) found"); for (DeviceRequest deviceRequest : deviceRequestList) { if (idInSelectionsList(deviceRequest.getId(), selections)) { @@ -58,11 +54,10 @@ public void processDeviceRequests() { } } - public void processMedicationRequests() { - Bundle medicationRequestBundle = prefetch.getMedicationRequestBundle(); + public void processMedicationRequests(Bundle medicationRequestBundle) { List medicationRequestList = Utilities.getResourcesOfTypeFromBundle(MedicationRequest.class, medicationRequestBundle); if (!medicationRequestList.isEmpty()) { - logger.info("r4/FhirBundleProcessor::getAndProcessMedicationRequests: MedicationRequest(s) found"); + logger.info("r4/FhirBundleProcessor::processMedicationRequests: MedicationRequest(s) found"); for (MedicationRequest medicationRequest : medicationRequestList) { if (idInSelectionsList(medicationRequest.getId(), selections)) { @@ -73,11 +68,10 @@ public void processMedicationRequests() { } } - public void processMedicationDispenses() { - Bundle medicationDispenseBundle = prefetch.getMedicationDispenseBundle(); + public void processMedicationDispenses(Bundle medicationDispenseBundle) { List medicationDispenseList = Utilities.getResourcesOfTypeFromBundle(MedicationDispense.class, medicationDispenseBundle); if (!medicationDispenseList.isEmpty()) { - logger.info("r4/FhirBundleProcessor::getAndProcessMedicationDispenses: MedicationDispense(s) found"); + logger.info("r4/FhirBundleProcessor::processMedicationDispenses: MedicationDispense(s) found"); List payorList = Utilities.getResourcesOfTypeFromBundle(Organization.class, medicationDispenseBundle); @@ -91,11 +85,10 @@ public void processMedicationDispenses() { } } - public void processServiceRequests() { - Bundle serviceRequestBundle = prefetch.getServiceRequestBundle(); + public void processServiceRequests(Bundle serviceRequestBundle) { List serviceRequestList = Utilities.getResourcesOfTypeFromBundle(ServiceRequest.class, serviceRequestBundle); if (!serviceRequestList.isEmpty()) { - logger.info("r4/FhirBundleProcessor::getAndProcessServiceRequests: ServiceRequest(s) found"); + logger.info("r4/FhirBundleProcessor::processServiceRequests: ServiceRequest(s) found"); for (ServiceRequest serviceRequest : serviceRequestList) { if (idInSelectionsList(serviceRequest.getId(), selections)) { @@ -106,11 +99,8 @@ public void processServiceRequests() { } } - public void processOrderSelectMedicationStatements() { - Bundle medicationRequestBundle = prefetch.getMedicationRequestBundle(); + public void processOrderSelectMedicationStatements(Bundle medicationRequestBundle, Bundle medicationStatementBundle) { List medicationRequestList = Utilities.getResourcesOfTypeFromBundle(MedicationRequest.class, medicationRequestBundle); - - Bundle medicationStatementBundle = prefetch.getMedicationStatementBundle(); List medicationStatementList = Utilities.getResourcesOfTypeFromBundle(MedicationStatement.class, medicationStatementBundle); if (!medicationRequestList.isEmpty()) { @@ -151,6 +141,8 @@ private List createCriteriaList(CodeableConcept if (insurance != null) { List coverages = insurance.stream() .map(reference -> (Coverage) reference.getResource()).collect(Collectors.toList()); + // Remove null coverages that may not have resolved. + coverages = coverages.stream().filter(coverage -> coverage != null).collect(Collectors.toList()); payors = Utilities.getPayors(coverages); } else if (payorList != null) { payors = payorList; diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java index 912b97d8d..b65c61f3c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java @@ -1,20 +1,36 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; import org.cdshooks.AlternativeTherapy; -import org.hl7.fhir.instance.model.api.IBase; +import org.cdshooks.CdsRequest; +import org.hl7.davinci.FatalRequestIncompleteException; +import org.hl7.davinci.FhirComponentsT; +import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; +import com.google.gson.JsonElement; + public class FhirRequestProcessor { static final Logger logger = LoggerFactory.getLogger(FhirRequestProcessor.class); + private static final String REFERENCE = "reference"; + public static IBaseResource swapTherapyInRequest(IBaseResource request, AlternativeTherapy alternativeTherapy) { IBaseResource output = request; @@ -164,4 +180,334 @@ public static IBaseResource addSupportingInfoToRequest(IBaseResource request, Re return output; } + + /** + * Adds the given resource to the given CrdPrefetch based on the given resource + * type. + * + * @param crdResponse + * @param resource + * @param requestType + */ + public static void addToCrdPrefetchRequest(CrdPrefetch crdResponse, ResourceType requestType, + List resourcesToAdd) { + switch (requestType) { + case DeviceRequest: + if (crdResponse.getDeviceRequestBundle() == null) { + crdResponse.setDeviceRequestBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getDeviceRequestBundle(), resourcesToAdd); + break; + case MedicationRequest: + if (crdResponse.getMedicationRequestBundle() == null) { + crdResponse.setMedicationRequestBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getMedicationRequestBundle(), resourcesToAdd); + break; + case NutritionOrder: + if (crdResponse.getNutritionOrderBundle() == null) { + crdResponse.setNutritionOrderBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getNutritionOrderBundle(), resourcesToAdd); + break; + case ServiceRequest: + if (crdResponse.getServiceRequestBundle() == null) { + crdResponse.setServiceRequestBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getServiceRequestBundle(), resourcesToAdd); + break; + case SupplyRequest: + if (crdResponse.getSupplyRequestBundle() == null) { + crdResponse.setSupplyRequestBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getSupplyRequestBundle(), resourcesToAdd); + break; + case Appointment: + if (crdResponse.getAppointmentBundle() == null) { + crdResponse.setAppointmentBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getAppointmentBundle(), resourcesToAdd); + break; + case Encounter: + if (crdResponse.getEncounterBundle() == null) { + crdResponse.setEncounterBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getEncounterBundle(), resourcesToAdd); + break; + case MedicationDispense: + if (crdResponse.getMedicationDispenseBundle() == null) { + crdResponse.setMedicationDispenseBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getMedicationDispenseBundle(), resourcesToAdd); + break; + case MedicationStatement: + if (crdResponse.getMedicationStatementBundle() == null) { + crdResponse.setMedicationStatementBundle(new Bundle()); + } + addNonDuplicateResourcesToBundle(crdResponse.getMedicationStatementBundle(), resourcesToAdd); + break; + default: + throw new RuntimeException("Unexpected resource type for draft order request. Given " + requestType + "."); + } + } + + /** + * Adds non-duplicate resources that do not already exist in the bundle to the bundle. + */ + private static void addNonDuplicateResourcesToBundle(Bundle bundle, List resourcesToAdd) { + for (BundleEntryComponent resourceEntry : resourcesToAdd) { + if (!bundle.getEntry().stream() + .anyMatch(bundleEntry -> bundleEntry.getResource().getId().equals(resourceEntry.getResource().getId()))) { + bundle.addEntry(resourceEntry); + } + } + } + + /** + * Extracts patients from the given bundle. + * @param bundle + * @return + */ + public static List extractPatientsFromBundle(Bundle bundle) { + List patients = new ArrayList<>(); + for (BundleEntryComponent entry : bundle.getEntry()) { + if (entry.getResource().getResourceType().equals(ResourceType.Patient)) { + patients.add((Patient) entry.getResource()); + } + } + return patients; + } + + /** + * Extracts the coverage elements from the given bundle. + * @param bundle + * @return + */ + public static List extractCoverageFromBundle(Bundle bundle) { + List coverages = new ArrayList<>(); + for (BundleEntryComponent entry : bundle.getEntry()) { + if (entry.getResource().getResourceType().equals(ResourceType.Coverage)) { + coverages.add((Coverage) entry.getResource()); + } + } + return coverages; + } + + /** + * Extracts the reference Ids from the given JSON. + * + * @param references + * @param jsonElement + */ + public static void extractReferenceIds(List references, JsonElement jsonElement) { + if (jsonElement.isJsonArray()) { + for (JsonElement innerElement : jsonElement.getAsJsonArray()) { + extractReferenceIds(references, innerElement); + } + } else if (jsonElement.isJsonObject()) { + if (jsonElement.getAsJsonObject().has(REFERENCE)) { + if (jsonElement.getAsJsonObject().get(REFERENCE).isJsonObject()) { + String referenceId = jsonElement.getAsJsonObject().get(REFERENCE).getAsJsonObject() + .get("myStringValue").toString(); + references.add(referenceId.replace("\"", "")); + } + } + } + } + + /** + * Adds the given coverage and patient to the given resource. + * @param resource The resource to add coverage and patient data to. + * @param patients The list of valid patients. + * @param coverages The list of valid coverages. + */ + public static void addInsuranceAndSubject(Resource resource, List patients, List coverages) { + // Source: https://build.fhir.org/ig/HL7/davinci-crd/hooks.html#prefetch + switch(resource.getResourceType()){ + case DeviceRequest: + DeviceRequest deviceRequest = (DeviceRequest) resource; + if(!coverages.isEmpty()){ + deviceRequest.getInsuranceFirstRep().setResource(coverages.get(0)); + } + if(!patients.isEmpty()){ + deviceRequest.getSubject().setResource(patients.get(0)); + } + break; + case Encounter: + Encounter encounter = (Encounter) resource; + // Encounter does not have an insurance field. + if(!patients.isEmpty()){ + encounter.getSubject().setResource(patients.get(0)); + } + break; + case MedicationRequest: + MedicationRequest medicationRequest = (MedicationRequest) resource; + if(!coverages.isEmpty()){ + medicationRequest.getInsuranceFirstRep().setResource(coverages.get(0)); + } + if(!patients.isEmpty()){ + medicationRequest.getSubject().setResource(patients.get(0)); + } + case MedicationDispense: + MedicationDispense dispense = (MedicationDispense) resource; + // MedicationDispense does not have an insurance field. + // It is also not defined in the prefetch section of the spec, though that is likely a typo from the duplicated MedicationRequest. + if(!patients.isEmpty()){ + dispense.getSubject().setResource(patients.get(0)); + } + break; + case ServiceRequest: + ServiceRequest serviceRequest = (ServiceRequest) resource; + if(!coverages.isEmpty()){ + serviceRequest.getInsuranceFirstRep().setResource(coverages.get(0)); + } + if(!patients.isEmpty()){ + serviceRequest.getSubject().setResource(patients.get(0)); + } + break; + case NutritionOrder: + // NutritionOrder does not define an insurance or subject field. + case Appointment: + // Appointment does not define an insurance or subject field. + default: + // The input request type is not one of the 7 defined above and in the spec. + throw new RuntimeException("Invalid request type: " + resource.getResourceType() + "."); + } + } + + /** + * Execute a Fhir Query with a URL-based query. + * @param queryUrl + * @param cdsRequest + * @param fhirComponents + * @param httpMethod + * @return + */ + public static IBaseResource executeFhirQueryUrl(String queryUrl, CdsRequest cdsRequest, + FhirComponentsT fhirComponents, HttpMethod httpMethod) { + return executeFhirQuery("", queryUrl, cdsRequest, fhirComponents, httpMethod); + } + + /** + * Execute a Fhir Query with a body-based query. + * @param queryBody + * @param cdsRequest + * @param fhirComponents + * @param httpMethod + * @return + */ + public static IBaseResource executeFhirQueryBody(String queryBody, CdsRequest cdsRequest, + FhirComponentsT fhirComponents, HttpMethod httpMethod) { + return executeFhirQuery(queryBody, "", cdsRequest, fhirComponents, httpMethod); + } + + /** + * Execute a Fhir query with the given query body and query url. + * @param queryBody + * @param queryUrl + * @param cdsRequest + * @param fhirComponents + * @param httpMethod + * @return + */ + public static IBaseResource executeFhirQuery(String queryBody, String queryUrl, CdsRequest cdsRequest, + FhirComponentsT fhirComponents, HttpMethod httpMethod) { + if (cdsRequest.getFhirServer() == null) { + throw new FatalRequestIncompleteException("Attempted to perform a Query Batch Request, but no fhir " + + "server provided."); + } + // Remove the trailing '/' if there is one. + String fhirBase = cdsRequest.getFhirServer(); + if (fhirBase != null && fhirBase.endsWith("/")) { + fhirBase = fhirBase.substring(0, fhirBase.length() - 1); + } + String fullUrl = fhirBase + "/" + queryUrl; + // TODO: Once our provider fhir server is up, switch the fetch to use the hapi client instead + // cdsRequest.getOauth(); + // FhirContext ctx = FhirContext.forR4(); + // BearerTokenAuthInterceptor authInterceptor = new BearerTokenAuthInterceptor(oauth.get("access_token")); + // IGenericClient client = ctx.newRestfulGenericClient(server); + // client.registerInterceptor(authInterceptor); + // return client; + // IGenericClient client = ctx.newRestfulGenericClient(serverBase); + // return client.search().byUrl(query).encodedJson().returnBundle(Bundle.class).execute(); + + String token = null; + if (cdsRequest.getFhirAuthorization() != null) { + token = cdsRequest.getFhirAuthorization().getAccessToken(); + } + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + if(!queryBody.isEmpty()){ + headers.setContentType(MediaType.APPLICATION_JSON); + } + if (token != null) { + headers.set("Authorization", "Bearer " + token); + } + HttpEntity entity = new HttpEntity<>(queryBody, headers); + try { + logger.info("Fetching: " + fullUrl); + // Request source: https://www.hl7.org/fhir/http.html#transaction + ResponseEntity response = restTemplate.exchange(fullUrl, httpMethod, entity, String.class); + logger.info("Fetched: " + response.getBody()); + return fhirComponents.getJsonParser().parseResource(response.getBody()); + } catch (RestClientException e) { + logger.warn("Unable to make the fetch request", e); + return null; + } + } + + public static IBaseResource addExtensionToRequest(IBaseResource request, Extension extension) { + IBaseResource output = request; + + switch (request.fhirType()) { + case "DeviceRequest": + case "MedicationRequest": + case "CommunicationRequest": + case "ServiceRequest": + case "NutritionOrder": + case "Appointment": + case "Encounter": + DomainResource domainResource = ((DomainResource) request); + domainResource.addExtension(extension); + output = domainResource; + break; + default: + logger.info("Unsupported fhir R4 resource type (" + request.fhirType() + ") when adding extension"); + throw new RuntimeException("Unsupported fhir R4 resource type " + request.fhirType()); + } + + return output; + } + + public static Reference getCoverageFromRequest(IBaseResource request) { + Reference coverage = null; + + switch (request.fhirType()) { + case "DeviceRequest": + DeviceRequest deviceRequest = ((DeviceRequest) request).copy(); + coverage = deviceRequest.getInsurance().get(0); + break; + case "MedicationRequest": + MedicationRequest medicationRequest = ((MedicationRequest) request).copy(); + coverage = medicationRequest.getInsurance().get(0); + break; + case "ServiceRequest": + ServiceRequest serviceRequest = ((ServiceRequest) request).copy(); + coverage = serviceRequest.getInsurance().get(0); + break; + case "MedicationDispense": + case "Appointment": + case "NutritionOrder": + case "SupplyRequest": + case "Encounter": + default: + logger.info("Unsupported fhir R4 resource type (" + request.fhirType() + ") when retrieving coverage"); + throw new NoCoverageException("No coverage found within fhir R4 resource type " + request.fhirType()); + } + + return coverage; + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/NoCoverageException.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/NoCoverageException.java new file mode 100644 index 000000000..3ec68ddd7 --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/NoCoverageException.java @@ -0,0 +1,13 @@ +package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; + + +public class NoCoverageException extends RuntimeException { + + public NoCoverageException() { super(); } + + public NoCoverageException(String message) { super(message); } + + public NoCoverageException(String message, Throwable cause) { super(message, cause); } + + public NoCoverageException(Throwable cause) { super(cause); } + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectService.java index 503a57e64..83b6a00ec 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectService.java @@ -13,9 +13,11 @@ import org.hl7.davinci.RequestIncompleteException; import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsService; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; +import org.hl7.davinci.endpoint.components.QueryBatchRequest; import org.hl7.davinci.endpoint.files.FileStore; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; import org.hl7.davinci.r4.FhirComponents; +import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.davinci.r4.crdhook.orderselect.CrdPrefetchTemplateElements; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; import org.hl7.fhir.r4.model.Coding; @@ -46,8 +48,9 @@ public List createCqlExecutionContexts(OrderSelec List selections = Arrays.asList(orderSelectRequest.getContext().getSelections()); - FhirBundleProcessor fhirBundleProcessor = new FhirBundleProcessor(orderSelectRequest.getPrefetch(), fileStore, baseUrl, selections); - fhirBundleProcessor.processOrderSelectMedicationStatements(); + FhirBundleProcessor fhirBundleProcessor = new FhirBundleProcessor(fileStore, baseUrl, selections); + CrdPrefetch prefetch = orderSelectRequest.getPrefetch(); + fhirBundleProcessor.processOrderSelectMedicationStatements(prefetch.getMedicationRequestBundle(), prefetch.getMedicationStatementBundle()); List results = fhirBundleProcessor.getResults(); if (results.isEmpty()) { @@ -113,4 +116,9 @@ private Coding getFirstCodeFromCodingListObject(Object c) { } return codingList.get(0); } + + @Override + protected void attempQueryBatchRequest(OrderSelectRequest request, QueryBatchRequest batchRequest) { + batchRequest.performQueryBatchRequest(request, request.getPrefetch()); + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java index 8b9c7d2ba..778f2c736 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java @@ -12,11 +12,13 @@ import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.RequestIncompleteException; import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsService; +import org.hl7.davinci.endpoint.components.QueryBatchRequest; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.hl7.davinci.endpoint.files.FileStore; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; import org.hl7.davinci.r4.FhirComponents; import org.hl7.davinci.r4.crdhook.ConfigurationOption; +import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.hl7.davinci.r4.crdhook.ordersign.CrdExtensionConfigurationOptions; import org.hl7.davinci.r4.crdhook.ordersign.CrdPrefetchTemplateElements; @@ -57,11 +59,12 @@ public class OrderSignService extends CdsService { @Override public List createCqlExecutionContexts(OrderSignRequest orderSignRequest, FileStore fileStore, String baseUrl) { - FhirBundleProcessor fhirBundleProcessor = new FhirBundleProcessor(orderSignRequest.getPrefetch(), fileStore, baseUrl); - fhirBundleProcessor.processDeviceRequests(); - fhirBundleProcessor.processMedicationRequests(); - fhirBundleProcessor.processServiceRequests(); - fhirBundleProcessor.processMedicationDispenses(); + FhirBundleProcessor fhirBundleProcessor = new FhirBundleProcessor(fileStore, baseUrl); + CrdPrefetch prefetch = orderSignRequest.getPrefetch(); + fhirBundleProcessor.processDeviceRequests(prefetch.getDeviceRequestBundle()); + fhirBundleProcessor.processMedicationRequests(prefetch.getMedicationRequestBundle()); + fhirBundleProcessor.processServiceRequests(prefetch.getServiceRequestBundle()); + fhirBundleProcessor.processMedicationDispenses(prefetch.getMedicationDispenseBundle()); List results = fhirBundleProcessor.getResults(); if (results.isEmpty()) { @@ -221,4 +224,9 @@ protected CqlResultsForCard executeCqlAndGetRelevantResults(Context context, Str return results; } + + @Override + protected void attempQueryBatchRequest(OrderSignRequest orderSignRequest, QueryBatchRequest batchRequest) { + batchRequest.performQueryBatchRequest(orderSignRequest, orderSignRequest.getPrefetch()); + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java index e67ff6f42..be77f9232 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java @@ -5,12 +5,15 @@ import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirRequestProcessor; +import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.NoCoverageException; import org.hl7.davinci.endpoint.database.FhirResource; import org.hl7.davinci.endpoint.database.FhirResourceRepository; +import org.hl7.davinci.r4.CardTypes; +import org.hl7.davinci.r4.CoverageGuidance; import org.hl7.davinci.r4.Utilities; -import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Extension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,11 +80,12 @@ public CqlResultsForCard setRequest(IBaseResource request) { /** * Transforms a result from the database into a card. * + * @param cardType * @param cqlResults * @return card with appropriate information */ - public static Card transform(CqlResultsForCard cqlResults, Boolean addLink) { - Card card = baseCard(); + public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Boolean addLink) { + Card card = baseCard(cardType); if (addLink) { Link link = new Link(); @@ -100,22 +104,24 @@ public static Card transform(CqlResultsForCard cqlResults, Boolean addLink) { /** * Transforms a result from the database into a card, defaults to adding the link. * + * @param cardType * @param cqlResults * @return card with appropriate information */ - public static Card transform(CqlResultsForCard cqlResults) { - return transform(cqlResults, true); + public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults) { + return transform(cardType, cqlResults, true); } /** * Transforms a result from the database into a card. * + * @param cardType * @param cqlResults * @param smartAppLaunchLink smart app launch Link * @return card with appropriate information */ - public static Card transform(CqlResultsForCard cqlResults, Link smartAppLaunchLink) { - Card card = transform(cqlResults); + public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Link smartAppLaunchLink) { + Card card = transform(cardType, cqlResults); List links = new ArrayList(card.getLinks()); links.add(smartAppLaunchLink); card.setLinks(links); @@ -125,12 +131,13 @@ public static Card transform(CqlResultsForCard cqlResults, Link smartAppLaunchLi /** * Tranform the CQL results for card * then add a list of smart app launch links to the card + * @param cardType * @param cqlResults The CQL results * @param smartAppLaunchLinks a list of links * @return card to be returned */ - public static Card transform(CqlResultsForCard cqlResults, List smartAppLaunchLinks) { - Card card = transform(cqlResults); + public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, List smartAppLaunchLinks) { + Card card = transform(cardType, cqlResults); List links = new ArrayList(card.getLinks()); links.addAll(smartAppLaunchLinks); card.setLinks(links); @@ -140,11 +147,12 @@ public static Card transform(CqlResultsForCard cqlResults, List smartAppLa /** * Creates a card with a summary but also has all of the necessary fields populated to be valid. * + * @param cardType * @param summary The desired summary for the card * @return valid card */ - public static Card summaryCard(String summary) { - Card card = baseCard(); + public static Card summaryCard(CardTypes cardType, String summary) { + Card card = baseCard(cardType); card.setSummary(summary); return card; } @@ -152,7 +160,7 @@ public static Card summaryCard(String summary) { public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, IBaseResource resource, FhirComponentsT fhirComponents) { logger.info("Build Alternative Therapy Card: " + alternativeTherapy.toString()); - Card card = baseCard(); + Card card = baseCard(CardTypes.THERAPY_ALTERNATIVES_OPT); card.setSummary("Alternative Therapy Suggested"); card.setDetail(alternativeTherapy.getDisplay() + " (" + alternativeTherapy.getCode() + ") should be used instead."); @@ -197,7 +205,7 @@ public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, public static Card drugInteractionCard(DrugInteraction drugInteraction) { logger.info("Build Drug Interaction Card: " + drugInteraction.getSummary()); - Card card = baseCard(); + Card card = baseCard(CardTypes.CONTRAINDICATION); card.setSummary(drugInteraction.getSummary()); card.setDetail(drugInteraction.getDetail()); card.setIndicator(Card.IndicatorEnum.WARNING); @@ -215,7 +223,7 @@ public static Card priorAuthCard(CqlResultsForCard cqlResults, FhirResourceRepository fhirResourceRepository) { logger.info("Build Prior Auth Card"); - Card card = transform(cqlResults, false); + Card card = transform(CardTypes.PRIOR_AUTH, cqlResults, false); // create the ClaimResponse ClaimResponse claimResponse = Utilities.createClaimResponse(priorAuthId, patientId, payerId, providerId, applicationFhirPath); @@ -239,13 +247,13 @@ public static Card priorAuthCard(CqlResultsForCard cqlResults, // add suggestion with ClaimResponse Suggestion suggestionWithClaimResponse = createSuggestionWithResource(outputRequest, claimResponse, fhirComponents, - "Store the prior authorization in the EHR"); + "Store the prior authorization in the EHR", true); card.addSuggestionsItem(suggestionWithClaimResponse); // add suggestion with annotation Suggestion suggestionWithAnnotation = createSuggestionWithNote(card, outputRequest, fhirComponents, "Store prior authorization as an annotation to the order", "Add authorization to record", - false); + false, CoverageGuidance.PRIOR_AUTH); card.addSuggestionsItem(suggestionWithAnnotation); card.setSelectionBehavior(Card.SelectionBehaviorEnum.AT_MOST_ONE); @@ -256,11 +264,12 @@ public static Card priorAuthCard(CqlResultsForCard cqlResults, public static Suggestion createSuggestionWithResource(IBaseResource request, IBaseResource resource, FhirComponentsT fhirComponents, - String label) { + String label, + boolean isRecommended) { Suggestion suggestion = new Suggestion(); suggestion.setLabel(label); - suggestion.setIsRecommended(true); + suggestion.setIsRecommended(isRecommended); // build the create Action Action createAction = new Action(fhirComponents); @@ -278,12 +287,15 @@ public static Suggestion createSuggestionWithResource(IBaseResource request, return suggestion; } + public static Suggestion createSuggestionWithNote(Card card, IBaseResource request, FhirComponentsT fhirComponents, String label, String description, - boolean isRecommended) { + boolean isRecommended, + CoverageGuidance coverageGuidance) { + Date now = new Date(); Suggestion requestWithNoteSuggestion = new Suggestion(); requestWithNoteSuggestion.setLabel(label); @@ -297,9 +309,77 @@ public static Suggestion createSuggestionWithNote(Card card, annotation.setAuthor(author); String text = card.getSummary() + ": " + card.getDetail(); annotation.setText(text); - annotation.setTime(new Date()); // set the date and time to now + annotation.setTime(now); // set the date and time to now IBaseResource resource = FhirRequestProcessor.addNoteToRequest(request, annotation); + try { + // build the Extension with the coverage information + Extension extension = new Extension(); + extension.setUrl("http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information"); + + Extension coverageInfo = new Extension(); + coverageInfo.setUrl("coverageInfo") + .setValue(coverageGuidance.getCoding()); + extension.addExtension(coverageInfo); + + Extension coverageExtension = new Extension(); + Reference coverageReference = new Reference(); + + //TODO: get the coverage from the prefetch and pass it into here instead of retrieving it from the request + coverageReference.setReference(FhirRequestProcessor.getCoverageFromRequest(request).getReference()); + + coverageExtension.setUrl("coverage") + .setValue(coverageReference); + extension.addExtension(coverageExtension); + + Extension date = new Extension(); + date.setUrl("date") + .setValue(new DateType().setValue(now)); + extension.addExtension(date); + Extension identifier = new Extension(); + String id = UUID.randomUUID().toString(); + identifier.setUrl("identifier") + .setValue(new StringType(id)); + extension.addExtension(identifier); + resource = FhirRequestProcessor.addExtensionToRequest(resource, extension); + } catch (NoCoverageException e) { + logger.warn("No Coverage, discrete coverage extension will not be included: " + e.getMessage()); + } + + try { + // build the Extension with the coverage information + Extension extension = new Extension(); + extension.setUrl("http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information"); + + Extension coverageInfo = new Extension(); + coverageInfo.setUrl("coverageInfo") + .setValue(coverageGuidance.getCoding()); + extension.addExtension(coverageInfo); + + Extension coverageExtension = new Extension(); + Reference coverageReference = new Reference(); + + //TODO: get the coverage from the prefetch and pass it into here instead of retrieving it from the request + coverageReference.setReference(FhirRequestProcessor.getCoverageFromRequest(request).getReference()); + + coverageExtension.setUrl("coverage") + .setValue(coverageReference); + extension.addExtension(coverageExtension); + + Extension date = new Extension(); + date.setUrl("date") + .setValue(new DateType().setValue(now)); + extension.addExtension(date); + Extension identifier = new Extension(); + String id = UUID.randomUUID().toString(); + identifier.setUrl("identifier") + .setValue(new StringType(id)); + extension.addExtension(identifier); + resource = FhirRequestProcessor.addExtensionToRequest(resource, extension); + } catch (NoCoverageException e) { + logger.warn("No Coverage, discrete coverage extension will not be included: " + e.getMessage()); + } + Action updateAction = new Action(fhirComponents); updateAction.setType(Action.TypeEnum.update); updateAction.setDescription(description); @@ -311,19 +391,25 @@ public static Suggestion createSuggestionWithNote(Card card, return requestWithNoteSuggestion; } + private static Source createSource(CardTypes cardType) { + Source source = new Source(); + source.setLabel("Da Vinci CRD Reference Implementation"); + source.setTopic(cardType.getCoding()); + return source; + } + /** * Creates an error card and adds it to the response if the response that is passed in does not * contain any cards. * + * @param cardType * @param response The response to check and add cards to */ - public static void errorCardIfNonePresent(CdsResponse response) { + public static void errorCardIfNonePresent(CardTypes cardType, CdsResponse response) { if (response.getCards() == null || response.getCards().size() == 0) { Card card = new Card(); card.setIndicator(Card.IndicatorEnum.WARNING); - Source source = new Source(); - source.setLabel("Da Vinci CRD Reference Implementation"); - card.setSource(source); + card.setSource(createSource(cardType)); String msg = "Unable to process hook request from provided information."; card.setSummary(msg); response.addCard(card); @@ -331,12 +417,10 @@ public static void errorCardIfNonePresent(CdsResponse response) { } } - private static Card baseCard() { + private static Card baseCard(CardTypes cardType) { Card card = new Card(); card.setIndicator(Card.IndicatorEnum.INFO); - Source source = new Source(); - source.setLabel("Da Vinci CRD Reference Implementation"); - card.setSource(source); + card.setSource(createSource(cardType)); return card; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/components/PrefetchHydrator.java b/server/src/main/java/org/hl7/davinci/endpoint/components/PrefetchHydrator.java index d72538e9a..8f5c109f0 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/components/PrefetchHydrator.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/components/PrefetchHydrator.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.beanutils.PropertyUtils; @@ -12,16 +11,11 @@ import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsService; +import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirRequestProcessor; import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; public class PrefetchHydrator { @@ -116,10 +110,6 @@ public void hydrate() { // check if the bundle actually has element String prefetchQuery = cdsService.prefetch.get(prefetchKey); String hydratedPrefetchQuery = hydratePrefetchQuery(prefetchQuery); - String token = null; - if (cdsRequest.getFhirAuthorization() != null) { - token = cdsRequest.getFhirAuthorization().getAccessToken(); - } // if we can't hydrate the query, it probably means we didnt get an apprpriate resource // e.g. this could be a query template for a medication order but we have a device request if (hydratedPrefetchQuery != null) { @@ -130,8 +120,8 @@ public void hydrate() { try { PropertyUtils .setProperty(crdResponse, prefetchKey, - prefetchElement.getReturnType().cast( - executeFhirQuery(hydratedPrefetchQuery, token))); + prefetchElement.getReturnType().cast(FhirRequestProcessor.executeFhirQueryUrl( + hydratedPrefetchQuery, cdsRequest, fhirComponents, HttpMethod.GET))); } catch (Exception e) { logger.warn("Failed to fill prefetch for key: " + prefetchKey, e); } @@ -140,36 +130,6 @@ public void hydrate() { } } - private IBaseResource executeFhirQuery(String query, String token) { - String fullUrl = cdsRequest.getFhirServer() + query; - // TODO: Once our provider fhir server is up, switch the fetch to use the hapi client instead - // cdsRequest.getOauth(); - // FhirContext ctx = FhirContext.forR4(); - // BearerTokenAuthInterceptor authInterceptor = new BearerTokenAuthInterceptor(oauth.get("access_token")); - // IGenericClient client = ctx.newRestfulGenericClient(server); - // client.registerInterceptor(authInterceptor); - // return client; - // IGenericClient client = ctx.newRestfulGenericClient(serverBase); - // return client.search().byUrl(query).encodedJson().returnBundle(Bundle.class).execute(); - - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - if (token != null) { - headers.set("Authorization", "Bearer " + token); - } - HttpEntity entity = new HttpEntity<>("", headers); - try { - logger.debug("Fetching: " + fullUrl); - ResponseEntity response = restTemplate.exchange(fullUrl, HttpMethod.GET, - entity, String.class); - return fhirComponents.getJsonParser().parseResource(response.getBody()); - } catch (RestClientException e) { - logger.warn("Unable to make the fetch request", e); - return null; - } - } - private String hydratePrefetchQuery(String prefetchQuery) { String[] tokenList = StringUtils.substringsBetween( prefetchQuery, PREFETCH_TOKEN_DELIM_OPEN, PREFETCH_TOKEN_DELIM_CLOSE); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/components/QueryBatchRequest.java b/server/src/main/java/org/hl7/davinci/endpoint/components/QueryBatchRequest.java new file mode 100644 index 000000000..b3c1b7467 --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/components/QueryBatchRequest.java @@ -0,0 +1,181 @@ +package org.hl7.davinci.endpoint.components; + +import org.cdshooks.CdsRequest; +import org.hl7.davinci.FhirComponentsT; +import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirRequestProcessor; +import org.hl7.davinci.r4.crdhook.CrdPrefetch; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Coverage; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; + +import ca.uhn.fhir.context.FhirContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * A Query Batch Request can be used to populate fields in a CDS Request that a Prefetch may have missed. + */ +public class QueryBatchRequest { + + private static final Logger logger = LoggerFactory.getLogger(QueryBatchRequest.class); + private static final String PRACTIONER_ROLE = "PractitionerRole"; + + private final FhirComponentsT fhirComponents; + + public QueryBatchRequest(FhirComponentsT fhirComponents) { + this.fhirComponents = fhirComponents; + } + + /** + * Backfills the missing required values of the response that prefetch may have missed. + * This implementation pulls the IDs of the required references from the request object's draft + * orders, checks which of those values are missing from the current CRD response, builds the + * Query Batch JSON request using + * http://build.fhir.org/ig/HL7/davinci-crd/hooks.html#fhir-resource-access, + * then populates the CRD response with the response from the Query Batch. + */ + public void performQueryBatchRequest(CdsRequest cdsRequest, CrdPrefetch crdPrefetch) { + logger.info("***** ***** Performing Query Batch Request."); + CrdPrefetch crdResponse = crdPrefetch; + // The list of references that should be queried in the batch request. + List requiredReferences = new ArrayList(); + + // Get the IDs of references in the request's draft orders. + Bundle draftOrdersBundle = cdsRequest.getContext().getDraftOrders(); + // This assumes that only the first draft order is relevant. + Resource initialRequestResource = draftOrdersBundle.getEntry().get(0).getResource(); + ResourceType requestType = initialRequestResource.getResourceType(); + // Extract the references by iterating through the JSON. + Gson gson = new Gson(); + final JsonObject jsonObject = gson.toJsonTree(initialRequestResource).getAsJsonObject(); + for (Map.Entry entry : jsonObject.entrySet()) { + FhirRequestProcessor.extractReferenceIds(requiredReferences, entry.getValue()); + } + + // Filter out references that already exist in the CRD Response. + requiredReferences = requiredReferences.stream() + .filter(referenceId -> !crdResponse.containsRequestResourceId(referenceId)) + .collect(Collectors.toList()); + + logger.info("References to query: " + requiredReferences); + if (requiredReferences.isEmpty()) { + logger.info("A Query Batch Request is not needed: all references have already already fetched."); + return; + } + + // Build the Query Batch Request JSON. + Bundle queryBatchRequestBundle = buildQueryBatchRequestBundle(requiredReferences); + String queryBatchRequestBody = FhirContext.forR4().newJsonParser().encodeResourceToString(queryBatchRequestBundle); + + // Make the query batch request to the EHR server. + Bundle queryResponseBundle = null; + try { + logger.info("Executing Query Batch Request: " + queryBatchRequestBody); + queryResponseBundle = (Bundle) FhirRequestProcessor.executeFhirQueryBody(queryBatchRequestBody, cdsRequest, this.fhirComponents, HttpMethod.POST); + queryResponseBundle = extractNestedBundledResources(queryResponseBundle); + logger.info("Extracted Query Batch Resources: " + + (queryResponseBundle).getEntry().stream().map(entry -> entry.getResource()).collect(Collectors.toList())); + } catch (Exception e) { + logger.error("Failed to backfill prefetch with Query Batch Request " + queryBatchRequestBody, e); + } + + if (queryResponseBundle == null) { + logger.error("No response recieved from the Query Batch Request."); + return; + } + + // Add the request resource to the query batch response as it may be missing. + // Coverage and Subject are not automatically being + // linked to the request object. It seems to somehow automatically link during + // standard prefetch, but not here so we're doing it manually. + List coverages = FhirRequestProcessor.extractCoverageFromBundle(queryResponseBundle); + List patients = FhirRequestProcessor.extractPatientsFromBundle(queryResponseBundle); + FhirRequestProcessor.addInsuranceAndSubject(initialRequestResource, patients, coverages); + BundleEntryComponent newEntry = new BundleEntryComponent(); + newEntry.setResource(initialRequestResource); + queryResponseBundle.addEntry(newEntry); + + // Add the query batch response resources to the CRD Prefetch request. + logger.info("Query Batch Response Entries: " + queryResponseBundle.getEntry()); + FhirRequestProcessor.addToCrdPrefetchRequest(crdResponse, requestType, queryResponseBundle.getEntry()); + logger.info("Post-Query Batch CRDResponse: " + crdResponse); + } + + /** + * Builds a query batch request bundle based on the given references to request. + * + * @param resourceReferences + * @return + */ + private static Bundle buildQueryBatchRequestBundle(List resourceReferences) { + // http://build.fhir.org/ig/HL7/davinci-crd/hooks.html#fhir-resource-access + Bundle queryBatchBundle = new Bundle(); + queryBatchBundle.setType(BundleType.BATCH); + for (String reference : resourceReferences) { + if (reference.contains(PRACTIONER_ROLE)) { + reference = QueryBatchRequest.buildPractionerRoleQuery(reference); + } + BundleEntryComponent entry = new BundleEntryComponent(); + BundleEntryRequestComponent request = new BundleEntryRequestComponent(); + request.setMethod(HTTPVerb.GET); + request.setUrl(reference); + entry.setRequest(request); + queryBatchBundle.addEntry(entry); + } + return queryBatchBundle; + } + + /** + * Adds support for PractitionerRole nested requests. + * + * @param reference + * @return + */ + private static String buildPractionerRoleQuery(String reference) { + String[] referenceIdSplit = reference.split("/"); + String referenceId = referenceIdSplit[referenceIdSplit.length - 1]; + String query = "PractitionerRole?_id=" + referenceId + + "&_include=PractitionerRole:organization&_include=PractitionerRole:practitioner&_include=PractitionerRole:location"; + return query; + } + + /** + * Extracts the resources inside a bundled bundle to be at the top level of the + * bundle, making them no longer nested. + * + * @param resource + * @return + */ + private static Bundle extractNestedBundledResources(Bundle bundle) { + List entriesToAdd = new ArrayList<>(); + List entriesToRemove = new ArrayList<>(); + for (int bundleIndex = 0; bundleIndex < bundle.getEntry().size(); bundleIndex++) { + BundleEntryComponent entry = bundle.getEntry().get(bundleIndex); + if (entry.getResource().getResourceType().equals(ResourceType.Bundle)) { + Bundle bundledBundle = (Bundle) entry.getResource(); + entriesToAdd.addAll(bundledBundle.getEntry()); + entriesToRemove.add(entry); + } + } + bundle.getEntry().addAll(entriesToAdd); + bundle.getEntry().removeAll(entriesToRemove); + return bundle; + } + +} diff --git a/server/src/main/java/org/hl7/davinci/endpoint/config/YamlConfig.java b/server/src/main/java/org/hl7/davinci/endpoint/config/YamlConfig.java index 7116d6122..c31a67e4a 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/config/YamlConfig.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/config/YamlConfig.java @@ -39,6 +39,8 @@ public class YamlConfig { private boolean urlEncodeAppContext; + private boolean queryBatchRequest; + public boolean getCheckJwt() { return checkJwt; } @@ -57,10 +59,18 @@ public void setCheckPractitionerLocation(boolean checkPractitionerLocation) { this.checkPractitionerLocation = checkPractitionerLocation; } + public void setQueryBatchRequest(boolean queryBatchRequest) { + this.queryBatchRequest = queryBatchRequest; + } + public boolean isCheckPractitionerLocation() { return checkPractitionerLocation; } + public boolean isQueryBatchRequest() { + return this.queryBatchRequest; + } + public boolean isAppendParamsToSmartLaunchUrl() { return appendParamsToSmartLaunchUrl; } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java index 7b57cfdf1..dfb5925fb 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java @@ -5,6 +5,7 @@ import org.hl7.davinci.endpoint.Utils; import org.hl7.davinci.endpoint.database.FhirResource; import org.hl7.davinci.endpoint.database.FhirResourceRepository; +import org.hl7.davinci.endpoint.fhir.r4.QuestionnairePackageOperation; import org.hl7.davinci.endpoint.files.FileResource; import org.hl7.davinci.endpoint.files.FileStore; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +21,7 @@ import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; + /** * Provides the REST interface that can be interacted with at [base]/api/fhir and [base]/fhir. */ @@ -116,9 +118,9 @@ public ResponseEntity getFhirResourceById(HttpServletRequest request, * @return * @throws IOException */ - @GetMapping(path = "/fhir/{fhirVersion}/{resource}") //?name={topic} + @GetMapping(path = "/fhir/{fhirVersion}/{resource}") //?name={TopicFormName} public ResponseEntity searchFhirResource(HttpServletRequest request, @PathVariable String fhirVersion, - @PathVariable String resource, @RequestParam(required = false) String name, @RequestParam(required = false) String url) throws IOException { + @PathVariable String resource, @RequestParam(required = false) String name, @RequestParam(required = false) String url, @RequestParam(required = false) String topic) throws IOException { fhirVersion = fhirVersion.toUpperCase(); resource = resource.toLowerCase(); @@ -129,12 +131,17 @@ public ResponseEntity searchFhirResource(HttpServletRequest request, @ if (name != null) { name = name.toLowerCase(); logger.info("GET /fhir/" + fhirVersion + "/" + resource + "?name=" + name); - fileResource = fileStore.getFhirResourceByTopic(fhirVersion, resource, name, baseUrl); + fileResource = fileStore.getFhirResourceByName(fhirVersion, resource, name, baseUrl); } else if (url != null) { logger.info("GET /fhir/" + fhirVersion + "/" + resource + "?url=" + url); fileResource = fileStore.getFhirResourceByUrl(fhirVersion, resource, url, baseUrl); } + else if (topic != null) { + topic = topic.toLowerCase(); + logger.info("GET /fhir/" + fhirVersion + "/" + resource + "?topic=" + topic); + fileResource = fileStore.getFhirResourcesByTopicAsBundle(fhirVersion, resource, topic, baseUrl); + } return processFileResource(fileResource); } @@ -171,7 +178,6 @@ public ResponseEntity submitFhirResource(HttpServletRequest request, Htt logger.warning("unsupported FHIR version: " + fhirVersion + ", not storing"); HttpStatus status = HttpStatus.BAD_REQUEST; MediaType contentType = MediaType.TEXT_PLAIN; - String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; return ResponseEntity.status(status).contentType(contentType) .body("Bad Request"); @@ -181,7 +187,6 @@ public ResponseEntity submitFhirResource(HttpServletRequest request, Htt logger.warning("submitFhirResource: unsupported FHIR Resource of type: " + fhirResourceInfo.getType() + " (" + fhirVersion + "), not storing"); HttpStatus status = HttpStatus.BAD_REQUEST; MediaType contentType = MediaType.TEXT_PLAIN; - String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; return ResponseEntity.status(status).contentType(contentType) .body("Bad Request"); @@ -213,4 +218,46 @@ public ResponseEntity submitFhirResource(HttpServletRequest request, Htt .header(HttpHeaders.LOCATION, baseUrl + "/fhir/" + fhirVersion + "/" + resource + "?identifier=" + newId) .body(statusMsg); } + + /** + * FHIR Operation to retrieve all of the Questionnaires and CQL files associated with a given request. + * @param fhirVersion (converted to uppercase) + * @return FHIR Bundle + * @throws IOException + */ + @PostMapping(path = "/fhir/{fhirVersion}/Questionnaire/$questionnaire-package", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) + public ResponseEntity questionnaireForOrderOperation(HttpServletRequest request, HttpEntity entity, + @PathVariable String fhirVersion) { + + fhirVersion = fhirVersion.toUpperCase(); + String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; + + logger.info("POST /fhir/" + fhirVersion + "/Questionnaire/$Questionnaire-package"); + + String resource = null; + if (fhirVersion.equalsIgnoreCase("R4")) { + QuestionnairePackageOperation operation = new QuestionnairePackageOperation(fileStore, baseUrl); + resource = operation.execute(entity.getBody()); + + if (resource == null) { + logger.warning("bad parameters"); + HttpStatus status = HttpStatus.BAD_REQUEST; + MediaType contentType = MediaType.TEXT_PLAIN; + + return ResponseEntity.status(status).contentType(contentType) + .body("Bad Parameters"); + } + + } else { + logger.warning("unsupported FHIR version: " + fhirVersion + ", not storing"); + HttpStatus status = HttpStatus.BAD_REQUEST; + MediaType contentType = MediaType.TEXT_PLAIN; + + return ResponseEntity.status(status).contentType(contentType) + .body("Bad Request"); + } + + return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(resource); + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java index 08f8e50af..b70442cef 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java @@ -7,6 +7,7 @@ public class FhirResourceCriteria { private String name = null; private String id = null; private String url = null; + private String topic = null; public String getFhirVersion() { return fhirVersion; } @@ -43,6 +44,13 @@ public FhirResourceCriteria setUrl(String url) { return this; } + public String getTopic() { return topic; } + + public FhirResourceCriteria setTopic(String topic) { + this.topic = topic; + return this; + } + public String toString() { String string = new String(); boolean first = true; @@ -90,6 +98,15 @@ public String toString() { } string = string + "url=" + url; } + + if (topic != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "topic=" + topic; + } return string; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java index 5d01a8a0f..88563ab61 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java @@ -27,7 +27,7 @@ List findById( "SELECT r FROM FhirResource r WHERE " + "r.fhirVersion = :#{#criteria.fhirVersion} " + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " - + "and r.name = :#{#criteria.name}") + + "and LOWER(r.name) = :#{#criteria.name}") List findByName( @Param("criteria") FhirResourceCriteria criteria ); @@ -41,6 +41,15 @@ List findByUrl( @Param("criteria") FhirResourceCriteria criteria ); + @Query( + "SELECT r FROM FhirResource r WHERE " + + "r.fhirVersion = :#{#criteria.fhirVersion} " + + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " + + "and LOWER(r.topic) = :#{#criteria.topic}") + List findByTopic( + @Param("criteria") FhirResourceCriteria criteria + ); + @Query( "SELECT r FROM FhirResource r " + "order by r.topic, r.id") diff --git a/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/Metadata.java b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/Metadata.java index c3186fe73..3043354f3 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/Metadata.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/Metadata.java @@ -86,9 +86,14 @@ private CapabilityStatement buildCapabilityStatement(String baseUrl) { questionnaire.addInteraction().setCode(TypeRestfulInteraction.READ); questionnaire.addInteraction().setCode(TypeRestfulInteraction.SEARCHTYPE); questionnaire.addInteraction().setCode(TypeRestfulInteraction.CREATE); + CapabilityStatementRestResourceOperationComponent questionnairePackageOperation = new CapabilityStatementRestResourceOperationComponent(); + questionnairePackageOperation.setName("questionnaire-package"); + questionnairePackageOperation.setDefinition("http://hl7.org/fhir/us/davinci-dtr/OperationDefinition/Questionnaire-package"); + questionnairePackageOperation.setDocumentation("Retrieve the Questionnaire(s), Libraries, and Valuesets for a given order and coverage. This operation is to support HL7 DaVinci DTR."); + questionnaire.addOperation(questionnairePackageOperation); rest.addResource(questionnaire); - // QuestionnaireResponse REsource + // QuestionnaireResponse Resource CapabilityStatementRestResourceComponent questionnaireResponse = new CapabilityStatementRestResourceComponent(); questionnaireResponse.setType("QuestionnaireResponse"); questionnaireResponse.addInteraction().setCode(TypeRestfulInteraction.READ); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java new file mode 100644 index 000000000..5f0e432e9 --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java @@ -0,0 +1,250 @@ +package org.hl7.davinci.endpoint.fhir.r4; + +import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirBundleProcessor; +import org.hl7.davinci.endpoint.files.FileStore; +import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.Coverage; +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.Parameters; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.parser.DataFormatException; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +//TODO: handle operation being passed one or more canonicals specifying the URL and, optionally, the version of the Questionnaire(s) to retrieve + +public class QuestionnairePackageOperation { + + static final Logger logger = LoggerFactory.getLogger(QuestionnairePackageOperation.class); + + FileStore fileStore; + String baseUrl; + + // map of Resources and ids/urls so that we can skip retrieving duplicates + HashMap resources = new HashMap<>(); + + public QuestionnairePackageOperation(FileStore fileStore, String baseUrl) { + this.fileStore = fileStore; + this.baseUrl = baseUrl; + } + + /* + * Do the work retrieving all of the Questionnaire, Library and Valueset Resources. + */ + public String execute(String resourceString) { + Parameters outputParameters = new Parameters(); + IBaseResource resource = null; + + try { + resource = org.hl7.davinci.r4.Utilities.parseFhirData(resourceString); + } catch (DataFormatException exception) { + logger.error("Failed to process input parameters: " + exception.getMessage()); + return null; + } + + if (resource.fhirType().equalsIgnoreCase("Parameters")) { + Parameters parameters = (Parameters)resource; + + //TODO: handle multiple FHIR Coverage Resources + Coverage coverage = (Coverage) getResource(parameters, "coverage"); + + // list of all of the orders + Bundle orders = getAllResources(parameters, "order"); + + if (coverage == null || orders.isEmpty()) { + logger.error("Failed to find order or coverage within parameters"); + return null; + } + + // create a single new bundle for all of the resources + Bundle completeBundle = new Bundle(); + + // list of items in bundle to avoid duplicates + List bundleContents = new ArrayList<>(); + + // process the orders to find the topics + FhirBundleProcessor fhirBundleProcessor = new FhirBundleProcessor(fileStore, baseUrl); + fhirBundleProcessor.processDeviceRequests(orders); + fhirBundleProcessor.processMedicationRequests(orders); + fhirBundleProcessor.processServiceRequests(orders); + fhirBundleProcessor.processMedicationDispenses(orders); + List topics = createTopicList(fhirBundleProcessor); + + for (String topic : topics) { + logger.info("--> process topic: " + topic); + + // get all of the Quesionnaires for the topic + Bundle bundle = fileStore.getFhirResourcesByTopicAsFhirBundle("R4", "Questionnaire", topic.toLowerCase(), baseUrl); + List bundleEntries = bundle.getEntry(); + for (BundleEntryComponent entry : bundleEntries) { + + addResourceToBundle(entry.getResource(), bundleContents, completeBundle); + + if (entry.getResource().fhirType().equalsIgnoreCase("Questionnaire")) { + Questionnaire questionnaire = (Questionnaire)entry.getResource(); + + List extensions = questionnaire.getExtension(); + for (Extension extension : extensions) { + if (extension.getUrl().endsWith("cqf-library")) { + CanonicalType data = (CanonicalType)extension.getValue(); + String url = data.asStringValue(); + Resource libraryResource = null; + + // look in the map and retrieve it instead of looking it up on disk if found + if (resources.containsKey(url)) { + libraryResource = resources.get(url); + } else { + libraryResource = fileStore.getFhirResourceByUrlAsFhirResource("R4", "Library", url, baseUrl); + resources.put(url, libraryResource); + } + + if (addResourceToBundle(libraryResource, bundleContents, completeBundle)) { + // recursively add the depends-on libraries if added to bundle + addLibraryDependencies((Library)libraryResource, bundleContents, completeBundle); + } + } + } + } + } // Questionnaires + } // topics + + // add the bundle to the output parameters if it contains any resources + if (!completeBundle.isEmpty()) { + ParametersParameterComponent parameter = new ParametersParameterComponent(); + parameter.setName("return"); + parameter.setResource(completeBundle); + outputParameters.addParameter(parameter); + } else { + logger.info("No matching Questionnaires found"); + } + } + + // if none found return null + if (outputParameters.isEmpty()) { + return null; + } + + // convert the outputParameters to a string + FhirContext ctx = new org.hl7.davinci.r4.FhirComponents().getFhirContext(); + IParser parser = ctx.newJsonParser().setPrettyPrint(true); + return parser.encodeResourceToString(outputParameters); + } + + private Resource getResource(Parameters parameters, String name) { + for (ParametersParameterComponent parameter : parameters.getParameter()) { + if (parameter.getName().equals(name)) + return parameter.getResource(); + } + return null; + } + + private Bundle getAllResources(Parameters parameters, String name) { + Bundle foundResources = new Bundle(); + for (ParametersParameterComponent parameter : parameters.getParameter()) { + if (parameter.getName().equals(name)) + foundResources.addEntry(new BundleEntryComponent().setResource(parameter.getResource())); + } + + return foundResources; + } + + private boolean addResourceToBundle(Resource resource, List bundleContents, Bundle questionnaireBundle) { + // only add the library if not already in the bundle + boolean added = false; + if (!bundleContents.contains(resource.getId())) { + // add the questionnaire to the bundle + BundleEntryComponent questionnaireBundleEntry = new BundleEntryComponent(); + questionnaireBundleEntry.setResource(resource); + questionnaireBundle.addEntry(questionnaireBundleEntry); + bundleContents.add(resource.getId()); + logger.info(" --> add " + resource.fhirType() + ": " + resource.getId()); + added = true; + } + return added; + } + + private List createTopicList(FhirBundleProcessor fhirBundleProcessor) { + List topics = new ArrayList<>(); + List results = fhirBundleProcessor.getResults(); + for (CoverageRequirementRuleResult result : results) { + // add topic to the list if not already contained + if (!topics.contains(result.getTopic())) { + topics.add(result.getTopic()); + } + } + return topics; + } + + /* + * Recursively add all of the libraries dependencies related by the "depends-on" type. + */ + private void addLibraryDependencies(Library library, List bundleContents, Bundle questionnaireBundle) { + List relatedArtifacts = library.getRelatedArtifact(); + for (RelatedArtifact relatedArtifact : relatedArtifacts) { + // only add the depends-on artifacts + if (relatedArtifact.getType() == RelatedArtifactType.DEPENDSON) { + String relatedArtifactReferenceString = relatedArtifact.getResource(); + String[] referenceParts = relatedArtifactReferenceString.split("/"); + String id = referenceParts[1]; + Resource referencedLibraryResource = null; + + // look in the map and retrieve it instead of looking it up on disk if found + if (resources.containsKey(id)) { + referencedLibraryResource = resources.get(id); + } else { + referencedLibraryResource = fileStore.getFhirResourceByIdAsFhirResource("R4", "Library", id, baseUrl); + resources.put(id, referencedLibraryResource); + } + + // only add the library if not already in the bundle + if (!bundleContents.contains(referencedLibraryResource.getId())) { + BundleEntryComponent referencedLibraryBundleEntry = new BundleEntryComponent(); + referencedLibraryBundleEntry.setResource(referencedLibraryResource); + questionnaireBundle.addEntry(referencedLibraryBundleEntry); + bundleContents.add(referencedLibraryResource.getId()); + + // recurse through the libraries... + addLibraryDependencies((Library)referencedLibraryResource, bundleContents, questionnaireBundle); + } + } + } + + // grab all of the ValueSets in the DataRequirement + List dataRequirements = library.getDataRequirement(); + for (DataRequirement dataRequirement : dataRequirements) { + List codeFilters = dataRequirement.getCodeFilter(); + for (DataRequirement.DataRequirementCodeFilterComponent codeFilter : codeFilters) { + String valueSetUrl = codeFilter.getValueSet(); + Resource valueSetResource = null; + + // look in the map and retrieve it instead of looking it up on disk if found + if (resources.containsKey(valueSetUrl)) { + valueSetResource = resources.get(valueSetUrl); + } else { + valueSetResource = fileStore.getFhirResourceByUrlAsFhirResource("R4", "ValueSet", valueSetUrl, baseUrl); + resources.put(valueSetUrl, valueSetResource); + } + + addResourceToBundle(valueSetResource, bundleContents, questionnaireBundle); + } + } + } +} diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index 8615c8fc5..83aca77f9 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -12,17 +12,24 @@ import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; import org.hl7.davinci.endpoint.vsac.ValueSetCache; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; +import java.util.ArrayList; import java.util.Locale; +import javax.annotation.processing.Filer; + public abstract class CommonFileStore implements FileStore { static final Logger logger = LoggerFactory.getLogger(CommonFileStore.class); @@ -67,47 +74,66 @@ public CommonFileStore() { protected abstract String readFhirResourceFromFile(FhirResource fhirResource, String fhirVersion); - protected FileResource readFhirResourceFromFiles(List fhirResourceList, String fhirVersion, + protected FileResource readFhirResourceFromFiles(FhirResource fhirResource, String fhirVersion, String baseUrl) { - if (!fhirResourceList.isEmpty()) { - // just return the first matched resource - FhirResource fhirResource = fhirResourceList.get(0); - String fileString = null; + String fileString = null; - // grab the data from the database directly if it is there - String data = fhirResource.getData(); - if (data != null) { - fileString = data; - } else { - fileString = readFhirResourceFromFile(fhirResource, fhirVersion); - } + // grab the data from the database directly if it is there + String data = fhirResource.getData(); + if (data != null) { + fileString = data; + } else { + fileString = readFhirResourceFromFile(fhirResource, fhirVersion); + } - if (fileString != null) { - String partialUrl = baseUrl + "fhir/" + fhirVersion + "/"; + if (fileString != null) { + String partialUrl = baseUrl + "fhir/" + fhirVersion + "/"; - // replace with the proper path - fileString = fileString.replace("", partialUrl); - byte[] fileData = fileString.getBytes(Charset.defaultCharset()); + // replace with the proper path + fileString = fileString.replace("", partialUrl); + byte[] fileData = fileString.getBytes(Charset.defaultCharset()); - FileResource fileResource = new FileResource(); - fileResource.setFilename(fhirResource.getFilename()); - fileResource.setResource(new ByteArrayResource(fileData)); - return fileResource; - } else { - logger.warn("CommonFhirStore::readFhirResourceFromFiles() empty fileString"); - return null; - } + FileResource fileResource = new FileResource(); + fileResource.setFilename(fhirResource.getFilename()); + fileResource.setResource(new ByteArrayResource(fileData)); + return fileResource; + } else { + logger.warn("CommonFhirStore::readFhirResourceFromFiles() empty fileString"); + return null; + } + } + protected FileResource readFhirResourceFromFiles(List fhirResourceList, String fhirVersion, + String baseUrl) { + if (!fhirResourceList.isEmpty()) { + // just return the first matched resource + FhirResource fhirResource = fhirResourceList.get(0); + return readFhirResourceFromFiles(fhirResource, fhirVersion, baseUrl); } else { logger.warn("CommonFileStore::readFhirResourceFromFiles() empty file resource list"); return null; } } - public FileResource getFhirResourceByTopic(String fhirVersion, String resourceType, String name, String baseUrl) { + protected List readFhirResourcesFromFiles(List fhirResourceList, String fhirVersion, + String baseUrl) { + List fileResources = new ArrayList<>(); + + // read all of the resources from the list from the file store + for (FhirResource fhirResource : fhirResourceList) { + FileResource fileResource = readFhirResourceFromFiles(fhirResource, fhirVersion, baseUrl); + if (fileResource != null) { + fileResources.add(fileResource); + } + } + + return fileResources; + } + + public FileResource getFhirResourceByName(String fhirVersion, String resourceType, String name, String baseUrl) { FhirResourceCriteria criteria = new FhirResourceCriteria(); criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setName(name); - logger.info("CommonFileStore::getFhirResourceByTopic(): " + criteria.toString()); + logger.info("CommonFileStore::getFhirResourceByName(): " + criteria.toString()); List fhirResourceList = fhirResources.findByName(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); @@ -181,6 +207,106 @@ public FileResource getFhirResourceByUrl(String fhirVersion, String resourceType return resource; } + + private Resource convertFileResourceToFhirResource(FileResource fileResource) { + if (fileResource == null) { + return null; + } else { + Resource resource = null; + try { + // convert the file resource into the fhir resource + resource = (Resource) parser.parseResource(fileResource.getResource().getInputStream()); + } catch(IOException ioe) { + logger.error("Issue parsing FHIR file resource when retrieving by URL.", ioe); + } + return resource; + } + } + + public Resource getFhirResourceByIdAsFhirResource(String fhirVersion, String resourceType, String id, String baseUrl) { + FileResource fileResource = getFhirResourceById(fhirVersion, resourceType, id, baseUrl); + return convertFileResourceToFhirResource(fileResource); + } + + public Resource getFhirResourceByUrlAsFhirResource(String fhirVersion, String resourceType, String url, String baseUrl) { + FileResource fileResource = getFhirResourceByUrl(fhirVersion, resourceType, url, baseUrl); + return convertFileResourceToFhirResource(fileResource); + } + + public List getFhirResourcesByTopic(String fhirVersion, String resourceType, String topic, String baseUrl) { + FhirResourceCriteria criteria = new FhirResourceCriteria(); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setTopic(topic); + logger.info("CommonFileStore::getFhirResourcesByTopic(): " + criteria.toString()); + + List fhirResourceList = fhirResources.findByTopic(criteria); + List resources = readFhirResourcesFromFiles(fhirResourceList, fhirVersion, baseUrl); + List outputResources = new ArrayList<>(); + + if (!resources.isEmpty()) { + for (FileResource resource : resources) { + if ((resource != null) && fhirVersion.equalsIgnoreCase("r4")) { + FileResource processedResource = resource; + + if (resourceType.equalsIgnoreCase("Questionnaire")) { + // If this is a questionnaire, run it through the processor to modify it before returning. + // We do not handle nested sub-questionnaire at this time. + processedResource = this.subQuestionnaireProcessor.processResource(resource, this, baseUrl); + processedResource = this.questionnaireValueSetProcessor.processResource(processedResource, this, baseUrl); + processedResource = this.questionnaireEmbeddedCQLProcessor.processResource(processedResource, null, null); + + } else if (resourceType.equalsIgnoreCase("Library")) { + // If this is a library, process it by replacing the content url with a base64 + // encoded version of the cql + // When requested via topic, do this even if flag is not set in config (embedCqlInLibrary) + processedResource = this.libraryContentProcessor.processResource(resource, this, baseUrl); + } + + // add the resource to the output list + outputResources.add(processedResource); + } + + } + } + + return outputResources; + } + + public Bundle getFhirResourcesByTopicAsFhirBundle(String fhirVersion, String resourceType, String topic, String baseUrl) { + List fileResources = getFhirResourcesByTopic(fhirVersion, resourceType, topic, baseUrl); + Bundle bundle = new Bundle(); + if (fileResources != null && !fileResources.isEmpty()) { + for (FileResource fileResource : fileResources) { + if (fileResource != null) { + Resource resource = null; + try { + // convert the file resource into the fhir resource + resource = (Resource) parser.parseResource(fileResource.getResource().getInputStream()); + BundleEntryComponent entry = new BundleEntryComponent().setResource(resource); + bundle.addEntry(entry); + } catch(IOException ioe) { + logger.error("Issue parsing FHIR file resource for preprocessing.", ioe); + return null; + } + } + } + } + + return bundle; + } + + public FileResource getFhirResourcesByTopicAsBundle(String fhirVersion, String resourceType, String topic, String baseUrl) { + Bundle bundle = getFhirResourcesByTopicAsFhirBundle(fhirVersion, resourceType, topic, baseUrl); + if (bundle.isEmpty()) { + return null; + } + + byte[] resourceData = parser.encodeResourceToString(bundle).getBytes(Charset.defaultCharset()); + FileResource outputFileResource = new FileResource(); + outputFileResource.setResource(new ByteArrayResource(resourceData)); + outputFileResource.setFilename("bundle.json"); + return outputFileResource; + } + // from RuleFinder public List findRules(CoverageRequirementRuleCriteria criteria) { logger.info("CommonFileStore::findRules(): " + criteria.toString()); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/FileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/FileStore.java index 56090c641..dc1c5e6b9 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/FileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/FileStore.java @@ -4,6 +4,8 @@ import org.hl7.davinci.endpoint.cql.CqlRule; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; import org.hl7.davinci.endpoint.database.RuleMapping; import org.hl7.davinci.endpoint.database.FhirResource; @@ -21,10 +23,18 @@ public interface FileStore { FileResource getFile(String topic, String fileName, String fhirVersion, boolean convert); - FileResource getFhirResourceByTopic(String fhirVersion, String resourceType, String name, String baseUrl); + // Get FHIR Resources as FileResource + FileResource getFhirResourceByName(String fhirVersion, String resourceType, String name, String baseUrl); FileResource getFhirResourceById(String fhirVersion, String resourceType, String id, String baseUrl); FileResource getFhirResourceById(String fhirVersion, String resourceType, String id, String baseUrl, boolean isRoot); FileResource getFhirResourceByUrl(String fhirVersion, String resourceType, String url, String baseUrl); + List getFhirResourcesByTopic(String fhirVersion, String resourceType, String topic, String baseUrl); + FileResource getFhirResourcesByTopicAsBundle(String fhirVersion, String resourceType, String topic, String baseUrl); + + // Get FHIR Resources as FHIR Resources + Resource getFhirResourceByIdAsFhirResource(String fhirVersion, String resourceType, String id, String baseUrl); + Resource getFhirResourceByUrlAsFhirResource(String fhirVersion, String resourceType, String url, String baseUrl); + Bundle getFhirResourcesByTopicAsFhirBundle(String fhirVersion, String resourceType, String topic, String baseUrl); // from RuleFinder List findRules(CoverageRequirementRuleCriteria criteria); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/LibraryContentProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/files/LibraryContentProcessor.java index c19108958..233995f26 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/LibraryContentProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/LibraryContentProcessor.java @@ -6,8 +6,9 @@ import org.hl7.fhir.r4.model.Library; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - + import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -48,7 +49,7 @@ protected Library processResource(Library inputResource, FileStore fileStore, St // content is CQL (assuming elm/json is actually CQL) String topic = urlParts[1]; - String fhirVersion = urlParts[2]; + String fhirVersion = urlParts[2].toUpperCase(); String fileName = urlParts[3]; List attachments = new ArrayList<>(); @@ -87,8 +88,9 @@ private Attachment base64EncodeToAttachment(FileResource fileResource, String mi Attachment attachment = new Attachment(); try { // base64 encode - byte[] byteData = new byte[(int) fileResource.getResource().contentLength()]; - fileResource.getResource().getInputStream().read(byteData, 0, byteData.length); + InputStream inputStream = fileResource.getResource().getInputStream(); + byte[] byteData = new byte[inputStream.available()]; + inputStream.read(byteData); String encodedData = Base64.encodeBase64String(byteData); attachment.setContentType(mimeType); Base64BinaryType b64bType = new Base64BinaryType(); @@ -100,3 +102,5 @@ private Attachment base64EncodeToAttachment(FileResource fileResource, String mi return attachment; } } + + diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/github/GitHubFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/github/GitHubFileStore.java index 74812fd7e..965868250 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/github/GitHubFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/github/GitHubFileStore.java @@ -316,6 +316,7 @@ public CqlRule getCqlRule(String topic, String fhirVersion) { public FileResource getFile(String topic, String fileName, String fhirVersion, boolean convert) { FileResource fileResource = new FileResource(); + fhirVersion = fhirVersion.toUpperCase(); fileResource.setFilename(fileName); String rulePath = config.getGitHubConfig().getRulePath(); diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index f4e8084e2..35aa7a934 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -50,7 +50,7 @@ urlEncodeAppContext: true # When this flag is true, the server will modify a FHIR Library when retrieved. # It will automatically retrieve, base64 encode, and embed the referenced (from url) # CQL into the data field of the content. -embedCqlInLibrary: false +embedCqlInLibrary: true cdsConnect: url: https://cdsconnect.ahrqstg.org @@ -77,3 +77,5 @@ valueSetCachePath: ValueSetCache/ hostOrg: default +# Configure whether Query Batch Requests will be used to backfill potentially missing prefetch resources. +queryBatchRequest: true diff --git a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java index 726b2d35e..5fd37bfcb 100644 --- a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java +++ b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java @@ -7,6 +7,7 @@ import org.hl7.davinci.endpoint.components.CardBuilder; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.cdshooks.Card; +import org.hl7.davinci.r4.CardTypes; import org.junit.jupiter.api.Test; public class CardBuilderTest { @@ -20,7 +21,7 @@ public void testRulesWithNoAuthNeeded() { coverageRequirements.setInfoLink("http://some.link"); coverageRequirements.setSummary("The summary!"); cardResults.setCoverageRequirements(coverageRequirements); - Card card = CardBuilder.transform(cardResults); + Card card = CardBuilder.transform(CardTypes.COVERAGE, cardResults); assertEquals("The summary!", card.getSummary()); assertEquals("Some details.", card.getDetail()); assertEquals("http://some.link", card.getLinks().get(0).getUrl()); diff --git a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/EndToEndRequestPrefetchTest.java b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/EndToEndRequestPrefetchTest.java index 8cd3a629c..74c0f31fb 100644 --- a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/EndToEndRequestPrefetchTest.java +++ b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/EndToEndRequestPrefetchTest.java @@ -3,7 +3,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,7 +12,7 @@ import java.nio.charset.Charset; import org.apache.commons.io.FileUtils; import org.hl7.davinci.endpoint.Application; -import org.junit.Ignore; +import org.hl7.davinci.endpoint.config.YamlConfig; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,7 +27,6 @@ import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; - @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) public class EndToEndRequestPrefetchTest { @@ -50,6 +48,8 @@ public class EndToEndRequestPrefetchTest { private int port; @Autowired private TestRestTemplate restTemplate; + @Autowired + private YamlConfig myConfig; public EndToEndRequestPrefetchTest() throws IOException { } @@ -68,6 +68,8 @@ public void shouldRunSuccessfully() { @Test public void shouldSuccessfullyFillPreFetch() { + // Disable Query Batch Request since it relies on a Fhir Server or mock class. + myConfig.setQueryBatchRequest(false); stubFor(get(urlMatching(prefetchUrlMatcher)) .willReturn(aResponse() .withStatus(200) @@ -88,10 +90,11 @@ public void shouldSuccessfullyFillPreFetch() { @Test public void shouldFailToFillPrefetch() { + // Disable Query Batch Request since it relies on a Fhir Server or mock class. + myConfig.setQueryBatchRequest(false); stubFor(get(urlMatching(prefetchUrlMatcher)) .willReturn(aResponse() .withStatus(404))); - HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity(deviceRequestEmptyPrefetchJson, headers);