Implement CDS on FHIR in CDS Hooks (#5240)

* WIP

* WIP refactor

* Initial commit

* measureRefactor WIP

* version bump

* Bump to core release 6.0.22 (#5028)

* Bump to core release 6.0.16

* Bump to core version 6.0.20

* Fix errors thrown as a result of VersionSpecificWorkerContextWrapper

* Bump to core 6.0.22

* adding tests WIP

* Rework MeasureServiceFActory

* measure refactor WIP

* Resolve 5126 hfj res ver prov might cause migration error on db that automatically indexes the primary key (#5127)

* dropped old index FK_RESVERPROV_RES_PID on RES_PID column before adding IDX_RESVERPROV_RES_PID

* added changelog

* changed to valid version number

* changed to valid version number, need to be ordered by version number...

* generic provider loader

* provider loader update

* tests wip

* 5123 - Use DEFAULT partition for server-based requests if none specified (#5124)

5123 - Use DEFAULT partition for server-based requests if none specified

* consent remove all suppresses next link in bundle (#5119)

* added FIXME with source of issue

* added FIXME with root cause

* added FIXME with root cause

* Providing solution to the issue and removing fixmes.

* Providing changelog

* auto-formatting.

* Adding new test.

* Adding a new test for standard paging

* let's try this and see if it works...?

* fix tests

* cleanup to trigger a new run

* fixing tests

---------

Co-authored-by: Ken Stevens <ken@smilecdr.com>
Co-authored-by: peartree <etienne.poirier@smilecdr.com>

* 5117 MDM Score for No Match Fields Should Not Be Included in Total Score  (#5118)

* fix, test, changelog

* fix, test, changelog

---------

Co-authored-by: justindar <justin.dar@smilecdr.com>

* measureRefactor tests WIP

* update tests wip

* remove baseCrR4Test

* update imports

* add paging provider config to tests

* dstu3 tests

* _source search parameter needs to support modifiers (#5095)

_source search parameter needs to support modifiers - added support form :contains, :missing, :above modifiers

* latest work

* cleanup

* Refactor operation providers to support use in multiple modules

* Fix HFQL docs (#5151)

* repository measure refactor

* undo bundle edit, add config

* embedded library true

* undo post of bundle

* clear library cache on test

* fix sumbit-data test

* Fix tests

* cleanup

* Update MeasureService.java

* latest

* 3.0.0 engine update

* merge cleanup

* cr updates

* cleanup

* Only create services for PlanDefs that have a trigger with a named event

* Fix parameters being sent through invoke

* Handle system actions

* cleanup

* missing packages from cql

* fix test data and test cases for repository api

* latest

* fix pom

* fix test config

* cleanup

* 3.0.0 clinical reasoning uplift

* r4 cql execution provider and tests

* fix submitdata provider config

* wip cql tests

* debugging cql op wip

* wip debugging cql

* update tests for $cql, fix class names

* prep for Pre6 uplift

* spotless checks and test fixes

* bump to 3.0.0-PRE6

* cleanup

* fix version

* cleanup

* cleanup

* fix exlusions in pom

* add in cache invalidation config

* fix resource resolution

* cleanup

* search converter bug and test for repository

* update pom

* update searchconverter

* version bump, add changelog

* remove term config, move IDaoRegistryUser class

* Break out dstu3 providers and move config to version folders

* merge cleanup

* merge cleanup

* Add changelog

* spotless

* fix error codes

* cleanup

* cleanup

* Handle missing beans in Operation configs

* spotless

* Use CondtionalOnBean for CR Operation and Repository configs

* Fix test config

* Add RepositoryFactory and RestfulServer to CdsConfigService

* Move createRequestDetails into CdsConfigService

* spotless

* review comments

---------

Co-authored-by: justin.mckelvy <justin.mckelvy@smilecdr.com>
Co-authored-by: tadgh <garygrantgraham@gmail.com>
Co-authored-by: dotasek <david.otasek@smilecdr.com>
Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
Co-authored-by: TynerGjs <132295567+TynerGjs@users.noreply.github.com>
Co-authored-by: Steve Corbett <137920358+steve-corbett-smilecdr@users.noreply.github.com>
Co-authored-by: Ken Stevens <khstevens@gmail.com>
Co-authored-by: Ken Stevens <ken@smilecdr.com>
Co-authored-by: peartree <etienne.poirier@smilecdr.com>
Co-authored-by: jdar8 <69840459+jdar8@users.noreply.github.com>
Co-authored-by: justindar <justin.dar@smilecdr.com>
Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com>
Co-authored-by: Nathan Doef <n.doef@protonmail.com>
Co-authored-by: Justin McKelvy <60718638+Capt-Mac@users.noreply.github.com>
This commit is contained in:
Brenin Rhodes 2023-09-27 11:34:57 -06:00 committed by GitHub
parent 6ee6031a5f
commit a2204654df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 5685 additions and 23 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5238
title: "Added an implementation of Clinical Reasoning CDS on FHIR to the CDS Hooks module that allows PlanDefinition
worfklows to be processed as CDS Services using the $apply operation.
"

View File

@ -33,6 +33,11 @@
<artifactId>hapi-fhir-storage</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-storage-cr</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>

View File

@ -20,8 +20,14 @@
package ca.uhn.hapi.fhir.cdshooks.api;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.common.IRepositoryFactory;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRestfulResponse;
import ca.uhn.fhir.rest.server.RestfulServer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.opencds.cqf.fhir.utility.Ids;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -37,4 +43,22 @@ public interface ICdsConfigService {
default DaoRegistry getDaoRegistry() {
return null;
}
@Nullable
default IRepositoryFactory getRepositoryFactory() {
return null;
}
@Nullable
default RestfulServer getRestfulServer() {
return null;
}
default RequestDetails createRequestDetails(FhirContext theFhirContext, String theId, String theResourceType) {
SystemRequestDetails rd = new SystemRequestDetails();
rd.setServer(getRestfulServer());
rd.setResponse(new SystemRestfulResponse(rd));
rd.setId(Ids.newId(theFhirContext.getVersion().getVersion(), theResourceType, theId));
return rd;
}
}

View File

@ -72,6 +72,14 @@ public interface ICdsServiceRegistry {
boolean theAllowAutoFhirClientPrefetch,
String theModuleId);
/**
* Register a new Clinical Reasoning CDS Service with the endpoint.
*
* @param theServiceId the id of the service PlanDefinition
* @return the service was registered
*/
boolean registerCrService(String theServiceId);
/**
* Remove registered CDS service with the service ID, only removes dynamically registered service
*

View File

@ -20,8 +20,13 @@
package ca.uhn.hapi.fhir.cdshooks.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.common.IRepositoryFactory;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsConfigService;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
@ -29,11 +34,21 @@ import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsConfigServiceImpl;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceRegistryImpl;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrServiceDstu3;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrServiceR4;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrServiceR5;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsServiceInterceptor;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.CrDiscoveryServiceDstu3;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.CrDiscoveryServiceR4;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.CrDiscoveryServiceR5;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.ICrDiscoveryServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchDaoSvc;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchFhirClientSvc;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchSvc;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsResolutionStrategySvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.opencds.cqf.fhir.api.Repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
@ -44,12 +59,23 @@ public class CdsHooksConfig {
public static final String CDS_HOOKS_OBJECT_MAPPER_FACTORY = "cdsHooksObjectMapperFactory";
public static final String PLAN_DEFINITION_RESOURCE_NAME = "PlanDefinition";
@Autowired(required = false)
private DaoRegistry myDaoRegistry;
@Autowired(required = false)
private MatchUrlService myMatchUrlService;
@Autowired(required = false)
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
@Autowired(required = false)
private IRepositoryFactory myRepositoryFactory;
@Autowired(required = false)
private RestfulServer myRestfulServer;
@Bean(name = CDS_HOOKS_OBJECT_MAPPER_FACTORY)
public ObjectMapper objectMapper(FhirContext theFhirContext) {
return new CdsHooksObjectMapperFactory(theFhirContext).newMapper();
@ -59,14 +85,78 @@ public class CdsHooksConfig {
public ICdsServiceRegistry cdsServiceRegistry(
CdsHooksContextBooter theCdsHooksContextBooter,
CdsPrefetchSvc theCdsPrefetchSvc,
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper) {
return new CdsServiceRegistryImpl(theCdsHooksContextBooter, theCdsPrefetchSvc, theObjectMapper);
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper,
ICdsCrServiceFactory theCdsCrServiceFactory,
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory) {
return new CdsServiceRegistryImpl(
theCdsHooksContextBooter,
theCdsPrefetchSvc,
theObjectMapper,
theCdsCrServiceFactory,
theCrDiscoveryServiceFactory);
}
@Bean
public ICdsCrServiceFactory cdsCrServiceFactory(FhirContext theFhirContext, ICdsConfigService theCdsConfigService) {
return id -> {
if (myRepositoryFactory == null) {
return null;
}
RequestDetails rd =
theCdsConfigService.createRequestDetails(theFhirContext, id, PLAN_DEFINITION_RESOURCE_NAME);
Repository repository = myRepositoryFactory.create(rd);
switch (theFhirContext.getVersion().getVersion()) {
case DSTU3:
return new CdsCrServiceDstu3(rd, repository);
case R4:
return new CdsCrServiceR4(rd, repository);
case R5:
return new CdsCrServiceR5(rd, repository);
default:
return null;
}
};
}
@Bean
public ICrDiscoveryServiceFactory crDiscoveryServiceFactory(
FhirContext theFhirContext, ICdsConfigService theCdsConfigService) {
return id -> {
if (myRepositoryFactory == null) {
return null;
}
RequestDetails rd =
theCdsConfigService.createRequestDetails(theFhirContext, id, PLAN_DEFINITION_RESOURCE_NAME);
Repository repository = myRepositoryFactory.create(rd);
switch (theFhirContext.getVersion().getVersion()) {
case DSTU3:
return new CrDiscoveryServiceDstu3(rd.getId(), repository);
case R4:
return new CrDiscoveryServiceR4(rd.getId(), repository);
case R5:
return new CrDiscoveryServiceR5(rd.getId(), repository);
default:
return null;
}
};
}
@Bean
public CdsServiceInterceptor cdsServiceInterceptor() {
if (myResourceChangeListenerRegistry == null) {
return null;
}
CdsServiceInterceptor listener = new CdsServiceInterceptor();
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
PLAN_DEFINITION_RESOURCE_NAME, SearchParameterMap.newSynchronous(), listener, 1000);
return listener;
}
@Bean
public ICdsConfigService cdsConfigService(
FhirContext theFhirContext, @Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper) {
return new CdsConfigServiceImpl(theFhirContext, theObjectMapper, myDaoRegistry);
return new CdsConfigServiceImpl(
theFhirContext, theObjectMapper, myDaoRegistry, myRepositoryFactory, myRestfulServer);
}
@Bean

View File

@ -0,0 +1,47 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
abstract class BaseCdsCrMethod implements ICdsMethod {
private ICdsCrServiceFactory myCdsCrServiceFactory;
public BaseCdsCrMethod(ICdsCrServiceFactory theCdsCrServiceFactory) {
myCdsCrServiceFactory = theCdsCrServiceFactory;
}
public Object invoke(ObjectMapper theObjectMapper, IModelJson theJson, String theServiceId) {
try {
return myCdsCrServiceFactory.create(theServiceId).invoke(theJson);
} catch (Exception e) {
if (e.getCause() != null && e.getCause() instanceof BaseServerResponseException) {
throw (BaseServerResponseException) e.getCause();
}
throw new ConfigurationException(Msg.code(2434) + "Failed to invoke $apply on " + theServiceId, e);
}
}
}

View File

@ -20,7 +20,9 @@
package ca.uhn.hapi.fhir.cdshooks.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.common.IRepositoryFactory;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsConfigService;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -31,14 +33,20 @@ public class CdsConfigServiceImpl implements ICdsConfigService {
private final FhirContext myFhirContext;
private final ObjectMapper myObjectMapper;
private final DaoRegistry myDaoRegistry;
private final IRepositoryFactory myRepositoryFactory;
private final RestfulServer myRestfulServer;
public CdsConfigServiceImpl(
@Nonnull FhirContext theFhirContext,
@Nonnull ObjectMapper theObjectMapper,
@Nullable DaoRegistry theDaoRegistry) {
@Nullable DaoRegistry theDaoRegistry,
@Nullable IRepositoryFactory theRepositoryFactory,
@Nullable RestfulServer theRestfulServer) {
myFhirContext = theFhirContext;
myObjectMapper = theObjectMapper;
myDaoRegistry = theDaoRegistry;
myRepositoryFactory = theRepositoryFactory;
myRestfulServer = theRestfulServer;
}
@Nonnull
@ -58,4 +66,16 @@ public class CdsConfigServiceImpl implements ICdsConfigService {
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
@Nullable
@Override
public IRepositoryFactory getRepositoryFactory() {
return myRepositoryFactory;
}
@Nullable
@Override
public RestfulServer getRestfulServer() {
return myRestfulServer;
}
}

View File

@ -0,0 +1,45 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
public class CdsCrServiceMethod extends BaseCdsCrMethod implements ICdsServiceMethod {
private final CdsServiceJson myCdsServiceJson;
public CdsCrServiceMethod(CdsServiceJson theCdsServiceJson, ICdsCrServiceFactory theCdsCrServiceFactory) {
super(theCdsCrServiceFactory);
myCdsServiceJson = theCdsServiceJson;
}
@Override
public CdsServiceJson getCdsServiceJson() {
return myCdsServiceJson;
}
@Override
public boolean isAllowAutoFhirClientPrefetch() {
// The $apply operation will make FHIR requests for any data it needs
// directly against the fhirServer of the ServiceRequest.
return false;
}
}

View File

@ -25,6 +25,8 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServicesJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.ICrDiscoveryServiceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,6 +35,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_CR_MODULE_ID;
public class CdsServiceCache {
static final Logger ourLog = LoggerFactory.getLogger(CdsServiceCache.class);
final Map<String, ICdsMethod> myServiceMap = new LinkedHashMap<>();
@ -66,6 +70,23 @@ public class CdsServiceCache {
}
}
public void registerCrService(
String theServiceId,
ICrDiscoveryServiceFactory theDiscoveryServiceFactory,
ICdsCrServiceFactory theCrServiceFactory) {
if (!isCdsServiceAlreadyRegistered(theServiceId, CDS_CR_MODULE_ID)) {
CdsServiceJson cdsServiceJson =
theDiscoveryServiceFactory.create(theServiceId).resolveService();
if (cdsServiceJson != null) {
final CdsCrServiceMethod cdsCrServiceMethod =
new CdsCrServiceMethod(cdsServiceJson, theCrServiceFactory);
myServiceMap.put(theServiceId, cdsCrServiceMethod);
myCdsServiceJson.addService(cdsServiceJson);
ourLog.info("Created service for {}", theServiceId);
}
}
}
public void registerFeedback(String theServiceId, Object theServiceBean, Method theMethod) {
final CdsFeedbackMethod cdsFeedbackMethod = new CdsFeedbackMethod(theServiceBean, theMethod);
myFeedbackMap.put(theServiceId, cdsFeedbackMethod);

View File

@ -30,6 +30,8 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServicesJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.ICrDiscoveryServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchSvc;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -49,14 +51,20 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
private final CdsHooksContextBooter myCdsHooksContextBooter;
private final CdsPrefetchSvc myCdsPrefetchSvc;
private final ObjectMapper myObjectMapper;
private final ICdsCrServiceFactory myCdsCrServiceFactory;
private final ICrDiscoveryServiceFactory myCrDiscoveryServiceFactory;
public CdsServiceRegistryImpl(
CdsHooksContextBooter theCdsHooksContextBooter,
CdsPrefetchSvc theCdsPrefetchSvc,
ObjectMapper theObjectMapper) {
ObjectMapper theObjectMapper,
ICdsCrServiceFactory theCdsCrServiceFactory,
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory) {
myCdsHooksContextBooter = theCdsHooksContextBooter;
myCdsPrefetchSvc = theCdsPrefetchSvc;
myObjectMapper = theObjectMapper;
myCdsCrServiceFactory = theCdsCrServiceFactory;
myCrDiscoveryServiceFactory = theCrDiscoveryServiceFactory;
}
@PostConstruct
@ -142,6 +150,17 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
theServiceId, theServiceFunction, theCdsServiceJson, theAllowAutoFhirClientPrefetch, theModuleId);
}
@Override
public boolean registerCrService(String theServiceId) {
try {
myServiceCache.registerCrService(theServiceId, myCrDiscoveryServiceFactory, myCdsCrServiceFactory);
} catch (Exception e) {
ourLog.error("Error received during CR CDS Service registration: {}", e.getMessage());
return false;
}
return true;
}
@Override
public void unregisterService(String theServiceId, String theModuleId) {
Validate.notNull(theServiceId);

View File

@ -0,0 +1,48 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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;
public class CdsCrConstants {
private CdsCrConstants() {}
public static final String CDS_CR_MODULE_ID = "CR";
// CDS Hook field names
public static final String CDS_PARAMETER_USER_ID = "userId";
public static final String CDS_PARAMETER_PATIENT_ID = "patientId";
public static final String CDS_PARAMETER_ENCOUNTER_ID = "encounterId";
public static final String CDS_PARAMETER_MEDICATIONS = "medications";
public static final String CDS_PARAMETER_PERFORMER = "performer";
public static final String CDS_PARAMETER_TASK = "task";
public static final String CDS_PARAMETER_ORDERS = "orders";
public static final String CDS_PARAMETER_SELECTIONS = "selections";
public static final String CDS_PARAMETER_DRAFT_ORDERS = "draftOrders";
public static final String CDS_PARAMETER_APPOINTMENTS = "appointments";
// $apply parameter names
public static final String APPLY_PARAMETER_PLAN_DEFINITION = "planDefinition";
public static final String APPLY_PARAMETER_CANONICAL = "canonical";
public static final String APPLY_PARAMETER_SUBJECT = "subject";
public static final String APPLY_PARAMETER_PRACTITIONER = "practitioner";
public static final String APPLY_PARAMETER_ENCOUNTER = "encounter";
public static final String APPLY_PARAMETER_PARAMETERS = "parameters";
public static final String APPLY_PARAMETER_DATA = "data";
public static final String APPLY_PARAMETER_DATA_ENDPOINT = "dataEndpoint";
}

View File

@ -0,0 +1,291 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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.rest.api.server.RequestDetails;
import ca.uhn.hapi.fhir.cdshooks.api.json.*;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.CarePlan;
import org.hl7.fhir.dstu3.model.Endpoint;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.ParameterDefinition;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.RelatedArtifact;
import org.hl7.fhir.dstu3.model.RequestGroup;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.instance.model.api.IBaseResource;
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 static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_DATA;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.APPLY_PARAMETER_DATA_ENDPOINT;
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.dstu3.Parameters.parameters;
import static org.opencds.cqf.fhir.utility.dstu3.Parameters.part;
public class CdsCrServiceDstu3 implements ICdsCrService {
protected final RequestDetails myRequestDetails;
protected final Repository myRepository;
protected CarePlan myResponse;
protected CdsServiceResponseJson myServiceResponse;
public CdsCrServiceDstu3(RequestDetails theRequestDetails, Repository theRepository) {
myRequestDetails = theRequestDetails;
myRepository = theRepository;
}
public FhirVersionEnum getFhirVersion() {
return FhirVersionEnum.DSTU3;
}
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));
}
if (theJson.getFhirServer() != null) {
Endpoint endpoint = new Endpoint().setAddress(theJson.getFhirServer());
if (theJson.getServiceRequestAuthorizationJson().getAccessToken() != null) {
String tokenType = getTokenType(theJson.getServiceRequestAuthorizationJson());
endpoint.addHeader(String.format(
"Authorization: %s %s",
tokenType, theJson.getServiceRequestAuthorizationJson().getAccessToken()));
}
parameters.addParameter(part(APPLY_PARAMETER_DATA_ENDPOINT, endpoint));
}
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<String, Resource> getResourcesFromBundle(Bundle theBundle) {
// using HashMap to avoid duplicates
Map<String, Resource> 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<String, Resource> 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 CarePlan;
myResponse = (CarePlan) theResponse;
CdsServiceResponseJson serviceResponse = new CdsServiceResponseJson();
if (myResponse.hasActivity()) {
Reference requestGroupRef = myResponse.getActivity().get(0).getReference();
RequestGroup mainRequest = (RequestGroup) resolveResource(requestGroupRef);
StringType canonical = mainRequest.getDefinition().get(0).getReferenceElement_();
PlanDefinition planDef = myRepository.read(
PlanDefinition.class,
new IdType(Canonicals.getResourceType(canonical), Canonicals.getIdPart(canonical)));
List<CdsServiceResponseLinkJson> links = resolvePlanLinks(planDef);
mainRequest.getAction().forEach(action -> serviceResponse.addCard(resolveAction(action, links)));
}
return serviceResponse;
}
protected List<CdsServiceResponseLinkJson> resolvePlanLinks(PlanDefinition thePlanDefinition) {
List<CdsServiceResponseLinkJson> links = new ArrayList<>();
// links - listed on each card
if (thePlanDefinition.hasRelatedArtifact()) {
thePlanDefinition.getRelatedArtifact().forEach(ra -> {
String linkUrl = ra.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(
RequestGroup.RequestGroupActionComponent theAction, List<CdsServiceResponseLinkJson> theLinks) {
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson()
.setSummary(theAction.getTitle())
.setDetail(theAction.getDescription())
.setLinks(theLinks);
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 void resolveSystemAction(RequestGroup.RequestGroupActionComponent theAction) {
if (theAction.hasType()
&& theAction.getType().hasCode()
&& !theAction.getType().getCode().equals("fire-event")) {
myServiceResponse.addServiceAction(new CdsServiceResponseSystemActionJson()
.setResource(resolveResource(theAction.getResource()))
.setType(theAction.getType().getCode()));
}
}
protected CdsServiceResponseCardSourceJson resolveSource(RequestGroup.RequestGroupActionComponent theAction) {
RelatedArtifact documentation = theAction.getDocumentationFirstRep();
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson()
.setLabel(documentation.getDisplay())
.setUrl(documentation.getUrl());
if (documentation.hasDocument() && documentation.getDocument().hasUrl()) {
source.setIcon(documentation.getDocument().getUrl());
}
return source;
}
protected CdsServiceResponseSuggestionJson resolveSuggestion(RequestGroup.RequestGroupActionComponent theAction) {
CdsServiceResponseSuggestionJson suggestion = new CdsServiceResponseSuggestionJson()
.setLabel(theAction.getTitle())
.setUuid(theAction.getId());
theAction.getAction().forEach(action -> suggestion.addAction(resolveSuggestionAction(action)));
return suggestion;
}
protected CdsServiceResponseSuggestionActionJson resolveSuggestionAction(
RequestGroup.RequestGroupActionComponent theAction) {
CdsServiceResponseSuggestionActionJson suggestionAction =
new CdsServiceResponseSuggestionActionJson().setDescription(theAction.getDescription());
if (theAction.hasType()
&& theAction.getType().hasCode()
&& !theAction.getType().getCode().equals("fire-event")) {
String actionCode = theAction.getType().getCode();
suggestionAction.setType(actionCode);
}
if (theAction.hasResource()) {
suggestionAction.setResource(resolveResource(theAction.getResource()));
}
return suggestionAction;
}
protected IBaseResource resolveResource(Reference theReference) {
return myResponse.getContained().stream()
.filter(resource -> resource.getId().equals(theReference.getReference()))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,334 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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.json.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.ParameterDefinition;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.RelatedArtifact;
import org.hl7.fhir.r4.model.RequestGroup;
import org.hl7.fhir.r4.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_DATA_ENDPOINT;
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.r4.Parameters.parameters;
import static org.opencds.cqf.fhir.utility.r4.Parameters.part;
public class CdsCrServiceR4 implements ICdsCrService {
protected final RequestDetails myRequestDetails;
protected final Repository myRepository;
protected Bundle myResponseBundle;
protected CdsServiceResponseJson myServiceResponse;
public CdsCrServiceR4(RequestDetails theRequestDetails, Repository theRepository) {
myRequestDetails = theRequestDetails;
myRepository = theRepository;
}
public FhirVersionEnum getFhirVersion() {
return FhirVersionEnum.R4;
}
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));
}
if (theJson.getFhirServer() != null) {
Endpoint endpoint = new Endpoint().setAddress(theJson.getFhirServer());
if (theJson.getServiceRequestAuthorizationJson().getAccessToken() != null) {
String tokenType = getTokenType(theJson.getServiceRequestAuthorizationJson());
endpoint.addHeader(String.format(
"Authorization: %s %s",
tokenType, theJson.getServiceRequestAuthorizationJson().getAccessToken()));
}
endpoint.addHeader("Epic-Client-ID: 2cb5af9f-f483-4e2a-aedc-54c3a31cb153");
parameters.addParameter(part(APPLY_PARAMETER_DATA_ENDPOINT, endpoint));
}
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<String, Resource> getResourcesFromBundle(Bundle theBundle) {
// using HashMap to avoid duplicates
Map<String, Resource> 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<String, Resource> 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;
myServiceResponse = new CdsServiceResponseJson();
if (myResponseBundle.hasEntry()) {
RequestGroup mainRequest =
(RequestGroup) 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<CdsServiceResponseLinkJson> links = resolvePlanLinks(planDef);
mainRequest.getAction().forEach(action -> myServiceResponse.addCard(resolveAction(action, links)));
}
return myServiceResponse;
}
protected List<CdsServiceResponseLinkJson> resolvePlanLinks(PlanDefinition thePlanDefinition) {
List<CdsServiceResponseLinkJson> links = new ArrayList<>();
// links - listed on each card
if (thePlanDefinition.hasRelatedArtifact()) {
thePlanDefinition.getRelatedArtifact().forEach(ra -> {
String linkUrl = ra.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(
RequestGroup.RequestGroupActionComponent theAction, List<CdsServiceResponseLinkJson> 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(2435) + "Invalid priority code: " + theCode);
}
return indicator;
}
protected void resolveSystemAction(RequestGroup.RequestGroupActionComponent 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(RequestGroup.RequestGroupActionComponent theAction) {
RelatedArtifact documentation = theAction.getDocumentationFirstRep();
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson()
.setLabel(documentation.getDisplay())
.setUrl(documentation.getUrl());
if (documentation.hasDocument() && documentation.getDocument().hasUrl()) {
source.setIcon(documentation.getDocument().getUrl());
}
return source;
}
protected CdsServiceResponseSuggestionJson resolveSuggestion(RequestGroup.RequestGroupActionComponent theAction) {
CdsServiceResponseSuggestionJson suggestion = new CdsServiceResponseSuggestionJson()
.setLabel(theAction.getTitle())
.setUuid(theAction.getId());
theAction.getAction().forEach(action -> suggestion.addAction(resolveSuggestionAction(action)));
return suggestion;
}
protected CdsServiceResponseSuggestionActionJson resolveSuggestionAction(
RequestGroup.RequestGroupActionComponent 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<IBaseResource> 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);
}
}

View File

@ -0,0 +1,337 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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.json.*;
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.Endpoint;
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_DATA_ENDPOINT;
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 Bundle myResponseBundle;
protected CdsServiceResponseJson myServiceResponse;
public CdsCrServiceR5(RequestDetails theRequestDetails, Repository theRepository) {
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));
}
if (theJson.getFhirServer() != null) {
Endpoint endpoint = new Endpoint().setAddress(theJson.getFhirServer());
if (theJson.getServiceRequestAuthorizationJson().getAccessToken() != null) {
String tokenType = getTokenType(theJson.getServiceRequestAuthorizationJson());
endpoint.addHeader(String.format(
"Authorization: %s %s",
tokenType, theJson.getServiceRequestAuthorizationJson().getAccessToken()));
}
parameters.addParameter(part(APPLY_PARAMETER_DATA_ENDPOINT, endpoint));
}
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<String, Resource> getResourcesFromBundle(Bundle theBundle) {
// using HashMap to avoid duplicates
Map<String, Resource> 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<String, Resource> 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<CdsServiceResponseLinkJson> links = resolvePlanLinks(planDef);
mainRequest.getAction().forEach(action -> serviceResponse.addCard(resolveAction(action, links)));
}
return serviceResponse;
}
protected List<CdsServiceResponseLinkJson> resolvePlanLinks(PlanDefinition thePlanDefinition) {
List<CdsServiceResponseLinkJson> 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<CdsServiceResponseLinkJson> 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<IBaseResource> 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);
}
}

View File

@ -0,0 +1,41 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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 org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.api.Repository;
public class CdsCrUtils {
public static IBaseResource readPlanDefinitionFromRepository(
FhirVersionEnum theFhirVersion, Repository theRepository, IIdType theId) {
switch (theFhirVersion) {
case DSTU3:
return theRepository.read(org.hl7.fhir.dstu3.model.PlanDefinition.class, theId);
case R4:
return theRepository.read(org.hl7.fhir.r4.model.PlanDefinition.class, theId);
case R5:
return theRepository.read(org.hl7.fhir.r5.model.PlanDefinition.class, theId);
default:
return null;
}
}
}

View File

@ -0,0 +1,93 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.ResourceChangeEvent;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceRegistryImpl;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrConstants.CDS_CR_MODULE_ID;
public class CdsServiceInterceptor implements IResourceChangeListener {
static final Logger ourLog = LoggerFactory.getLogger(CdsServiceInterceptor.class);
@Autowired
CdsServiceRegistryImpl myCdsServiceRegistry;
public CdsServiceInterceptor() {}
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
handleChange(ResourceChangeEvent.fromCreatedUpdatedDeletedResourceIds(
new ArrayList<>(theResourceIds), Collections.emptyList(), Collections.emptyList()));
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
if (theResourceChangeEvent == null) return;
if (theResourceChangeEvent.getCreatedResourceIds() != null
&& !theResourceChangeEvent.getCreatedResourceIds().isEmpty()) {
insert(theResourceChangeEvent.getCreatedResourceIds());
}
if (theResourceChangeEvent.getUpdatedResourceIds() != null
&& !theResourceChangeEvent.getUpdatedResourceIds().isEmpty()) {
update(theResourceChangeEvent.getUpdatedResourceIds());
}
if (theResourceChangeEvent.getDeletedResourceIds() != null
&& !theResourceChangeEvent.getDeletedResourceIds().isEmpty()) {
delete(theResourceChangeEvent.getDeletedResourceIds());
}
}
private void insert(List<IIdType> theCreatedIds) {
for (IIdType id : theCreatedIds) {
try {
myCdsServiceRegistry.registerCrService(id.getIdPart());
} catch (Exception e) {
ourLog.info(String.format("Failed to create service for %s", id.getIdPart()));
}
}
}
private void update(List<IIdType> updatedIds) {
try {
delete(updatedIds);
insert(updatedIds);
} catch (Exception e) {
ourLog.info(String.format("Failed to update service(s) for %s", updatedIds));
}
}
private void delete(List<IIdType> deletedIds) {
for (IIdType id : deletedIds) {
myCdsServiceRegistry.unregisterService(id.getIdPart(), CDS_CR_MODULE_ID);
}
}
}

View File

@ -0,0 +1,82 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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.model.api.IModelJson;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.fhir.api.Repository;
import java.util.Collections;
public interface ICdsCrService {
IBaseParameters encodeParams(CdsServiceRequestJson theJson);
CdsServiceResponseJson encodeResponse(Object theResponse);
FhirVersionEnum getFhirVersion();
Repository getRepository();
default Object invoke(IModelJson theJson) {
IBaseParameters params = encodeParams((CdsServiceRequestJson) theJson);
IBaseResource response = invokeApply(params);
return encodeResponse(response);
}
default IBaseResource invokeApply(IBaseParameters theParams) {
var operationName = getFhirVersion() == FhirVersionEnum.R4
? ProviderConstants.CR_OPERATION_R5_APPLY
: ProviderConstants.CR_OPERATION_APPLY;
switch (getFhirVersion()) {
case DSTU3:
return getRepository()
.invoke(
org.hl7.fhir.dstu3.model.PlanDefinition.class,
operationName,
theParams,
org.hl7.fhir.dstu3.model.CarePlan.class,
Collections.singletonMap(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON));
case R4:
return getRepository()
.invoke(
org.hl7.fhir.r4.model.PlanDefinition.class,
operationName,
theParams,
org.hl7.fhir.r4.model.Bundle.class,
Collections.singletonMap(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON));
case R5:
return getRepository()
.invoke(
org.hl7.fhir.r5.model.PlanDefinition.class,
operationName,
theParams,
org.hl7.fhir.r5.model.Bundle.class,
Collections.singletonMap(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON));
default:
return null;
}
}
}

View File

@ -0,0 +1,24 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 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;
public interface ICdsCrServiceFactory {
ICdsCrService create(String theServiceId);
}

View File

@ -0,0 +1,82 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.r4.model.TriggerDefinition;
import java.util.stream.Collectors;
public class CrDiscoveryElementDstu3 implements ICrDiscoveryElement {
protected PlanDefinition myPlanDefinition;
protected PrefetchUrlList myPrefetchUrlList;
public CrDiscoveryElementDstu3(PlanDefinition thePlanDefinition, PrefetchUrlList thePrefetchUrlList) {
myPlanDefinition = thePlanDefinition;
myPrefetchUrlList = thePrefetchUrlList;
}
public CdsServiceJson getCdsServiceJson() {
if (myPlanDefinition == null
|| !myPlanDefinition.hasAction()
|| myPlanDefinition.getAction().stream().noneMatch(a -> a.hasTriggerDefinition())) {
return null;
}
var triggerDefs = myPlanDefinition.getAction().stream()
.filter(a -> a.hasTriggerDefinition())
.flatMap(a -> a.getTriggerDefinition().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
var service = new CdsServiceJson()
.setId(myPlanDefinition.getIdElement().getIdPart())
.setTitle(myPlanDefinition.getTitle())
.setDescription(myPlanDefinition.getDescription())
.setHook(triggerDefs.get(0).getEventName());
if (myPrefetchUrlList == null) {
myPrefetchUrlList = new PrefetchUrlList();
}
int itemNo = 0;
if (!myPrefetchUrlList.stream()
.anyMatch(p -> p.equals("Patient/{{context.patientId}}")
|| p.equals("Patient?_id={{context.patientId}}")
|| p.equals("Patient?_id=Patient/{{context.patientId}}"))) {
String key = getKey(++itemNo);
service.addPrefetch(key, "Patient?_id={{context.patientId}}");
service.addSource(key, CdsResolutionStrategyEnum.SERVICE);
}
for (String item : myPrefetchUrlList) {
String key = getKey(++itemNo);
service.addPrefetch(key, item);
service.addSource(key, CdsResolutionStrategyEnum.SERVICE);
}
return service;
}
}

View File

@ -0,0 +1,82 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.TriggerDefinition;
import java.util.stream.Collectors;
public class CrDiscoveryElementR4 implements ICrDiscoveryElement {
protected PlanDefinition myPlanDefinition;
protected PrefetchUrlList myPrefetchUrlList;
public CrDiscoveryElementR4(PlanDefinition thePlanDefinition, PrefetchUrlList thePrefetchUrlList) {
myPlanDefinition = thePlanDefinition;
myPrefetchUrlList = thePrefetchUrlList;
}
public CdsServiceJson getCdsServiceJson() {
if (myPlanDefinition == null
|| !myPlanDefinition.hasAction()
|| myPlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) {
return null;
}
var triggerDefs = myPlanDefinition.getAction().stream()
.filter(a -> a.hasTrigger())
.flatMap(a -> a.getTrigger().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
var service = new CdsServiceJson()
.setId(myPlanDefinition.getIdElement().getIdPart())
.setTitle(myPlanDefinition.getTitle())
.setDescription(myPlanDefinition.getDescription())
.setHook(triggerDefs.get(0).getName());
if (myPrefetchUrlList == null) {
myPrefetchUrlList = new PrefetchUrlList();
}
int itemNo = 0;
if (!myPrefetchUrlList.stream()
.anyMatch(p -> p.equals("Patient/{{context.patientId}}")
|| p.equals("Patient?_id={{context.patientId}}")
|| p.equals("Patient?_id=Patient/{{context.patientId}}"))) {
String key = getKey(++itemNo);
service.addPrefetch(key, "Patient?_id={{context.patientId}}");
service.addSource(key, CdsResolutionStrategyEnum.NONE);
}
for (String item : myPrefetchUrlList) {
String key = getKey(++itemNo);
service.addPrefetch(key, item);
service.addSource(key, CdsResolutionStrategyEnum.NONE);
}
return service;
}
}

View File

@ -0,0 +1,82 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import org.hl7.fhir.r4.model.TriggerDefinition;
import org.hl7.fhir.r5.model.PlanDefinition;
import java.util.stream.Collectors;
public class CrDiscoveryElementR5 implements ICrDiscoveryElement {
protected PlanDefinition myPlanDefinition;
protected PrefetchUrlList myPrefetchUrlList;
public CrDiscoveryElementR5(PlanDefinition thePlanDefinition, PrefetchUrlList thePrefetchUrlList) {
myPlanDefinition = thePlanDefinition;
myPrefetchUrlList = thePrefetchUrlList;
}
public CdsServiceJson getCdsServiceJson() {
if (myPlanDefinition == null
|| !myPlanDefinition.hasAction()
|| myPlanDefinition.getAction().stream().noneMatch(a -> a.hasTrigger())) {
return null;
}
var triggerDefs = myPlanDefinition.getAction().stream()
.filter(a -> a.hasTrigger())
.flatMap(a -> a.getTrigger().stream())
.filter(t -> t.getType().equals(TriggerDefinition.TriggerType.NAMEDEVENT))
.collect(Collectors.toList());
if (triggerDefs == null || triggerDefs.isEmpty()) {
return null;
}
var service = new CdsServiceJson()
.setId(myPlanDefinition.getIdElement().getIdPart())
.setTitle(myPlanDefinition.getTitle())
.setDescription(myPlanDefinition.getDescription())
.setHook(triggerDefs.get(0).getName());
if (myPrefetchUrlList == null) {
myPrefetchUrlList = new PrefetchUrlList();
}
int itemNo = 0;
if (!myPrefetchUrlList.stream()
.anyMatch(p -> p.equals("Patient/{{context.patientId}}")
|| p.equals("Patient?_id={{context.patientId}}")
|| p.equals("Patient?_id=Patient/{{context.patientId}}"))) {
String key = getKey(++itemNo);
service.addPrefetch(key, "Patient?_id={{context.patientId}}");
service.addSource(key, CdsResolutionStrategyEnum.SERVICE);
}
for (String item : myPrefetchUrlList) {
String key = getKey(++itemNo);
service.addPrefetch(key, item);
service.addSource(key, CdsResolutionStrategyEnum.SERVICE);
}
return service;
}
}

View File

@ -0,0 +1,445 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.DataRequirement;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.dstu3.SearchHelper;
import java.util.ArrayList;
import java.util.List;
public class CrDiscoveryServiceDstu3 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected Repository myRepository;
protected final IIdType myPlanDefinitionId;
public CrDiscoveryServiceDstu3(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH;
}
public CdsServiceJson resolveService() {
return resolveService(
CdsCrUtils.readPlanDefinitionFromRepository(FhirVersionEnum.DSTU3, myRepository, myPlanDefinitionId));
}
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementDstu3(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson();
}
return null;
}
public boolean isEca(PlanDefinition thePlanDefinition) {
if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) {
for (Coding coding : thePlanDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) {
return true;
}
}
}
return false;
}
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// Assuming 1 library
// TODO: enhance to handle multiple libraries - need a way to identify primary
// library
Library library = null;
if (thePlanDefinition.hasLibrary()
&& thePlanDefinition.getLibraryFirstRep().hasReference()) {
library = myRepository.read(
Library.class, thePlanDefinition.getLibraryFirstRep().getReferenceElement());
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(StringType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
StringType codeFilterComponentString = null;
if (codeFilterComponent.hasValueSetStringType()) {
codeFilterComponentString = codeFilterComponent.getValueSetStringType();
} else if (codeFilterComponent.hasValueSetReference()) {
codeFilterComponentString = new StringType(
codeFilterComponent.getValueSetReference().getReference());
} else if (codeFilterComponent.hasValueCoding()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getValueCoding();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
if (codeFilterComponentString != null) {
for (String codes : resolveValueSetCodes(codeFilterComponentString)) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (!library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
case "ProcedureRequest":
if (thePath.equals("bodySite")) return "body-site";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodySite":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "EligibilityRequest":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingManifest":
case "ImagingStudy":
case "Immunization":
case "ImmunizationRecommendation":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "ProcedureRequest":
case "Provenance":
case "QuestionnaireResponse":
case "ReferralRequest":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodySite":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "patient";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "EligibilityRequest":
return "patient";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingManifest":
return "patient";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "ProcedureRequest":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "ReferralRequest":
return "patient";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -0,0 +1,429 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DataRequirement;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.r4.SearchHelper;
import java.util.ArrayList;
import java.util.List;
public class CrDiscoveryServiceR4 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected final Repository myRepository;
protected final IIdType myPlanDefinitionId;
public CrDiscoveryServiceR4(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH;
}
public CdsServiceJson resolveService() {
return resolveService(
CdsCrUtils.readPlanDefinitionFromRepository(FhirVersionEnum.R4, myRepository, myPlanDefinitionId));
}
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementR4(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson();
}
return null;
}
public boolean isEca(PlanDefinition planDefinition) {
if (planDefinition.hasType() && planDefinition.getType().hasCoding()) {
for (Coding coding : planDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) {
return true;
}
}
}
return false;
}
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
Library library = null;
if (thePlanDefinition.hasLibrary() && !thePlanDefinition.getLibrary().isEmpty()) {
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> valueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : valueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(CanonicalType valueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, valueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(List<String> ret, StringBuilder codes, String system, String code) {
String codeToken = system + "|" + code;
int postAppendLength = codes.length() + codeToken.length();
if (codes.length() > 0 && postAppendLength < myMaxUriLength) {
codes.append(",");
} else if (postAppendLength > myMaxUriLength) {
ret.add(codes.toString());
codes = new StringBuilder();
}
codes.append(codeToken);
return codes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path = mapCodePathToSearchParam(theDataRequirement.getType(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "policy-holder";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -0,0 +1,431 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DataRequirement;
import org.hl7.fhir.r5.model.Library;
import org.hl7.fhir.r5.model.PlanDefinition;
import org.hl7.fhir.r5.model.ValueSet;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.r5.SearchHelper;
import java.util.ArrayList;
import java.util.List;
public class CrDiscoveryServiceR5 implements ICrDiscoveryService {
protected final String PATIENT_ID_CONTEXT = "{{context.patientId}}";
protected final int DEFAULT_MAX_URI_LENGTH = 8000;
protected int myMaxUriLength;
protected final Repository myRepository;
protected final IIdType myPlanDefinitionId;
public CrDiscoveryServiceR5(IIdType thePlanDefinitionId, Repository theRepository) {
myPlanDefinitionId = thePlanDefinitionId;
myRepository = theRepository;
myMaxUriLength = DEFAULT_MAX_URI_LENGTH;
}
public CdsServiceJson resolveService() {
return resolveService(
CdsCrUtils.readPlanDefinitionFromRepository(FhirVersionEnum.R5, myRepository, myPlanDefinitionId));
}
protected CdsServiceJson resolveService(IBaseResource thePlanDefinition) {
if (thePlanDefinition instanceof PlanDefinition) {
PlanDefinition planDef = (PlanDefinition) thePlanDefinition;
return new CrDiscoveryElementR5(planDef, getPrefetchUrlList(planDef)).getCdsServiceJson();
}
return null;
}
public boolean isEca(PlanDefinition thePlanDefinition) {
if (thePlanDefinition.hasType() && thePlanDefinition.getType().hasCoding()) {
for (Coding coding : thePlanDefinition.getType().getCoding()) {
if (coding.getCode().equals("eca-rule")) {
return true;
}
}
}
return false;
}
public Library resolvePrimaryLibrary(PlanDefinition thePlanDefinition) {
// The CPGComputablePlanDefinition profile limits the cardinality of library to 1
Library library = null;
if (thePlanDefinition.hasLibrary() && !thePlanDefinition.getLibrary().isEmpty()) {
library = (Library) SearchHelper.searchRepositoryByCanonical(
myRepository, thePlanDefinition.getLibrary().get(0));
}
return library;
}
public List<String> resolveValueCodingCodes(List<Coding> theValueCodings) {
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
for (Coding coding : theValueCodings) {
if (coding.hasCode()) {
String system = coding.getSystem();
String code = coding.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
result.add(codes.toString());
return result;
}
public List<String> resolveValueSetCodes(CanonicalType theValueSetId) {
ValueSet valueSet = (ValueSet) SearchHelper.searchRepositoryByCanonical(myRepository, theValueSetId);
List<String> result = new ArrayList<>();
StringBuilder codes = new StringBuilder();
if (valueSet.hasExpansion() && valueSet.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent contains :
valueSet.getExpansion().getContains()) {
String system = contains.getSystem();
String code = contains.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
} else if (valueSet.hasCompose() && valueSet.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent concepts : valueSet.getCompose().getInclude()) {
String system = concepts.getSystem();
if (concepts.hasConcept()) {
for (ValueSet.ConceptReferenceComponent concept : concepts.getConcept()) {
String code = concept.getCode();
codes = getCodesStringBuilder(result, codes, system, code);
}
}
}
}
result.add(codes.toString());
return result;
}
protected StringBuilder getCodesStringBuilder(
List<String> theList, StringBuilder theCodes, String theSystem, String theCode) {
String codeToken = theSystem + "|" + theCode;
int postAppendLength = theCodes.length() + codeToken.length();
if (theCodes.length() > 0 && postAppendLength < myMaxUriLength) {
theCodes.append(",");
} else if (postAppendLength > myMaxUriLength) {
theList.add(theCodes.toString());
theCodes = new StringBuilder();
}
theCodes.append(codeToken);
return theCodes;
}
public List<String> createRequestUrl(DataRequirement theDataRequirement) {
if (!isPatientCompartment(theDataRequirement.getType().toCode())) return null;
String patientRelatedResource = theDataRequirement.getType() + "?"
+ getPatientSearchParam(theDataRequirement.getType().toCode())
+ "=Patient/" + PATIENT_ID_CONTEXT;
List<String> ret = new ArrayList<>();
if (theDataRequirement.hasCodeFilter()) {
for (DataRequirement.DataRequirementCodeFilterComponent codeFilterComponent :
theDataRequirement.getCodeFilter()) {
if (!codeFilterComponent.hasPath()) continue;
String path =
mapCodePathToSearchParam(theDataRequirement.getType().toCode(), codeFilterComponent.getPath());
if (codeFilterComponent.hasValueSetElement()) {
for (String codes : resolveValueSetCodes(codeFilterComponent.getValueSetElement())) {
ret.add(patientRelatedResource + "&" + path + "=" + codes);
}
} else if (codeFilterComponent.hasCode()) {
List<Coding> codeFilterValueCodings = codeFilterComponent.getCode();
boolean isFirstCodingInFilter = true;
for (String code : resolveValueCodingCodes(codeFilterValueCodings)) {
if (isFirstCodingInFilter) {
ret.add(patientRelatedResource + "&" + path + "=" + code);
} else {
ret.add("," + code);
}
isFirstCodingInFilter = false;
}
}
}
return ret;
} else {
ret.add(patientRelatedResource);
return ret;
}
}
public PrefetchUrlList getPrefetchUrlList(PlanDefinition thePlanDefinition) {
PrefetchUrlList prefetchList = new PrefetchUrlList();
if (thePlanDefinition == null) return null;
if (!isEca(thePlanDefinition)) return null;
Library library = resolvePrimaryLibrary(thePlanDefinition);
// TODO: resolve data requirements
if (library == null || !library.hasDataRequirement()) return null;
for (DataRequirement dataRequirement : library.getDataRequirement()) {
List<String> requestUrls = createRequestUrl(dataRequirement);
if (requestUrls != null) {
prefetchList.addAll(requestUrls);
}
}
return prefetchList;
}
protected String mapCodePathToSearchParam(String theDataType, String thePath) {
switch (theDataType) {
case "MedicationAdministration":
if (thePath.equals("medication")) return "code";
break;
case "MedicationDispense":
if (thePath.equals("medication")) return "code";
break;
case "MedicationRequest":
if (thePath.equals("medication")) return "code";
break;
case "MedicationStatement":
if (thePath.equals("medication")) return "code";
break;
default:
if (thePath.equals("vaccineCode")) return "vaccine-code";
break;
}
return thePath.replace('.', '-').toLowerCase();
}
public static boolean isPatientCompartment(String theDataType) {
if (theDataType == null) {
return false;
}
switch (theDataType) {
case "Account":
case "AdverseEvent":
case "AllergyIntolerance":
case "Appointment":
case "AppointmentResponse":
case "AuditEvent":
case "Basic":
case "BodyStructure":
case "CarePlan":
case "CareTeam":
case "ChargeItem":
case "Claim":
case "ClaimResponse":
case "ClinicalImpression":
case "Communication":
case "CommunicationRequest":
case "Composition":
case "Condition":
case "Consent":
case "Coverage":
case "CoverageEligibilityRequest":
case "CoverageEligibilityResponse":
case "DetectedIssue":
case "DeviceRequest":
case "DeviceUseStatement":
case "DiagnosticReport":
case "DocumentManifest":
case "DocumentReference":
case "Encounter":
case "EnrollmentRequest":
case "EpisodeOfCare":
case "ExplanationOfBenefit":
case "FamilyMemberHistory":
case "Flag":
case "Goal":
case "Group":
case "ImagingStudy":
case "Immunization":
case "ImmunizationEvaluation":
case "ImmunizationRecommendation":
case "Invoice":
case "List":
case "MeasureReport":
case "Media":
case "MedicationAdministration":
case "MedicationDispense":
case "MedicationRequest":
case "MedicationStatement":
case "MolecularSequence":
case "NutritionOrder":
case "Observation":
case "Patient":
case "Person":
case "Procedure":
case "Provenance":
case "QuestionnaireResponse":
case "RelatedPerson":
case "RequestGroup":
case "ResearchSubject":
case "RiskAssessment":
case "Schedule":
case "ServiceRequest":
case "Specimen":
case "SupplyDelivery":
case "SupplyRequest":
case "VisionPrescription":
return true;
default:
return false;
}
}
public String getPatientSearchParam(String theDataType) {
switch (theDataType) {
case "Account":
return "subject";
case "AdverseEvent":
return "subject";
case "AllergyIntolerance":
return "patient";
case "Appointment":
return "actor";
case "AppointmentResponse":
return "actor";
case "AuditEvent":
return "patient";
case "Basic":
return "patient";
case "BodyStructure":
return "patient";
case "CarePlan":
return "patient";
case "CareTeam":
return "patient";
case "ChargeItem":
return "subject";
case "Claim":
return "patient";
case "ClaimResponse":
return "patient";
case "ClinicalImpression":
return "subject";
case "Communication":
return "subject";
case "CommunicationRequest":
return "subject";
case "Composition":
return "subject";
case "Condition":
return "patient";
case "Consent":
return "patient";
case "Coverage":
return "policy-holder";
case "DetectedIssue":
return "patient";
case "DeviceRequest":
return "subject";
case "DeviceUseStatement":
return "subject";
case "DiagnosticReport":
return "subject";
case "DocumentManifest":
return "subject";
case "DocumentReference":
return "subject";
case "Encounter":
return "patient";
case "EnrollmentRequest":
return "subject";
case "EpisodeOfCare":
return "patient";
case "ExplanationOfBenefit":
return "patient";
case "FamilyMemberHistory":
return "patient";
case "Flag":
return "patient";
case "Goal":
return "patient";
case "Group":
return "member";
case "ImagingStudy":
return "patient";
case "Immunization":
return "patient";
case "ImmunizationRecommendation":
return "patient";
case "Invoice":
return "subject";
case "List":
return "subject";
case "MeasureReport":
return "patient";
case "Media":
return "subject";
case "MedicationAdministration":
return "patient";
case "MedicationDispense":
return "patient";
case "MedicationRequest":
return "subject";
case "MedicationStatement":
return "subject";
case "MolecularSequence":
return "patient";
case "NutritionOrder":
return "patient";
case "Observation":
return "subject";
case "Patient":
return "_id";
case "Person":
return "patient";
case "Procedure":
return "patient";
case "Provenance":
return "patient";
case "QuestionnaireResponse":
return "subject";
case "RelatedPerson":
return "patient";
case "RequestGroup":
return "subject";
case "ResearchSubject":
return "individual";
case "RiskAssessment":
return "subject";
case "Schedule":
return "actor";
case "ServiceRequest":
return "patient";
case "Specimen":
return "subject";
case "SupplyDelivery":
return "patient";
case "SupplyRequest":
return "subject";
case "VisionPrescription":
return "patient";
}
return null;
}
}

View File

@ -0,0 +1,30 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
public interface ICrDiscoveryElement {
CdsServiceJson getCdsServiceJson();
default String getKey(int itemNo) {
return "item" + Integer.toString(itemNo);
}
}

View File

@ -0,0 +1,26 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
public interface ICrDiscoveryService {
CdsServiceJson resolveService();
}

View File

@ -0,0 +1,24 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
public interface ICrDiscoveryServiceFactory {
ICrDiscoveryService create(String theServiceId);
}

View File

@ -0,0 +1,45 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
public class PrefetchUrlList extends CopyOnWriteArrayList<String> {
@Override
public boolean add(String theElement) {
for (String s : this) {
if (s.equals(theElement)) return false;
if (theElement.startsWith(s)) return false;
}
return super.add(theElement);
}
@Override
public boolean addAll(Collection<? extends String> theAdd) {
if (theAdd != null) {
for (String s : theAdd) {
add(s);
}
}
return true;
}
}

View File

@ -27,6 +27,7 @@ import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsCrServiceMethod;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -56,6 +57,11 @@ public class CdsPrefetchSvc {
public void augmentRequest(CdsServiceRequestJson theCdsServiceRequestJson, ICdsServiceMethod theServiceMethod) {
CdsServiceJson serviceSpec = theServiceMethod.getCdsServiceJson();
if (theServiceMethod instanceof CdsCrServiceMethod) {
// CdsCrServices will retrieve data from the dao or fhir server passed in as needed,
// checking for missing prefetch is not necessary.
return;
}
Set<String> missingPrefetch = findMissingPrefetch(serviceSpec, theCdsServiceRequestJson);
if (missingPrefetch.isEmpty()) {
return;

View File

@ -0,0 +1,17 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr;
import ca.uhn.fhir.context.FhirContext;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestCrConfig.class})
public abstract class BaseCrTest {
public static final String PLAN_DEFINITION_RESOURCE_NAME = "PlanDefinition";
protected static final String TEST_ADDRESS = "http://test:8000/fhir";
@Autowired
protected FhirContext myFhirContext;
}

View File

@ -0,0 +1,14 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestCrConfig {
@Bean
FhirContext fhirContext() {
return FhirContext.forR4Cached();
}
}

View File

@ -0,0 +1,44 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.BaseCrTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CrDiscoveryServiceR4Test extends BaseCrTest {
@Test
public void testR4DiscoveryService() throws JsonProcessingException {
Bundle bundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-ASLPCrd-Content.json");
Repository repository = new InMemoryFhirRepository(myFhirContext, bundle);
final IdType planDefinitionId = new IdType(PLAN_DEFINITION_RESOURCE_NAME, "ASLPCrd");
final CdsServiceJson cdsServiceJson = new CrDiscoveryServiceR4(planDefinitionId, repository).resolveService();
final ObjectMapper objectMapper = new CdsHooksObjectMapperFactory(myFhirContext).newMapper();
// execute
final String actual = objectMapper.writeValueAsString(cdsServiceJson);
final String expected = "{\n" +
" \"hook\" : \"order-sign\",\n" +
" \"title\" : \"ASLPCrd Workflow\",\n" +
" \"description\" : \"An example workflow for the CRD step of DaVinci Burden Reduction.\",\n" +
" \"id\" : \"ASLPCrd\",\n" +
" \"prefetch\" : {\n" +
" \"item1\" : \"Patient?_id=Patient/{{context.patientId}}\",\n" +
" \"item2\" : \"ServiceRequest?patient=Patient/{{context.patientId}}\",\n" +
" \"item3\" : \"Condition?patient=Patient/{{context.patientId}}&code=http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes|ASLP.A1.DE19\",\n" +
" \"item4\" : \"Condition?patient=Patient/{{context.patientId}}&code=http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes|ASLP.A1.DE18\",\n" +
" \"item5\" : \"Observation?subject=Patient/{{context.patientId}}&code=http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes|ASLP.A1.DE19\"\n" +
" }\n" +
"}";
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,78 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr.resolution;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.BaseCrTest;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.CdsCrServiceR4;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.codesystems.ActionType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CdsCrServiceR4Test extends BaseCrTest {
private ObjectMapper myObjectMapper;@BeforeEach
public void loadJson() throws IOException {
myObjectMapper = new CdsHooksObjectMapperFactory(myFhirContext).newMapper();
}
@Test
public void testR4Params() throws IOException {
final String rawRequest = ClasspathUtil.loadResource("ASLPCrdServiceRequest.json");
final CdsServiceRequestJson cdsServiceRequestJson = myObjectMapper.readValue(rawRequest, CdsServiceRequestJson.class);
final Bundle bundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-ASLPCrd-Content.json");
final Repository repository = new InMemoryFhirRepository(myFhirContext, bundle);
final RequestDetails requestDetails = new SystemRequestDetails();
final IdType planDefinitionId = new IdType(PLAN_DEFINITION_RESOURCE_NAME, "ASLPCrd");
requestDetails.setId(planDefinitionId);
final Parameters params = new CdsCrServiceR4(requestDetails, repository).encodeParams(cdsServiceRequestJson);
assertTrue(params.getParameter().size() == 3);
assertTrue(params.getParameter("parameters").hasResource());
}
@Test
public void testR4Response() {
final Bundle bundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-ASLPCrd-Content.json");
final Repository repository = new InMemoryFhirRepository(myFhirContext, bundle);
final Bundle responseBundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-ASLPCrd-Response.json");
final RequestDetails requestDetails = new SystemRequestDetails();
final IdType planDefinitionId = new IdType(PLAN_DEFINITION_RESOURCE_NAME, "ASLPCrd");
requestDetails.setId(planDefinitionId);
final CdsServiceResponseJson cdsServiceResponseJson = new CdsCrServiceR4(requestDetails, repository).encodeResponse(responseBundle);
assertTrue(cdsServiceResponseJson.getCards().size() == 1);
assertTrue(!cdsServiceResponseJson.getCards().get(0).getSummary().isEmpty());
assertTrue(!cdsServiceResponseJson.getCards().get(0).getDetail().isEmpty());
}
@Test
@Disabled // Disabled until the CDS on FHIR specification details how to map system actions.
public void testSystemActionResponse() {
final Bundle bundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-DischargeInstructionsPlan-Content.json");
final Repository repository = new InMemoryFhirRepository(myFhirContext, bundle);
final Bundle responseBundle = ClasspathUtil.loadResource(myFhirContext, Bundle.class, "Bundle-DischargeInstructionsPlan-Response.json");
final RequestDetails requestDetails = new SystemRequestDetails();
final IdType planDefinitionId = new IdType(PLAN_DEFINITION_RESOURCE_NAME, "DischargeInstructionsPlan");
requestDetails.setId(planDefinitionId);
final CdsServiceResponseJson cdsServiceResponseJson = new CdsCrServiceR4(requestDetails, repository).encodeResponse(responseBundle);
assertTrue(cdsServiceResponseJson.getServiceActions().size() == 1);
assertTrue(cdsServiceResponseJson.getServiceActions().get(0).getType().equals(ActionType.CREATE.toCode()));
assertNotNull(cdsServiceResponseJson.getServiceActions().get(0).getResource());
}
}

View File

@ -0,0 +1,87 @@
{
"hook" : "order-sign",
"hookInstance": "randomGUIDforthehookevent",
"fhirServer" : "https://localhost:8000",
"context" : {
"patientId" : "Patient/123",
"draftOrders" : {
"resourceType": "Bundle",
"entry": [
{
"resource": {
"resourceType": "ServiceRequest",
"id": "SleepStudy",
"meta": {
"profile": [
"http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order",
"http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-servicerequest"
]
},
"status": "draft",
"intent": "order",
"code": {
"coding": [
{
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE2",
"display": "Home sleep apnea testing (HSAT)"
}
],
"text": "Home sleep apnea testing (HSAT)"
},
"subject": {
"reference": "Patient/positive"
},
"authoredOn": "2023-04-06",
"reasonReference": [
{
"reference": "Condition/SleepApnea"
}
],
"occurrenceDateTime": "2023-04-10T08:00:00.000Z",
"requester": {
"reference": "Practitioner/Practitioner-positive"
}
}
},
{
"resource": {
"resourceType": "ServiceRequest",
"id": "SleepStudy2",
"meta": {
"profile": [
"http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order",
"http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-servicerequest"
]
},
"status": "draft",
"intent": "order",
"code": {
"coding": [
{
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE14",
"display": "Artificial intelligence (AI)"
}
],
"text": "Artificial intelligence (AI)"
},
"subject": {
"reference": "Patient/positive"
},
"authoredOn": "2023-04-06",
"reasonReference": [
{
"reference": "Condition/SleepApnea"
}
],
"occurrenceDateTime": "2023-04-15T08:00:00.000Z",
"requester": {
"reference": "Practitioner/Practitioner-positive"
}
}
}
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,388 @@
{
"resourceType": "Bundle",
"id": "ASLPCrd",
"type": "collection",
"entry": [
{
"resource": {
"resourceType": "RequestGroup",
"id": "ASLPCrd",
"instantiatesCanonical": [
"http://example.org/sdh/dtr/aslp/PlanDefinition/ASLPCrd|1.0.0"
],
"status": "draft",
"intent": "proposal",
"subject": {
"reference": "positive"
},
"action": [
{
"extension": [
{
"url": null,
"valueString": "warning"
}
],
"title": "Patient requires prior authoriztion for a sleep study",
"description": "Patient requires prior authorization due to: history of diabetes. Please open your DTR application and complete Questionniare",
"resource": {
"reference": "Task/ASLPCrd"
}
}
]
}
},
{
"resource": {
"resourceType": "Task",
"id": "ASLPCrd",
"meta": {
"versionId": "1"
},
"extension": [
{
"url": "http://hl7.org/fhir/aphl/StructureDefinition/condition",
"valueExpression": {
"language": "text/cql-identifier",
"expression": "Is Prior Auth Required"
}
},
{
"url": "http://hl7.org/fhir/aphl/StructureDefinition/input",
"valueDataRequirement": {
"type": "ServiceRequest",
"profile": [
"http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order"
]
}
}
],
"basedOn": [
{
"reference": "RequestGroup/ASLPCrd",
"type": "RequestGroup"
}
],
"status": "draft",
"intent": "proposal",
"code": {
"coding": [
{
"system": "http://hl7.org/fhir/uv/cpg/CodeSystem/cpg-activity-type",
"code": "collect-information",
"display": "Collect Information"
}
]
},
"for": {
"reference": "positive"
},
"input": [
{
"type": {
"coding": [
{
"code": "collect-information"
}
]
},
"valueCanonical": "http://example.org/sdh/dtr/aslp/Questionnaire/ASLPA1"
},
{
"type": {
"coding": [
{
"code": "collect-information"
}
]
},
"valueCanonical": "http://example.org/sdh/dtr/aslp/Questionnaire/ASLPA2"
}
]
}
},
{
"resource": {
"resourceType": "Questionnaire",
"id": "ASLPCrd",
"item": [
{
"linkId": "1",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order",
"text": "ASLP Sleep Study Order",
"type": "group",
"item": [
{
"linkId": "1.1",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order#ServiceRequest.code",
"text": "Sleep Study",
"type": "choice",
"required": true,
"answerOption": [
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE2",
"display": "Home sleep apnea testing (HSAT)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE3",
"display": "Peripheral artery tonometry (PAT)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE4",
"display": "Actigraphy"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE5",
"display": "Prescreening devices or procedures"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE6",
"display": "Acoustic pharyngometry"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE7",
"display": "Digital therapeutics"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE8",
"display": "Home oximetry monitoring"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE9",
"display": "Polysomnogram"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE10",
"display": "Facility-based positive airway pressure (PAP) titration study"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE11",
"display": "Facility-based, daytime, abbreviated, cardiorespiratory sleep studies (PAP NAP testing)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE12",
"display": "Multiple sleep latency test (MSLT)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE13",
"display": "Maintenance of wakefulness test (MWT)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE14",
"display": "Artificial intelligence (AI)"
}
}
],
"initial": [
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE2",
"display": "Home sleep apnea testing (HSAT)"
}
},
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE14",
"display": "Artificial intelligence (AI)"
}
}
]
},
{
"linkId": "1.2",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order#ServiceRequest.occurrence[x]",
"text": "Sleep Study Date",
"type": "dateTime",
"required": true,
"initial": [
{
"valueDateTime": "2023-04-10T08:00:00.000Z"
},
{
"valueDateTime": "2023-04-15T08:00:00.000Z"
}
]
}
]
},
{
"linkId": "2",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-diagnosis-of-obstructive-sleep-apnea",
"text": "ASLP Diagnosis of Obstructive Sleep Apnea",
"type": "group",
"item": [
{
"linkId": "2.1",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-diagnosis-of-obstructive-sleep-apnea#Condition.code",
"text": "Diagnosis of Obstructive Sleep Apnea",
"type": "choice",
"required": true,
"answerOption": [
{
"valueCoding": {
"system": "http://example.org/sdh/dtr/aslp/CodeSystem/aslp-codes",
"code": "ASLP.A1.DE17",
"display": "Obstructive sleep apnea (OSA)"
}
}
]
}
]
},
{
"linkId": "3",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-history-of-hypertension",
"text": "ASLP History of Hypertension",
"type": "group",
"item": [
{
"linkId": "3.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "3.2",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-history-of-hypertension#Observation.value[x]",
"text": "History of Hypertension",
"type": "boolean",
"required": true
}
]
},
{
"linkId": "4",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-history-of-diabetes",
"text": "ASLP History of Diabetes",
"type": "group",
"item": [
{
"linkId": "4.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "4.2",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-history-of-diabetes#Observation.value[x]",
"text": "History of Diabetes",
"type": "boolean",
"required": true
}
]
},
{
"linkId": "5",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-neck-circumference",
"text": "ASLP Neck Circumference",
"type": "group",
"item": [
{
"linkId": "5.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "5.2",
"text": "An error occurred during item creation: Unknown QuestionnaireItemType code 'Quantity'",
"type": "display"
}
]
},
{
"linkId": "6",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-height",
"text": "ASLP Height",
"type": "group",
"item": [
{
"linkId": "6.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "6.2",
"text": "An error occurred during item creation: Unknown QuestionnaireItemType code 'Quantity'",
"type": "display"
}
]
},
{
"linkId": "7",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-weight",
"text": "ASLP Weight",
"type": "group",
"item": [
{
"linkId": "7.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "7.2",
"text": "An error occurred during item creation: Unknown QuestionnaireItemType code 'Quantity'",
"type": "display"
}
]
},
{
"linkId": "8",
"definition": "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-bmi",
"text": "ASLP BMI",
"type": "group",
"item": [
{
"linkId": "8.1",
"text": "An error occurred during item creation: null",
"type": "display"
},
{
"linkId": "8.2",
"text": "An error occurred during item creation: Unknown QuestionnaireItemType code 'Quantity'",
"type": "display"
}
]
}
]
}
}
]
}

View File

@ -0,0 +1,161 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "ActivityDefinition/SendMessageActivity",
"resource": {
"resourceType": "ActivityDefinition",
"id": "SendMessageActivity",
"meta": {
"profile": [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationactivity"
]
},
"kind": "CommunicationRequest",
"profile": "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationrequest",
"intent": "proposal",
"extension": [
{
"url": "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability",
"valueCode": "publishable"
},
{
"url": "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel",
"valueCode": "structured"
}
],
"url": "http://example.org/ActivityDefinition/SendMessageActivity",
"name": "SendMessageActivity",
"title": "ActivityDefinition SendMessageActivity",
"status": "draft",
"experimental": true,
"publisher": "Example",
"jurisdiction": [
{
"coding": [
{
"code": "001",
"system": "http://unstats.un.org/unsd/methods/m49/m49.htm",
"display": "World"
}
]
}
],
"version": "0.1.0",
"description": "Example Activity Definition for a recommendation to send a message",
"code": {
"coding": [
{
"code": "send-message",
"system": "http://hl7.org/fhir/uv/cpg/CodeSystem/cpg-activity-type",
"display": "Send a message"
}
]
},
"doNotPerform": false,
"dynamicValue": [
{
"path": "payload[0].contentString",
"expression": {
"language": "text/fhirpath",
"expression": "'Greeting: Hello! ' + %subject.name.given.first() + ' Message: Example Activity Definition for a recommendation to send a message Practitioner: ' + %practitioner.name.given.first()"
}
}
]
},
"request": {
"method": "PUT",
"url": "ActivityDefinition/SendMessageActivity"
}
},
{
"fullUrl": "PlanDefinition/DischargeInstructionsPlan",
"resource": {
"resourceType": "PlanDefinition",
"id": "DischargeInstructionsPlan",
"meta": {
"profile": [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-computableplandefinition"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability",
"valueCode": "publishable"
},
{
"url": "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel",
"valueCode": "structured"
}
],
"url": "http://example.org/PlanDefinition/DischargeInstructionsPlan",
"name": "DischargeInstructionsPlan",
"title": "PlanDefinition DischargeInstructionsPlan",
"status": "draft",
"experimental": true,
"publisher": "Example",
"jurisdiction": [
{
"coding": [
{
"code": "001",
"system": "http://unstats.un.org/unsd/methods/m49/m49.htm",
"display": "World"
}
]
}
],
"version": "0.1.0",
"description": "Provide patient discharge instructions",
"type": {
"coding": [
{
"code": "clinical-protocol",
"system": "http://terminology.hl7.org/CodeSystem/plan-definition-type",
"display": "Clinical Protocol"
}
]
},
"action": [
{
"title": "Send message with discharge instructions",
"code": [
{
"coding": [
{
"code": "provide-counseling",
"system": "http://hl7.org/fhir/uv/cpg/CodeSystem/cpg-common-process",
"display": "Provide Counseling"
}
]
}
],
"type": {
"coding": [
{
"code": "create",
"system": "http://terminology.hl7.org/CodeSystem/action-type"
}
]
},
"dynamicValue": [
{
"path": "payload[0].contentString",
"expression": {
"language": "text/fhirpath",
"expression": "'Provide patient discharge instructions for ' + %subject.name.given.first()"
}
}
],
"definitionCanonical": "http://example.org/ActivityDefinition/SendMessageActivity"
}
]
},
"request": {
"method": "PUT",
"url": "PlanDefinition/DischargeInstructionsPlan"
}
}
]
}

View File

@ -0,0 +1,87 @@
{
"resourceType": "Bundle",
"id": "DischargeInstructionsPlan",
"type": "collection",
"entry": [
{
"resource": {
"resourceType": "RequestGroup",
"id": "DischargeInstructionsPlan",
"meta": {
"profile": [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-strategy"
]
},
"instantiatesCanonical": [
"http://example.org/PlanDefinition/DischargeInstructionsPlan|0.1.0"
],
"status": "draft",
"intent": "proposal",
"subject": {
"reference": "Patient/Patient1"
},
"encounter": {
"reference": "Encounter/Encounter1"
},
"author": {
"reference": "Practitioner/Practitioner1"
},
"action": [
{
"title": "Send message with discharge instructions",
"code": [
{
"coding": [
{
"system": "http://hl7.org/fhir/uv/cpg/CodeSystem/cpg-common-process",
"code": "provide-counseling",
"display": "Provide Counseling"
}
]
}
],
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/action-type",
"code": "create"
}
]
},
"resource": {
"reference": "CommunicationRequest/SendMessageActivity"
}
}
]
}
},
{
"resource": {
"resourceType": "CommunicationRequest",
"id": "SendMessageActivity",
"meta": {
"versionId": "2",
"profile": [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationrequest"
]
},
"status": "draft",
"doNotPerform": false,
"subject": {
"reference": "Patient/Patient1"
},
"encounter": {
"reference": "Encounter/Encounter1"
},
"payload": [
{
"contentString": "Provide patient discharge instructions for Alice"
}
],
"requester": {
"reference": "Practitioner/Practitioner1"
}
}
}
]
}

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.ElementsSupportEnum;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableListMultimap;
@ -72,6 +73,7 @@ public class SystemRequestDetails extends RequestDetails {
super(theDetails);
if (nonNull(theDetails.getServer())) {
myServer = theDetails.getServer();
myFhirContext = theDetails.getFhirContext();
}
}
@ -152,6 +154,10 @@ public class SystemRequestDetails extends RequestDetails {
return myServer;
}
public void setServer(RestfulServer theServer) {
this.myServer = theServer;
}
@Override
public String getServerBaseForRequest() {
return null;

View File

@ -0,0 +1,74 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.rest.server.BaseRestfulResponse;
import ca.uhn.fhir.util.IoUtil;
import org.apache.commons.lang3.Validate;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* A default RestfulResponse that returns the body as an IBaseResource and ignores everything else.
*/
public class SystemRestfulResponse extends BaseRestfulResponse<SystemRequestDetails> {
private Writer myWriter;
private ByteArrayOutputStream myOutputStream;
public SystemRestfulResponse(SystemRequestDetails theSystemRequestDetails) {
super(theSystemRequestDetails);
}
@Nonnull
@Override
public Writer getResponseWriter(int theStatusCode, String theContentType, String theCharset, boolean theRespondGzip)
throws IOException {
Validate.isTrue(myWriter == null, "getResponseWriter() called multiple times");
Validate.isTrue(myOutputStream == null, "getResponseWriter() called after getResponseOutputStream()");
myWriter = new StringWriter();
return myWriter;
}
@Nonnull
@Override
public OutputStream getResponseOutputStream(
int theStatusCode, String theContentType, @Nullable Integer theContentLength) throws IOException {
Validate.isTrue(myWriter == null, "getResponseOutputStream() called multiple times");
Validate.isTrue(myOutputStream == null, "getResponseOutputStream() called after getResponseWriter()");
myOutputStream = new ByteArrayOutputStream();
return myOutputStream;
}
@Override
public Object commitResponse(@Nonnull Closeable theWriterOrOutputStream) throws IOException {
IoUtil.closeQuietly(theWriterOrOutputStream);
return getRequestDetails().getServer().getFhirContext().newJsonParser().parseResource(myWriter.toString());
}
}

View File

@ -139,6 +139,12 @@
<version>${spring-security-core.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring_boot_version}</version>
</dependency>
<!-- This is needed for the CqlExceptionHandlingInterceptor -->
<dependency>
<groupId>javax.servlet</groupId>

View File

@ -23,10 +23,12 @@ import ca.uhn.fhir.cr.common.IRepositoryFactory;
import ca.uhn.fhir.cr.repo.HapiFhirRepository;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnBean(RestfulServer.class)
public class RepositoryConfig {
@Bean
IRepositoryFactory repositoryFactory(DaoRegistry theDaoRegistry, RestfulServer theRestfulServer) {

View File

@ -26,14 +26,17 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class ApplyOperationConfig {
@Bean
ca.uhn.fhir.cr.dstu3.IActivityDefinitionProcessorFactory dstu3ActivityDefinitionProcessorFactory(
IRepositoryFactory theRepositoryFactory, EvaluationSettings theEvaluationSettings) {
@ -61,7 +64,6 @@ public class ApplyOperationConfig {
@Bean(name = "applyOperationLoader")
public ProviderLoader applyOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class ExtractOperationConfig {
@Bean
ca.uhn.fhir.cr.dstu3.IQuestionnaireResponseProcessorFactory dstu3QuestionnaireResponseProcessorFactory(
@ -49,7 +53,6 @@ public class ExtractOperationConfig {
@Bean(name = "extractOperationLoader")
public ProviderLoader extractOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class PackageOperationConfig {
@Bean
ca.uhn.fhir.cr.dstu3.IPlanDefinitionProcessorFactory dstu3PlanDefinitionProcessorFactory(
@ -60,7 +64,6 @@ public class PackageOperationConfig {
@Bean(name = "packageOperationLoader")
public ProviderLoader packageOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class PopulateOperationConfig {
@Bean
ca.uhn.fhir.cr.dstu3.IQuestionnaireProcessorFactory dstu3QuestionnaireProcessorFactory(
@ -48,7 +52,6 @@ public class PopulateOperationConfig {
@Bean(name = "populateOperationLoader")
public ProviderLoader populateOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,14 +26,17 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class ApplyOperationConfig {
@Bean
ca.uhn.fhir.cr.r4.IActivityDefinitionProcessorFactory r4ActivityDefinitionProcessorFactory(
IRepositoryFactory theRepositoryFactory, EvaluationSettings theEvaluationSettings) {
@ -61,7 +64,6 @@ public class ApplyOperationConfig {
@Bean(name = "applyOperationLoader")
public ProviderLoader applyOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class ExtractOperationConfig {
@Bean
ca.uhn.fhir.cr.r4.IQuestionnaireResponseProcessorFactory r4QuestionnaireResponseProcessorFactory(
@ -49,7 +53,6 @@ public class ExtractOperationConfig {
@Bean(name = "extractOperationLoader")
public ProviderLoader extractOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class PackageOperationConfig {
@Bean
ca.uhn.fhir.cr.r4.IPlanDefinitionProcessorFactory r4PlanDefinitionProcessorFactory(
@ -60,7 +64,6 @@ public class PackageOperationConfig {
@Bean(name = "packageOperationLoader")
public ProviderLoader packageOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -26,12 +26,16 @@ import ca.uhn.fhir.cr.config.ProviderLoader;
import ca.uhn.fhir.cr.config.ProviderSelector;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.opencds.cqf.fhir.cql.EvaluationSettings;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
@Configuration
@ConditionalOnBean({IRepositoryFactory.class, RestfulServer.class, EvaluationSettings.class})
public class PopulateOperationConfig {
@Bean
ca.uhn.fhir.cr.r4.IQuestionnaireProcessorFactory r4QuestionnaireProcessorFactory(
@ -48,7 +52,6 @@ public class PopulateOperationConfig {
@Bean(name = "populateOperationLoader")
public ProviderLoader populateOperationLoader(
ApplicationContext theApplicationContext, FhirContext theFhirContext, RestfulServer theRestfulServer) {
var selector = new ProviderSelector(
theFhirContext,
Map.of(

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.cr.repo;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
@ -43,6 +44,7 @@ class RequestDetailsCloner {
newDetails.setParameters(new HashMap<>());
newDetails.setResourceName(null);
newDetails.setCompartmentName(null);
newDetails.setResponse(theDetails.getResponse());
return new DetailsBuilder(newDetails);
}
@ -65,7 +67,9 @@ class RequestDetailsCloner {
}
DetailsBuilder setParameters(IBaseParameters theParameters) {
myDetails.setResource(theParameters);
IParser parser = myDetails.getServer().getFhirContext().newJsonParser();
myDetails.setRequestContents(
parser.encodeResourceToString(theParameters).getBytes());
return this;
}

View File

@ -2,6 +2,10 @@ package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.IResourceLoader;
import ca.uhn.fhir.cr.config.r4.ApplyOperationConfig;
import ca.uhn.fhir.cr.config.r4.ExtractOperationConfig;
import ca.uhn.fhir.cr.config.r4.PackageOperationConfig;
import ca.uhn.fhir.cr.config.r4.PopulateOperationConfig;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -35,7 +39,13 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
@ContextConfiguration(classes = {TestCrR4Config.class})
@ContextConfiguration(classes = {
TestCrR4Config.class,
ApplyOperationConfig.class,
ExtractOperationConfig.class,
PackageOperationConfig.class,
PopulateOperationConfig.class
})
public abstract class BaseCrR4TestServer extends BaseJpaR4Test implements IResourceLoader {
public static IGenericClient ourClient;

View File

@ -31,12 +31,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
@Import({TestCrConfig.class, CrR4Config.class,
ApplyOperationConfig.class,
ExtractOperationConfig.class,
PackageOperationConfig.class,
PopulateOperationConfig.class
})
@Import({TestCrConfig.class, CrR4Config.class})
public class TestCrR4Config {
@Primary
@Bean