ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrServiceR5 Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsConfigService;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceIndicatorEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestAuthorizationJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardSourceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseLinkJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSuggestionActionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSuggestionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSystemActionJson;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.ParameterDefinition;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.PlanDefinition;
import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.r5.model.RelatedArtifact;
import org.hl7.fhir.r5.model.RequestOrchestration;
import org.hl7.fhir.r5.model.Resource;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.Canonicals;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_DATA;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_ENCOUNTER;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_PARAMETERS;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_PRACTITIONER;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_SUBJECT;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_PARAMETER_DRAFT_ORDERS;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_PARAMETER_ENCOUNTER_ID;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_PARAMETER_PATIENT_ID;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_PARAMETER_USER_ID;
import static org.opencds.cqf.fhir.utility.r5.Parameters.parameters;
import static org.opencds.cqf.fhir.utility.r5.Parameters.part;
public class CdsCrServiceR5 implements ICdsCrService {
protected final RequestDetails myRequestDetails;
protected final Repository myRepository;
protected final ICdsConfigService myCdsConfigService;
protected Bundle myResponseBundle;
protected CdsServiceResponseJson myServiceResponse;
public CdsCrServiceR5(
RequestDetails theRequestDetails, Repository theRepository, ICdsConfigService theCdsConfigService) {
myCdsConfigService = theCdsConfigService;
myRequestDetails = theRequestDetails;
myRepository = theRepository;
}
public FhirVersionEnum getFhirVersion() {
return FhirVersionEnum.R5;
}
public Repository getRepository() {
return myRepository;
}
public Parameters encodeParams(CdsServiceRequestJson theJson) {
Parameters parameters = parameters()
.addParameter(part(APPLY_PARAMETER_SUBJECT, theJson.getContext().getString(CDS_PARAMETER_PATIENT_ID)));
if (theJson.getContext().containsKey(CDS_PARAMETER_USER_ID)) {
parameters.addParameter(
part(APPLY_PARAMETER_PRACTITIONER, theJson.getContext().getString(CDS_PARAMETER_USER_ID)));
}
if (theJson.getContext().containsKey(CDS_PARAMETER_ENCOUNTER_ID)) {
parameters.addParameter(
part(APPLY_PARAMETER_ENCOUNTER, theJson.getContext().getString(CDS_PARAMETER_ENCOUNTER_ID)));
}
var cqlParameters = parameters();
if (theJson.getContext().containsKey(CDS_PARAMETER_DRAFT_ORDERS)) {
addCqlParameters(
cqlParameters,
theJson.getContext().getResource(CDS_PARAMETER_DRAFT_ORDERS),
CDS_PARAMETER_DRAFT_ORDERS);
}
if (cqlParameters.hasParameter()) {
parameters.addParameter(part(APPLY_PARAMETER_PARAMETERS, cqlParameters));
}
Bundle data = getPrefetchResources(theJson);
if (data.hasEntry()) {
parameters.addParameter(part(APPLY_PARAMETER_DATA, data));
}
return parameters;
}
protected String getTokenType(CdsServiceRequestAuthorizationJson theJson) {
String tokenType = theJson.getTokenType();
return tokenType == null || tokenType.isEmpty() ? "Bearer" : tokenType;
}
protected Parameters addCqlParameters(
Parameters theParameters, IBaseResource theContextResource, String theParamName) {
// We are making the assumption that a Library created for a hook will provide parameters for the fields
// specified for the hook
if (theContextResource instanceof Bundle) {
((Bundle) theContextResource)
.getEntry()
.forEach(x -> theParameters.addParameter(part(theParamName, x.getResource())));
} else {
theParameters.addParameter(part(theParamName, (Resource) theContextResource));
}
if (theParameters.getParameter().size() == 1) {
Extension listExtension = new Extension(
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-parameterDefinition",
new ParameterDefinition()
.setMax("*")
.setName(theParameters.getParameterFirstRep().getName()));
theParameters.getParameterFirstRep().addExtension(listExtension);
}
return theParameters;
}
protected Map getResourcesFromBundle(Bundle theBundle) {
// using HashMap to avoid duplicates
Map resourceMap = new HashMap<>();
theBundle
.getEntry()
.forEach(x -> resourceMap.put(x.fhirType() + x.getResource().getId(), x.getResource()));
return resourceMap;
}
protected Bundle getPrefetchResources(CdsServiceRequestJson theJson) {
// using HashMap to avoid duplicates
Map resourceMap = new HashMap<>();
Bundle prefetchResources = new Bundle();
Resource resource;
for (String key : theJson.getPrefetchKeys()) {
resource = (Resource) theJson.getPrefetch(key);
if (resource == null) {
continue;
}
if (resource instanceof Bundle) {
resourceMap.putAll(getResourcesFromBundle((Bundle) resource));
} else {
resourceMap.put(resource.fhirType() + resource.getId(), resource);
}
}
resourceMap.forEach((key, value) -> prefetchResources.addEntry().setResource(value));
return prefetchResources;
}
public CdsServiceResponseJson encodeResponse(Object theResponse) {
assert theResponse instanceof Bundle;
myResponseBundle = (Bundle) theResponse;
CdsServiceResponseJson serviceResponse = new CdsServiceResponseJson();
if (myResponseBundle.hasEntry()) {
RequestOrchestration mainRequest =
(RequestOrchestration) myResponseBundle.getEntry().get(0).getResource();
CanonicalType canonical = mainRequest.getInstantiatesCanonical().get(0);
PlanDefinition planDef = myRepository.read(
PlanDefinition.class,
new IdType(Canonicals.getResourceType(canonical), Canonicals.getIdPart(canonical)));
List links = resolvePlanLinks(planDef);
mainRequest.getAction().forEach(action -> serviceResponse.addCard(resolveAction(action, links)));
}
return serviceResponse;
}
protected List resolvePlanLinks(PlanDefinition thePlanDefinition) {
List links = new ArrayList<>();
// links - listed on each card
if (thePlanDefinition.hasRelatedArtifact()) {
thePlanDefinition.getRelatedArtifact().forEach(ra -> {
String linkUrl = ra.getDocument().getUrl();
if (linkUrl != null) {
CdsServiceResponseLinkJson link = new CdsServiceResponseLinkJson().setUrl(linkUrl);
if (ra.hasDisplay()) {
link.setLabel(ra.getDisplay());
}
if (ra.hasExtension()) {
link.setType(ra.getExtensionFirstRep().getValue().primitiveValue());
} else link.setType("absolute"); // default
links.add(link);
}
});
}
return links;
}
protected CdsServiceResponseCardJson resolveAction(
RequestOrchestration.RequestOrchestrationActionComponent theAction,
List theLinks) {
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson()
.setSummary(theAction.getTitle())
.setDetail(theAction.getDescription())
.setLinks(theLinks);
if (theAction.hasPriority()) {
card.setIndicator(resolveIndicator(theAction.getPriority().toCode()));
}
if (theAction.hasDocumentation()) {
card.setSource(resolveSource(theAction));
}
if (theAction.hasSelectionBehavior()) {
card.setSelectionBehaviour(theAction.getSelectionBehavior().toCode());
theAction.getAction().forEach(action -> resolveSuggestion(action));
}
// Leaving this out until the spec details how to map system actions.
// if (theAction.hasType() && theAction.hasResource()) {
// resolveSystemAction(theAction);
// }
return card;
}
protected CdsServiceIndicatorEnum resolveIndicator(String theCode) {
CdsServiceIndicatorEnum indicator;
switch (theCode) {
case "routine":
indicator = CdsServiceIndicatorEnum.INFO;
break;
case "urgent":
indicator = CdsServiceIndicatorEnum.WARNING;
break;
case "stat":
indicator = CdsServiceIndicatorEnum.CRITICAL;
break;
default:
indicator = null;
break;
}
if (indicator == null) {
// Code 2435-2440 are reserved for this error message across versions
throw new IllegalArgumentException(Msg.code(2436) + "Invalid priority code: " + theCode);
}
return indicator;
}
protected void resolveSystemAction(RequestOrchestration.RequestOrchestrationActionComponent theAction) {
if (theAction.hasType()
&& theAction.getType().hasCoding()
&& theAction.getType().getCodingFirstRep().hasCode()
&& !theAction.getType().getCodingFirstRep().getCode().equals("fire-event")) {
myServiceResponse.addServiceAction(new CdsServiceResponseSystemActionJson()
.setResource(resolveResource(theAction.getResource()))
.setType(theAction.getType().getCodingFirstRep().getCode()));
}
}
protected CdsServiceResponseCardSourceJson resolveSource(
RequestOrchestration.RequestOrchestrationActionComponent theAction) {
RelatedArtifact documentation = theAction.getDocumentationFirstRep();
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson()
.setLabel(documentation.getDisplay())
.setUrl(documentation.getDocument().getUrl());
// If we use the document for the url, what do we use for the icon?
// if (documentation.hasDocument() && documentation.getDocument().hasUrl()) {
// source.setIcon(documentation.getDocument().getUrl());
// }
return source;
}
protected CdsServiceResponseSuggestionJson resolveSuggestion(
RequestOrchestration.RequestOrchestrationActionComponent theAction) {
CdsServiceResponseSuggestionJson suggestion = new CdsServiceResponseSuggestionJson()
.setLabel(theAction.getTitle())
.setUuid(theAction.getId());
theAction.getAction().forEach(action -> suggestion.addAction(resolveSuggestionAction(action)));
return suggestion;
}
protected CdsServiceResponseSuggestionActionJson resolveSuggestionAction(
RequestOrchestration.RequestOrchestrationActionComponent theAction) {
CdsServiceResponseSuggestionActionJson suggestionAction =
new CdsServiceResponseSuggestionActionJson().setDescription(theAction.getDescription());
if (theAction.hasType()
&& theAction.getType().hasCoding()
&& theAction.getType().getCodingFirstRep().hasCode()
&& !theAction.getType().getCodingFirstRep().getCode().equals("fire-event")) {
String actionCode = theAction.getType().getCodingFirstRep().getCode();
suggestionAction.setType(actionCode);
}
if (theAction.hasResource()) {
suggestionAction.setResource(resolveResource(theAction.getResource()));
// Leaving this out until the spec details how to map system actions.
// if (!suggestionAction.getType().isEmpty()) {
// resolveSystemAction(theAction);
// }
}
return suggestionAction;
}
protected IBaseResource resolveResource(Reference theReference) {
String reference = theReference.getReference();
String[] split = reference.split("/");
String id = reference.contains("/") ? split[1] : reference;
String resourceType = reference.contains("/") ? split[0] : theReference.getType();
List results = myResponseBundle.getEntry().stream()
.filter(entry -> entry.hasResource()
&& entry.getResource().getResourceType().toString().equals(resourceType)
&& entry.getResource().getIdPart().equals(id))
.map(entry -> entry.getResource())
.collect(Collectors.toList());
return results.isEmpty() ? null : results.get(0);
}
}