Compare commits

...

13 Commits

Author SHA1 Message Date
Brenin Rhodes 066f3717aa
Merge cdce4f4806 into 3f6d1eb29b 2024-09-26 10:43:19 -04:00
Thomas Papke 3f6d1eb29b
#5768 Upgrade to latest simple-java-mail (#6261) 2024-09-26 02:07:27 +00:00
Tadgh 377e44b6ca
attribution and pom change (#6309) 2024-09-25 20:38:22 +00:00
Martha Mitran 20d3e6bb25
Fix missing qualifier search for reference search parameters (#6306)
* Fix missing qualifier search for reference search parameter. Reuse tests and run them for both indexing enabled and disabled.

* Fix edge case where the search parameter has multiple paths. Add a test. Fix some of the compile warnings in a test class.

* Fix test setup.
2024-09-24 23:46:38 +00:00
Brenin Rhodes cdce4f4806 Merge branch 'master' into 5873-support-effective-data-reqs 2024-04-30 19:57:36 -06:00
Brenin Rhodes e81827fe9d cleanup 2024-04-25 09:04:50 -06:00
Brenin Rhodes 3cb57324be cleanup 2024-04-25 08:38:49 -06:00
Brenin Rhodes e8d49fedaf cleanup 2024-04-25 08:10:46 -06:00
Brenin Rhodes 4f48bce959 cleanup 2024-04-24 15:02:49 -06:00
Brenin Rhodes a881ce620c cleanup unused imports 2024-04-24 14:44:41 -06:00
Brenin Rhodes a49b133a39 Add to doc 2024-04-24 14:39:02 -06:00
Brenin Rhodes 3e762cced8 Add changelog 2024-04-24 14:32:05 -06:00
Brenin Rhodes 09eb56e2db Refactor prefetch generation out of discovery service. Add support for CRMI extensions. 2024-04-24 14:26:25 -06:00
27 changed files with 2450 additions and 1818 deletions

View File

@ -0,0 +1,7 @@
---
type: add
issue: 5873
title: "Added support for CDS on FHIR PlanDefinitions that use the CRMI Effective Data Requirements extension when
creating the JSON for the Service Discovery. Also added support for use of the FHIR Query Pattern extension when
generating Prefetch Templates for Service Discovery. If a DataRequirement has FHIR Query Pattern extensions they
will be used, otherwise Prefetch Templates will be generated."

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 6290
title: "Previously, a specific migration task was using the `TRIM()` function, which does not exist in MSSQL 2012. This was causing migrations targeting MSSQL 2012 to fail.
This has been corrected and replaced with usage of a combination of LTRIM() and RTRIM(). Thanks to Primož Delopst at Better for the contribution!"

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 6305
title: "Previously, when having StorageSettings#getIndexMissingFields() == IndexEnabledEnum.DISABLED (default value)
and attempting to search with the missing qualifier against a resource type with multiple search parameters of type reference,
the returned results would be incorrect. This has been fixed."

View File

@ -136,6 +136,8 @@ To create CDS Services from PlanDefinitions the dependencies for a FHIR Storage
Any PlanDefinition resource with an action that has a trigger of type [named-event](http://hl7.org/fhir/R4/codesystem-trigger-type.html#trigger-type-named-event) will have a CDS Service created using the PlanDefinition.id as the service id and the name of the trigger as the hook that the service is created for per the [CDS on FHIR Specification](https://hl7.org/fhir/clinicalreasoning-cds-on-fhir.html#surfacing-clinical-decision-support).
The [CRMI Effective Data Requirements](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/StructureDefinition-crmi-effectiveDataRequirements.html) extension, if present, will be used to determine the library to use when creating the prefetch templates. Otherwise, the primary library of the PlanDefinition will be used.
CDS Services created this way will show up as registered services and can be called just as other services are called. The CDS Service request will be converted into parameters for the [$apply operation](/docs/clinical_reasoning/plan_definitions.html#apply), the results of which are then converted into a CDS Response per the [CDS on FHIR Specification](https://hl7.org/fhir/clinicalreasoning-cds-on-fhir.html#consuming-decision-support).
These CDS Services will take advantage of the [Auto Prefetch](/docs/cds_hooks/#auto-prefetch) feature. Prefetch data is included as a Bundle in the `data` parameter of the $apply call.

View File

@ -800,14 +800,19 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im
subquery.addCustomColumns(1);
subquery.addFromTable(getTable());
String resourceType = theParams.getResourceTablePredicateBuilder().getResourceType();
RuntimeSearchParam paramDefinition =
mySearchParamRegistry.getRuntimeSearchParam(resourceType, theParams.getParamName());
List<String> pathList = paramDefinition.getPathsSplitForResourceType(resourceType);
Condition subQueryCondition = ComboCondition.and(
BinaryCondition.equalTo(
getResourceIdColumn(),
theParams.getResourceTablePredicateBuilder().getResourceIdColumn()),
BinaryCondition.equalTo(
getResourceTypeColumn(),
generatePlaceholder(
theParams.getResourceTablePredicateBuilder().getResourceType())));
BinaryCondition.equalTo(getResourceTypeColumn(), generatePlaceholder(resourceType)),
ComboCondition.or(pathList.stream()
.map(path -> BinaryCondition.equalTo(getColumnSourcePath(), generatePlaceholder(path)))
.toArray(BinaryCondition[]::new)));
subquery.addCondition(subQueryCondition);

View File

@ -70,6 +70,11 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<optional>true</optional>
</dependency>
<!-- test dependencies -->
<dependency>

View File

@ -28,8 +28,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

View File

@ -17,8 +17,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

View File

@ -217,7 +217,7 @@ import static org.mockito.Mockito.when;
public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4Test.class);
private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw;
private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor();
private final CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor();
@Autowired
private ISearchDao mySearchEntityDao;
@ -413,15 +413,15 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient pt1 = new Patient();
pt1.addName().setFamily("Elizabeth");
String pt1id = myPatientDao.create(pt1).getId().toUnqualifiedVersionless().getValue();
String pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless().getValue();
Patient pt2 = new Patient();
pt2.addName().setFamily("fghijk");
String pt2id = myPatientDao.create(pt2).getId().toUnqualifiedVersionless().getValue();
String pt2id = myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless().getValue();
Patient pt3 = new Patient();
pt3.addName().setFamily("zzzzz");
myPatientDao.create(pt3).getId().toUnqualifiedVersionless().getValue();
myPatientDao.create(pt3, mySrd).getId().toUnqualifiedVersionless().getValue();
Bundle output = myClient
@ -450,7 +450,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient pt1 = new Patient();
pt1.addName().setFamily("Smith%");
String pt1id = myPatientDao.create(pt1).getId().toUnqualifiedVersionless().getValue();
String pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless().getValue();
Bundle output = myClient
.search()
@ -463,7 +463,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient pt2 = new Patient();
pt2.addName().setFamily("Sm%ith");
String pt2id = myPatientDao.create(pt2).getId().toUnqualifiedVersionless().getValue();
String pt2id = myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless().getValue();
output = myClient
.search()
@ -740,7 +740,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient p = new Patient();
p.addName().setFamily("FAM").addGiven("GIV");
IIdType id = myPatientDao.create(p).getId();
IIdType id = myPatientDao.create(p, mySrd).getId();
myClient.read().resource("Patient").withId(id.toUnqualifiedVersionless()).execute();
@ -763,7 +763,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient p = new Patient();
p.addName().setFamily("FAM").addGiven("GIV");
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
myClient
.delete()
@ -1025,57 +1025,58 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testCreateAndReadBackResourceWithContainedReferenceToContainer() {
myFhirContext.setParserErrorHandler(new StrictErrorHandler());
String input = "{\n" +
" \"resourceType\": \"Organization\",\n" +
" \"id\": \"1\",\n" +
" \"meta\": {\n" +
" \"tag\": [\n" +
" {\n" +
" \"system\": \"https://blah.org/deployment\",\n" +
" \"code\": \"e69414dd-b5c2-462d-bcfd-9d04d6b16596\",\n" +
" \"display\": \"DEPLOYMENT\"\n" +
" },\n" +
" {\n" +
" \"system\": \"https://blah.org/region\",\n" +
" \"code\": \"b47d7a5b-b159-4bed-a8f8-3258e6603adb\",\n" +
" \"display\": \"REGION\"\n" +
" },\n" +
" {\n" +
" \"system\": \"https://blah.org/provider\",\n" +
" \"code\": \"28c30004-0333-40cf-9e7f-3f9e080930bd\",\n" +
" \"display\": \"PROVIDER\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"contained\": [\n" +
" {\n" +
" \"resourceType\": \"Location\",\n" +
" \"id\": \"2\",\n" +
" \"position\": {\n" +
" \"longitude\": 51.443238301454289,\n" +
" \"latitude\": 7.34196905697293\n" +
" },\n" +
" \"managingOrganization\": {\n" +
" \"reference\": \"#\"\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"type\": [\n" +
" {\n" +
" \"coding\": [\n" +
" {\n" +
" \"system\": \"https://blah.org/fmc/OrganizationType\",\n" +
" \"code\": \"CLINIC\",\n" +
" \"display\": \"Clinic\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" ],\n" +
" \"name\": \"testOrg\"\n" +
"}";
String input = """
{
"resourceType": "Organization",
"id": "1",
"meta": {
"tag": [
{
"system": "https://blah.org/deployment",
"code": "e69414dd-b5c2-462d-bcfd-9d04d6b16596",
"display": "DEPLOYMENT"
},
{
"system": "https://blah.org/region",
"code": "b47d7a5b-b159-4bed-a8f8-3258e6603adb",
"display": "REGION"
},
{
"system": "https://blah.org/provider",
"code": "28c30004-0333-40cf-9e7f-3f9e080930bd",
"display": "PROVIDER"
}
]
},
"contained": [
{
"resourceType": "Location",
"id": "2",
"position": {
"longitude": 51.443238301454289,
"latitude": 7.34196905697293
},
"managingOrganization": {
"reference": "#"
}
}
],
"type": [
{
"coding": [
{
"system": "https://blah.org/fmc/OrganizationType",
"code": "CLINIC",
"display": "Clinic"
}
]
}
],
"name": "testOrg"
}""";
Organization org = myFhirContext.newJsonParser().parseResource(Organization.class, input);
IIdType id = myOrganizationDao.create(org).getId();
IIdType id = myOrganizationDao.create(org, mySrd).getId();
org = myOrganizationDao.read(id);
String output = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(org);
@ -1095,9 +1096,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
List<IBaseResource> outcome = myClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
runInTransaction(() -> {
assertEquals(100, myResourceTableDao.count());
});
runInTransaction(() -> assertEquals(100, myResourceTableDao.count()));
Bundle found = myClient
.search()
@ -1306,7 +1305,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testCreateQuestionnaireResponseWithValidation() throws IOException {
public void testCreateQuestionnaireResponseWithValidation() {
CodeSystem cs = new CodeSystem();
cs.setUrl("http://cs");
cs.setStatus(Enumerations.PublicationStatus.ACTIVE);
@ -1906,8 +1905,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
* Try it with a raw socket call. The Apache client won't let us use the unescaped "|" in the URL but we want to make sure that works too..
*/
Socket sock = new Socket();
sock.setSoTimeout(3000);
try {
try (sock) {
sock.setSoTimeout(3000);
sock.connect(new InetSocketAddress("localhost", myPort));
sock.getOutputStream().write(("DELETE /fhir/context/Patient?identifier=http://ghh.org/patient|" + methodName + " HTTP/1.1\n").getBytes(StandardCharsets.UTF_8));
sock.getOutputStream().write("Host: localhost\n".getBytes(StandardCharsets.UTF_8));
@ -1915,7 +1914,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
BufferedReader socketInput = new BufferedReader(new InputStreamReader(sock.getInputStream()));
// String response = "";
StringBuilder b = new StringBuilder();
char[] buf = new char[1000];
while (socketInput.read(buf) != -1) {
@ -1925,9 +1923,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.debug("Resp: {}", resp);
} catch (SocketTimeoutException e) {
e.printStackTrace();
} finally {
sock.close();
ourLog.debug(e.getMessage(), e);
}
Thread.sleep(1000);
@ -2398,7 +2394,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(idValues).as(idValues.toString()).hasSize(10);
idValues = searchAndReturnUnqualifiedIdValues(myServerBase + "/_history?_at=gt" + InstantDt.withCurrentTime().getYear());
assertThat(idValues).hasSize(0);
assertThat(idValues).isEmpty();
}
@ -2427,7 +2423,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myMemoryCacheService.invalidateCaches(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID);
}
Bundle history = myClient.history().onInstance(id.getValue()).andReturnBundle(Bundle.class).execute();
Bundle history = myClient.history().onInstance(id.getValue()).returnBundle(Bundle.class).execute();
assertEquals(1, history.getEntry().size());
BundleEntryComponent historyEntry0 = history.getEntry().get(0);
// validate entry.fullUrl
@ -2476,7 +2472,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myMemoryCacheService.invalidateCaches(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID);
}
Bundle history = myClient.history().onInstance(id.getValue()).andReturnBundle(Bundle.class).execute();
Bundle history = myClient.history().onInstance(id.getValue()).returnBundle(Bundle.class).execute();
assertEquals(1, history.getEntry().size());
BundleEntryComponent historyEntry0 = history.getEntry().get(0);
// validate entry.fullUrl
@ -2508,7 +2504,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info("Res ID: {}", id);
Bundle history = myClient.history().onInstance(id.getValue()).andReturnBundle(Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA).execute();
Bundle history = myClient.history().onInstance(id.getValue()).returnBundle(Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA).execute();
assertThat(history.getEntry()).hasSize(3);
assertEquals(id.withVersion("3").getValue(), history.getEntry().get(0).getResource().getId());
assertThat(((Patient) history.getEntry().get(0).getResource()).getName()).hasSize(1);
@ -2746,7 +2742,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
int total = 20;
Organization org = new Organization();
org.setName("ORG");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless();
Coding tagCode = new Coding();
tagCode.setCode("test");
@ -2757,7 +2753,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.addTag(tagCode);
t.setStatus(Task.TaskStatus.REQUESTED);
t.getOwner().setReference(orgId.getValue());
myTaskDao.create(t);
myTaskDao.create(t, mySrd);
}
HashSet<String> ids = new HashSet<>();
@ -2835,12 +2831,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
if (orgCount > 0) {
Organization org = new Organization();
org.setName("ORG");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless();
orgCount--;
t.getOwner().setReference(orgId.getValue());
}
myTaskDao.create(t);
myTaskDao.create(t, mySrd);
}
}
@ -2909,12 +2905,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
if (orgCount > 0) {
Organization org = new Organization();
org.setName("ORG");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless();
orgCount--;
t.getOwner().setReference(orgId.getValue());
}
myTaskDao.create(t);
myTaskDao.create(t, mySrd);
}
}
@ -2961,13 +2957,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testIncludeCountDoesntIncludeIncludes() {
Organization org = new Organization();
org.setName("ORG");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless();
for (int i = 0; i < 10; i++) {
Patient pt = new Patient();
pt.getManagingOrganization().setReference(orgId.getValue());
pt.addName().setFamily("FAM" + i);
myPatientDao.create(pt);
myPatientDao.create(pt, mySrd);
}
Bundle bundle = myClient
@ -3168,7 +3164,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient newPt = myClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
assertFalse(newPt.getActive());
}
@Test
@ -3196,7 +3192,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient newPt = myClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
assertTrue(newPt.getActive());
}
@Test
@ -3226,7 +3222,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient newPt = myClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
assertFalse(newPt.getActive());
}
@Test
@ -3255,7 +3251,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Patient newPt = myClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
assertFalse(newPt.getActive());
}
@Test
@ -3323,12 +3319,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
{
Bundle returned = myClient.search().forResource(Patient.class).encodedXml().returnBundle(Bundle.class).execute();
assertThat(returned.getEntry().size()).isGreaterThan(1);
assertThat(returned.getEntry()).hasSizeGreaterThan(1);
assertEquals(BundleType.SEARCHSET, returned.getType());
}
{
Bundle returned = myClient.search().forResource(Patient.class).encodedJson().returnBundle(Bundle.class).execute();
assertThat(returned.getEntry().size()).isGreaterThan(1);
assertThat(returned.getEntry()).hasSizeGreaterThan(1);
}
}
@ -3350,7 +3346,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
});
Bundle bundle = myClient.history().onServer().andReturnBundle(Bundle.class).execute();
Bundle bundle = myClient.history().onServer().returnBundle(Bundle.class).execute();
assertEquals(1, bundle.getTotal());
assertThat(bundle.getEntry()).hasSize(1);
assertEquals(id2.getIdPart(), bundle.getEntry().get(0).getResource().getIdElement().getIdPart());
@ -3507,15 +3503,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(text).doesNotContain("\"B\"");
assertThat(text).doesNotContain("\"B1\"");
}
// HttpGet read = new HttpGet(ourServerBase + "/Observation?patient=P5000000302&_sort:desc=code&code:in=http://fkcfhir.org/fhir/vs/ccdacapddialysisorder");
// try (CloseableHttpResponse response = ourHttpClient.execute(read)) {
// String text = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
// ourLog.info(text);
// assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatusLine().getStatusCode());
// assertThat(text).doesNotContain("\"text\",\"type\"");
// }
}
@Test
@ -3873,9 +3860,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
p.addName().setFamily(methodName + "1");
IIdType pid1 = myClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
Thread.sleep(10);
long time1 = System.currentTimeMillis();
Thread.sleep(10);
Patient p2 = new Patient();
p2.addName().setFamily(methodName + "2");
@ -4064,9 +4049,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
public void testSearchLastUpdatedParamRp() throws InterruptedException {
String methodName = "testSearchLastUpdatedParamRp";
int sleep = 100;
Thread.sleep(sleep);
DateTimeType beforeAny = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI);
IIdType id1a;
{
@ -4083,9 +4065,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
id1b = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
}
Thread.sleep(1100);
DateTimeType beforeR2 = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI);
Thread.sleep(1100);
IIdType id2;
{
@ -4249,13 +4229,12 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp);
matches = bundle.getEntry().size();
assertThat(matches).isGreaterThan(0);
assertThat(matches).isPositive();
}
@Test
public void testSearchReturnsSearchDate() throws Exception {
Date before = new Date();
Thread.sleep(1);
//@formatter:off
Bundle found = myClient
@ -4266,7 +4245,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.execute();
//@formatter:on
Thread.sleep(1);
Date after = new Date();
InstantType updated = found.getMeta().getLastUpdatedElement();
@ -4300,7 +4278,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4313,7 +4291,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4326,7 +4304,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4339,24 +4317,24 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
// > 1m
String uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt1|http://unitsofmeasure.org|m");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(3);
//>= 100cm
uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt100|http://unitsofmeasure.org|cm");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(3);
//>= 10dm
uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt10|http://unitsofmeasure.org|dm");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(3);
}
@ -4381,7 +4359,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4392,7 +4370,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4403,7 +4381,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4414,7 +4392,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
String uri;
@ -4451,7 +4429,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4462,7 +4440,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -4474,7 +4452,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
myCaptureQueriesListener.clear();
@ -4490,8 +4468,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
//-- check use normalized quantity table to search
String searchSql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, true);
assertThat(searchSql).doesNotContain("HFJ_SPIDX_QUANTITY t0");
assertThat(searchSql).contains("HFJ_SPIDX_QUANTITY_NRML");
assertThat(searchSql).doesNotContain("HFJ_SPIDX_QUANTITY t0").contains("HFJ_SPIDX_QUANTITY_NRML");
}
@Test
@ -5044,7 +5021,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5056,7 +5033,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid2 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5068,7 +5045,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid3 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5080,7 +5057,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid4 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
String uri = myServerBase + "/Observation?_sort=code-value-quantity";
@ -5092,7 +5069,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
found = myFhirContext.newXmlParser().parseResource(Bundle.class, output);
}
ourLog.debug("Bundle: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(found));
ourLog.debug("Bundle: {}\n", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(found));
List<IIdType> list = toUnqualifiedVersionlessIds(found);
assertThat(found.getEntry()).hasSize(4);
@ -5129,7 +5106,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5145,7 +5122,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid2 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5161,7 +5138,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
oid3 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -5176,7 +5153,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
comp.setValue(new Quantity().setValue(250));
oid4 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
String uri = myServerBase + "/Observation?_sort=combo-code-value-quantity";
@ -5188,7 +5165,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
found = myFhirContext.newXmlParser().parseResource(Bundle.class, output);
}
ourLog.debug("Bundle: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(found));
ourLog.debug("Bundle: {}\n", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(found));
List<IIdType> list = toUnqualifiedVersionlessIds(found);
assertThat(found.getEntry()).hasSize(4);
@ -5264,9 +5241,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
List<IIdType> list = toUnqualifiedVersionlessIds(found);
ourLog.info(methodName + " found: " + list.toString() + " - Wanted " + orgMissing + " but not " + orgNotMissing);
assertThat(list).doesNotContain(orgNotMissing);
assertThat(list).doesNotContain(deletedIdMissingTrue);
assertThat(list).contains(orgMissing);
assertThat(list).doesNotContain(orgNotMissing).doesNotContain(deletedIdMissingTrue).contains(orgMissing);
}
@Test
@ -5927,7 +5902,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
Date before = new Date();
Thread.sleep(100);
pt = new Patient();
pt.setId(id.getIdPart());
@ -6450,7 +6424,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
cc.addCoding().setCode("2345-7").setSystem("http://loinc.org");
obs.setValue(new Quantity().setValueElement(new DecimalType(125.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm"));
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
IIdType opid1 = myObservationDao.create(obs, mySrd).getId();
@ -6463,7 +6437,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
cc.addCoding().setCode("2345-7").setSystem("http://loinc.org");
obs.setValue(new Quantity().setValueElement(new DecimalType(24.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm"));
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
myObservationDao.update(obs, mySrd);
}
@ -6479,7 +6453,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -6492,7 +6466,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
{
@ -6505,25 +6479,25 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myObservationDao.create(obs, mySrd);
ourLog.debug("Observation: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
ourLog.debug("Observation: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
}
// > 1m
String uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt1|http://unitsofmeasure.org|m");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(2);
//>= 100cm
uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt100|http://unitsofmeasure.org|cm");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(2);
//>= 10dm
uri = myServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt10|http://unitsofmeasure.org|dm");
ourLog.info("uri = " + uri);
ourLog.info("uri = {}", uri);
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertThat(ids).hasSize(2);
}
@ -6540,7 +6514,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patient.setBirthDateElement(new DateType("2073"));
pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
ourLog.debug("Patient: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient));
ourLog.debug("Patient: {}\n", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient));
ourLog.info("pid0 " + pid0);
}
@ -6553,7 +6527,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp);
ourLog.debug("Patient: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
ourLog.debug("Patient: {}\n", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
}
uri = myServerBase + "/Patient?_total=accurate&birthdate=gt2072-01-01";
@ -6564,7 +6538,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp);
ourLog.debug("Patient: \n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
ourLog.debug("Patient: {}\n", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
}
}
@ -6995,9 +6969,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
&& theInput.IsEnforceRefOnType
&& theInput.IsEnforceRefOnWrite).isFalse();
} catch (InvalidRequestException ex) {
assertThat(ex.getMessage().contains(
"Invalid resource reference"
)).as(ex.getMessage()).isTrue();
assertThat(ex.getMessage()).as(ex.getMessage()).contains("Invalid resource reference");
} finally {
myStorageSettings.setEnforceReferentialIntegrityOnWrite(isEnforceRefOnWrite);
myStorageSettings.setEnforceReferenceTargetTypes(isEnforceRefTargetTypes);
@ -7331,9 +7303,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patient.setBirthDate(cal.getTime());
}
return patient;
}, (isMissing) -> {
return doSearch(Patient.class, Patient.BIRTHDATE.isMissing(isMissing));
});
}, (isMissing) -> doSearch(Patient.class, Patient.BIRTHDATE.isMissing(isMissing)));
}
@ParameterizedTest
@ -7346,9 +7316,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patient.setGender(AdministrativeGender.FEMALE);
}
return patient;
}, isMissing -> {
return doSearch(Patient.class, Patient.GENDER.isMissing(isMissing));
});
}, isMissing -> doSearch(Patient.class, Patient.GENDER.isMissing(isMissing)));
}
@ParameterizedTest
@ -7364,9 +7332,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patient.setGeneralPractitioner(Collections.singletonList(new Reference(practitionerId)));
}
return patient;
}, isMissing -> {
return doSearch(Patient.class, Patient.GENERAL_PRACTITIONER.isMissing(isMissing));
});
}, isMissing -> doSearch(Patient.class, Patient.GENERAL_PRACTITIONER.isMissing(isMissing)));
}
@ParameterizedTest
@ -7409,9 +7375,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
sp.setUrl("http://example.com");
}
return sp;
}, isMissing -> {
return doSearch(SearchParameter.class, SearchParameter.URL.isMissing(isMissing));
});
}, isMissing -> doSearch(SearchParameter.class, SearchParameter.URL.isMissing(isMissing)));
}
@ParameterizedTest
@ -7424,9 +7388,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
obs.setValue(new Quantity(3));
}
return obs;
}, isMissing -> {
return doSearch(Observation.class, Observation.VALUE_QUANTITY.isMissing(isMissing));
});
}, isMissing -> doSearch(Observation.class, Observation.VALUE_QUANTITY.isMissing(isMissing)));
}
@ParameterizedTest
@ -7457,7 +7419,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
Y doTask(X theInput);
}
private static class MissingSearchTestParameters {
public static class MissingSearchTestParameters {
/**
* The setting for IndexMissingFields
*/

View File

@ -79,25 +79,11 @@
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId>
<!-- Excluded in favor of jakarta.activation:jakarta.activation-api -->
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<scope>compile</scope>
<!-- Excluded in favor of jakarta.activation:jakarta.activation-api -->
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

View File

@ -20,10 +20,13 @@
package ca.uhn.hapi.fhir.cdshooks.svc.cr;
public class CdsCrConstants {
private CdsCrConstants() {}
public static final String CDS_CR_MODULE_ID = "CR";
public static final String CRMI_EFFECTIVE_DATA_REQUIREMENTS =
"http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-effectiveDataRequirements";
public static final String CQF_FHIR_QUERY_PATTERN = "http://hl7.org/fhir/StructureDefinition/cqf-fhirQueryPattern";
// CDS Hook field names
public static final String CDS_PARAMETER_USER_ID = "userId";
public static final String CDS_PARAMETER_PATIENT_ID = "patientId";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,9 +21,9 @@ package ca.uhn.fhir.rest.server.mail;
import jakarta.annotation.Nonnull;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.mailer.AsyncResponse;
import java.util.List;
import java.util.function.Consumer;
public interface IMailSvc {
void sendMail(@Nonnull List<Email> theEmails);
@ -31,7 +31,5 @@ public interface IMailSvc {
void sendMail(@Nonnull Email theEmail);
void sendMail(
@Nonnull Email theEmail,
@Nonnull Runnable theOnSuccess,
@Nonnull AsyncResponse.ExceptionConsumer theErrorHandler);
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler);
}

View File

@ -20,12 +20,9 @@
package ca.uhn.fhir.rest.server.mail;
import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.Validate;
import org.simplejavamail.MailException;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.email.Recipient;
import org.simplejavamail.api.mailer.AsyncResponse;
import org.simplejavamail.api.mailer.AsyncResponse.ExceptionConsumer;
import org.simplejavamail.api.mailer.Mailer;
import org.simplejavamail.api.mailer.config.TransportStrategy;
import org.simplejavamail.mailer.MailerBuilder;
@ -33,6 +30,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class MailSvc implements IMailSvc {
@ -42,14 +41,14 @@ public class MailSvc implements IMailSvc {
private final Mailer myMailer;
public MailSvc(@Nonnull MailConfig theMailConfig) {
Validate.notNull(theMailConfig);
Objects.requireNonNull(theMailConfig);
myMailConfig = theMailConfig;
myMailer = makeMailer(myMailConfig);
}
@Override
public void sendMail(@Nonnull List<Email> theEmails) {
Validate.notNull(theEmails);
Objects.requireNonNull(theEmails);
theEmails.forEach(theEmail -> send(theEmail, new OnSuccess(theEmail), new ErrorHandler(theEmail)));
}
@ -60,21 +59,23 @@ public class MailSvc implements IMailSvc {
@Override
public void sendMail(
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull ExceptionConsumer theErrorHandler) {
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler) {
send(theEmail, theOnSuccess, theErrorHandler);
}
private void send(
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull ExceptionConsumer theErrorHandler) {
Validate.notNull(theEmail);
Validate.notNull(theOnSuccess);
Validate.notNull(theErrorHandler);
@Nonnull Email theEmail, @Nonnull Runnable theOnSuccess, @Nonnull Consumer<Throwable> theErrorHandler) {
Objects.requireNonNull(theEmail);
Objects.requireNonNull(theOnSuccess);
Objects.requireNonNull(theErrorHandler);
try {
final AsyncResponse asyncResponse = myMailer.sendMail(theEmail, true);
if (asyncResponse != null) {
asyncResponse.onSuccess(theOnSuccess);
asyncResponse.onException(theErrorHandler);
}
myMailer.sendMail(theEmail, true).whenComplete((result, ex) -> {
if (ex != null) {
theErrorHandler.accept(ex);
} else {
theOnSuccess.run();
}
});
} catch (MailException e) {
theErrorHandler.accept(e);
}
@ -117,7 +118,7 @@ public class MailSvc implements IMailSvc {
}
}
private class ErrorHandler implements ExceptionConsumer {
private class ErrorHandler implements Consumer<Throwable> {
private final Email myEmail;
private ErrorHandler(@Nonnull Email theEmail) {
@ -125,7 +126,7 @@ public class MailSvc implements IMailSvc {
}
@Override
public void accept(Exception t) {
public void accept(Throwable t) {
ourLog.error("Email not sent" + makeMessage(myEmail), t);
}
}

View File

@ -4,6 +4,7 @@ import com.icegreen.greenmail.junit5.GreenMailExtension;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;
import jakarta.annotation.Nonnull;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -11,7 +12,6 @@ import org.simplejavamail.MailException;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.email.EmailBuilder;
import javax.mail.internet.MimeMessage;
import java.util.Arrays;
import java.util.List;
@ -86,13 +86,14 @@ public class MailSvcIT {
@Test
public void testSendMailWithInvalidToAddressExpectErrorHandler() {
// setup
final Email email = withEmail("xyz");
String invalidEmailAdress = "xyz";
final Email email = withEmail(invalidEmailAdress);
// execute
fixture.sendMail(email,
() -> fail("Should not execute on Success"),
(e) -> {
assertTrue(e instanceof MailException);
assertEquals("Invalid TO address: " + email, e.getMessage());
assertEquals("Invalid TO address: " + invalidEmailAdress, e.getMessage());
});
// validate
assertTrue(ourGreenMail.waitForIncomingEmail(1000, 0));

34
pom.xml
View File

@ -869,6 +869,7 @@
<developer>
<id>delopst</id>
<name>Primož Delopst</name>
<organization>Better</organization>
</developer>
<developer>
<id>Zach Smith</id>
@ -1160,27 +1161,38 @@
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId>
<version>6.6.1</version>
<version>8.11.2</version>
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<groupId>com.github.bbottema</groupId>
<artifactId>jetbrains-runtime-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>jakarta.activation</artifactId>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>2.1.0-rc-1</version>
<exclusions>
<exclusion>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail-junit5</artifactId>
<version>1.6.4</version>
<version>2.1.0-rc-1</version>
<scope>compile</scope>
</dependency>
<!-- mail end -->