From b977daae5895e6f6c2fd89726284aab71e0a48c3 Mon Sep 17 00:00:00 2001 From: c-schuler Date: Thu, 21 Jul 2022 09:10:53 -0600 Subject: [PATCH] #481: Fixed CDS Hooks service cache (#562) * #481: added interceptor to check update services cache when PlanDefinitions are inserted/updated/deleted ... added test * #481: added CDS Hooks services cache as a resource listener ... updated tests * Fixing tests --- .../cqf/ruler/cdshooks/CdsHooksConfig.java | 15 +- .../cqf/ruler/cdshooks/CdsServicesCache.java | 98 +++++ .../discovery/DiscoveryResolutionR4.java | 5 + .../discovery/DiscoveryResolutionStu3.java | 5 + .../ruler/cdshooks/dstu3/CdsHooksServlet.java | 21 +- .../ruler/cdshooks/r4/CdsHooksServlet.java | 26 +- .../plugin/cdshooks/ResourceChangeEvent.java | 44 ++ .../cdshooks/dstu3/CdsHooksServletIT.java | 30 +- .../plugin/cdshooks/r4/CdsHooksServletIT.java | 71 ++- .../resources/HelloWorld-plandefinition.json | 74 ++++ .../resources/Screening-plandefinition.json | 404 ++++++++++++++++++ 11 files changed, 728 insertions(+), 65 deletions(-) create mode 100644 plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsServicesCache.java create mode 100644 plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/ResourceChangeEvent.java create mode 100644 plugin/cds-hooks/src/test/resources/HelloWorld-plandefinition.json create mode 100644 plugin/cds-hooks/src/test/resources/Screening-plandefinition.json diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsHooksConfig.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsHooksConfig.java index 2612d411a..8d8202b62 100644 --- a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsHooksConfig.java +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsHooksConfig.java @@ -1,9 +1,8 @@ package org.opencds.cqf.ruler.cdshooks; -import java.util.concurrent.atomic.AtomicReference; - -import com.google.gson.JsonArray; - +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.opencds.cqf.ruler.cdshooks.providers.ProviderConfiguration; import org.opencds.cqf.ruler.cql.CqlProperties; import org.opencds.cqf.ruler.external.annotations.OnDSTU3Condition; @@ -33,9 +32,11 @@ public ProviderConfiguration providerConfiguration(CdsHooksProperties cdsPropert return new ProviderConfiguration(cdsProperties, cqlProperties); } - @Bean(name = "globalCdsServiceCache") - public AtomicReference cdsServiceCache() { - return new AtomicReference<>(); + @Bean + public CdsServicesCache cdsServiceInterceptor(IResourceChangeListenerRegistry resourceChangeListenerRegistry, DaoRegistry daoRegistry) { + CdsServicesCache listener = new CdsServicesCache(daoRegistry); + resourceChangeListenerRegistry.registerResourceResourceChangeListener("PlanDefinition", SearchParameterMap.newSynchronous(), listener, 1000); + return listener; } @Bean diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsServicesCache.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsServicesCache.java new file mode 100644 index 000000000..a513daac2 --- /dev/null +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/CdsServicesCache.java @@ -0,0 +1,98 @@ +package org.opencds.cqf.ruler.cdshooks; + +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.cache.IResourceChangeEvent; +import ca.uhn.fhir.jpa.cache.IResourceChangeListener; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.opencds.cqf.ruler.cdshooks.discovery.DiscoveryResolutionR4; +import org.opencds.cqf.ruler.cdshooks.discovery.DiscoveryResolutionStu3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class CdsServicesCache implements IResourceChangeListener { + private static final Logger logger = LoggerFactory.getLogger(CdsServicesCache.class); + + private AtomicReference cdsServiceCache; + private IFhirResourceDao planDefinitionDao; + private DiscoveryResolutionR4 discoveryResolutionR4; + private DiscoveryResolutionStu3 discoveryResolutionStu3; + + public CdsServicesCache(DaoRegistry daoRegistry) { + this.planDefinitionDao = daoRegistry.getResourceDao("PlanDefinition"); + this.discoveryResolutionR4 = new DiscoveryResolutionR4(daoRegistry); + this.discoveryResolutionStu3 = new DiscoveryResolutionStu3(daoRegistry); + this.cdsServiceCache = new AtomicReference<>(new JsonArray()); + } + + public AtomicReference getCdsServiceCache() { + return this.cdsServiceCache; + } + + public void clearCache() { + this.cdsServiceCache = new AtomicReference<>(new JsonArray()); + } + + @Override + public void handleInit(Collection collection) { + + } + + @Override + public void handleChange(IResourceChangeEvent iResourceChangeEvent) { + if (iResourceChangeEvent == null) return; + if (iResourceChangeEvent.getCreatedResourceIds() != null && !iResourceChangeEvent.getCreatedResourceIds().isEmpty()) { + insert(iResourceChangeEvent.getCreatedResourceIds()); + } + if (iResourceChangeEvent.getUpdatedResourceIds() != null && !iResourceChangeEvent.getUpdatedResourceIds().isEmpty()) { + update(iResourceChangeEvent.getUpdatedResourceIds()); + } + if (iResourceChangeEvent.getDeletedResourceIds() != null && !iResourceChangeEvent.getDeletedResourceIds().isEmpty()) { + delete(iResourceChangeEvent.getDeletedResourceIds()); + } + } + + private void insert(List createdIds) { + for (IIdType id : createdIds) { + try { + IBaseResource resource = planDefinitionDao.read(id); + if (resource instanceof PlanDefinition) { + cdsServiceCache.get().add(discoveryResolutionR4.resolveService((PlanDefinition) resource)); + } else if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { + cdsServiceCache.get().add(discoveryResolutionStu3.resolveService((org.hl7.fhir.dstu3.model.PlanDefinition) resource)); + } + } catch (Exception e) { + logger.info(String.format("Failed to create service for %s", id.getIdPart())); + } + } + } + + private void update(List updatedIds) { + try { + delete(updatedIds); + insert(updatedIds); + } catch (Exception e) { + logger.info(String.format("Failed to update service(s) for %s", updatedIds)); + } + } + + private void delete(List deletedIds) { + for (IIdType id : deletedIds) { + for (int i = 0; i < cdsServiceCache.get().size(); i++) { + if (((JsonObject) cdsServiceCache.get().get(i)).get("id").getAsString().equals(id.getIdPart())) { + cdsServiceCache.get().remove(i); + break; + } + else logger.info(String.format("Failed to delete service for %s", id.getIdPart())); + } + } + } +} diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionR4.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionR4.java index 4a4e6e1d7..55f4fe1df 100644 --- a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionR4.java +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionR4.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import com.google.gson.JsonObject; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DataRequirement; @@ -198,6 +199,10 @@ public DiscoveryResponse resolve() { return response; } + public JsonObject resolveService(PlanDefinition planDefinition) { + return new DiscoveryElementR4(planDefinition, getPrefetchUrlList(planDefinition)).getAsJson(); + } + private String mapCodePathToSearchParam(String dataType, String path) { switch (dataType) { case "MedicationAdministration": diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionStu3.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionStu3.java index 3a3b9a369..8b20825c7 100644 --- a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionStu3.java +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/discovery/DiscoveryResolutionStu3.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import com.google.gson.JsonObject; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.DataRequirement; @@ -207,6 +208,10 @@ public DiscoveryResponse resolve() { return response; } + public JsonObject resolveService(PlanDefinition planDefinition) { + return new DiscoveryElementStu3(planDefinition, getPrefetchUrlList(planDefinition)).getAsJson(); + } + private String mapCodePathToSearchParam(String dataType, String path) { switch (dataType) { case "MedicationAdministration": diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/dstu3/CdsHooksServlet.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/dstu3/CdsHooksServlet.java index 2ff522528..baf9a3768 100644 --- a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/dstu3/CdsHooksServlet.java +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/dstu3/CdsHooksServlet.java @@ -7,7 +7,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -27,7 +26,7 @@ import org.opencds.cqf.cql.engine.model.ModelResolver; import org.opencds.cqf.cql.engine.terminology.TerminologyProvider; import org.opencds.cqf.ruler.behavior.DaoRegistryUser; -import org.opencds.cqf.ruler.cdshooks.discovery.DiscoveryResolutionStu3; +import org.opencds.cqf.ruler.cdshooks.CdsServicesCache; import org.opencds.cqf.ruler.cdshooks.evaluation.EvaluationContext; import org.opencds.cqf.ruler.cdshooks.evaluation.Stu3EvaluationContext; import org.opencds.cqf.ruler.cdshooks.hooks.Hook; @@ -98,7 +97,7 @@ public class CdsHooksServlet extends HttpServlet implements DaoRegistryUser { private ModelResolver modelResolver; @Autowired - private AtomicReference services; + CdsServicesCache cdsServicesCache; protected ProviderConfiguration getProviderConfiguration() { return this.providerConfiguration; @@ -317,21 +316,13 @@ private JsonObject getService(String service) { } private JsonArray getServicesArray() { - JsonArray cachedServices = this.services.get(); - if (cachedServices == null || cachedServices.size() == 0) { - cachedServices = getServices().get("services").getAsJsonArray(); - services.set(cachedServices); - } - - return cachedServices; + return this.cdsServicesCache.getCdsServiceCache().get(); } private JsonObject getServices() { - DiscoveryResolutionStu3 discoveryResolutionStu3 = new DiscoveryResolutionStu3(daoRegistry); - - discoveryResolutionStu3.setMaxUriLength(this.getProviderConfiguration().getMaxUriLength()); - - return discoveryResolutionStu3.resolve().getAsJson(); + JsonObject services = new JsonObject(); + services.add("services", this.cdsServicesCache.getCdsServiceCache().get()); + return services; } private String toJsonResponse(List cards) { diff --git a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/r4/CdsHooksServlet.java b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/r4/CdsHooksServlet.java index 3f02b7409..dd24e8257 100644 --- a/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/r4/CdsHooksServlet.java +++ b/plugin/cds-hooks/src/main/java/org/opencds/cqf/ruler/cdshooks/r4/CdsHooksServlet.java @@ -7,7 +7,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -27,7 +26,7 @@ import org.opencds.cqf.cql.engine.model.ModelResolver; import org.opencds.cqf.cql.engine.terminology.TerminologyProvider; import org.opencds.cqf.ruler.behavior.DaoRegistryUser; -import org.opencds.cqf.ruler.cdshooks.discovery.DiscoveryResolutionR4; +import org.opencds.cqf.ruler.cdshooks.CdsServicesCache; import org.opencds.cqf.ruler.cdshooks.evaluation.EvaluationContext; import org.opencds.cqf.ruler.cdshooks.evaluation.R4EvaluationContext; import org.opencds.cqf.ruler.cdshooks.hooks.Hook; @@ -81,6 +80,7 @@ public class CdsHooksServlet extends HttpServlet implements DaoRegistryUser { @Autowired private LibraryLoaderFactory libraryLoaderFactory; + @Autowired private JpaLibraryContentProviderFactory jpaLibraryContentProviderFactory; @@ -97,7 +97,7 @@ public class CdsHooksServlet extends HttpServlet implements DaoRegistryUser { private ModelResolver modelResolver; @Autowired - private AtomicReference services; + CdsServicesCache cdsServicesCache; protected ProviderConfiguration getProviderConfiguration() { return this.providerConfiguration; @@ -119,6 +119,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.info(request.getRequestURI()); + if (!request.getRequestURL().toString().endsWith("/cds-services") && !request.getRequestURL().toString().endsWith("/cds-services/")) { logger.error(request.getRequestURI()); @@ -132,8 +133,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) @Override @SuppressWarnings("deprecation") - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.info(request.getRequestURI()); try { @@ -317,21 +317,13 @@ private JsonObject getService(String service) { } private JsonArray getServicesArray() { - JsonArray cachedServices = this.services.get(); - if (cachedServices == null || cachedServices.size() == 0) { - cachedServices = getServices().get("services").getAsJsonArray(); - services.set(cachedServices); - } - - return cachedServices; + return this.cdsServicesCache.getCdsServiceCache().get(); } private JsonObject getServices() { - DiscoveryResolutionR4 discoveryResolutionR4 = new DiscoveryResolutionR4(daoRegistry); - - discoveryResolutionR4.setMaxUriLength(this.getProviderConfiguration().getMaxUriLength()); - - return discoveryResolutionR4.resolve().getAsJson(); + JsonObject services = new JsonObject(); + services.add("services", this.cdsServicesCache.getCdsServiceCache().get()); + return services; } private String toJsonResponse(List cards) { diff --git a/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/ResourceChangeEvent.java b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/ResourceChangeEvent.java new file mode 100644 index 000000000..d4bbe523e --- /dev/null +++ b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/ResourceChangeEvent.java @@ -0,0 +1,44 @@ +package org.opencds.cqf.ruler.plugin.cdshooks; + +import ca.uhn.fhir.jpa.cache.IResourceChangeEvent; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.List; + +public class ResourceChangeEvent implements IResourceChangeEvent { + private List createdResourceIds; + private List updatedResourceIds; + private List deletedResourceIds; + + @Override + public List getCreatedResourceIds() { + return this.createdResourceIds; + } + + public void setCreatedResourceIds(List createdResourceIds) { + this.createdResourceIds = createdResourceIds; + } + + @Override + public List getUpdatedResourceIds() { + return this.updatedResourceIds; + } + + public void setUpdatedResourceIds(List updatedResourceIds) { + this.updatedResourceIds = updatedResourceIds; + } + + @Override + public List getDeletedResourceIds() { + return this.deletedResourceIds; + } + + public void setDeletedResourceIds(List deletedResourceIds) { + this.deletedResourceIds = deletedResourceIds; + } + + @Override + public boolean isEmpty() { + return false; + } +} diff --git a/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/dstu3/CdsHooksServletIT.java b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/dstu3/CdsHooksServletIT.java index 5c3de716b..652334e42 100644 --- a/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/dstu3/CdsHooksServletIT.java +++ b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/dstu3/CdsHooksServletIT.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.Collections; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -16,12 +17,16 @@ import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.PlanDefinition; +import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.opencds.cqf.ruler.Application; import org.opencds.cqf.ruler.cdshooks.CdsHooksConfig; +import org.opencds.cqf.ruler.cdshooks.CdsServicesCache; +import org.opencds.cqf.ruler.plugin.cdshooks.ResourceChangeEvent; import org.opencds.cqf.ruler.test.RestIntegrationTest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -30,21 +35,19 @@ import com.google.gson.JsonObject; @ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class, - CdsHooksConfig.class }, properties = { - "hapi.fhir.fhir_version=dstu3", "hapi.fhir.security.basic_auth.enabled=false" - }) -public class CdsHooksServletIT extends RestIntegrationTest { - - String ourCdsBase; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class, CdsHooksConfig.class }, properties = {"hapi.fhir.fhir_version=dstu3", "hapi.fhir.security.basic_auth.enabled=false"}) +class CdsHooksServletIT extends RestIntegrationTest { + @Autowired + CdsServicesCache cdsServicesCache; + private String ourCdsBase; @BeforeEach - public void beforeEach() { + void beforeEach() { ourCdsBase = "http://localhost:" + getPort() + "/cds-services"; } @Test - public void testGetCdsServices() throws IOException { + void testGetCdsServices() throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet request = new HttpGet(ourCdsBase); request.addHeader("Content-Type", "application/json"); @@ -53,10 +56,15 @@ public void testGetCdsServices() throws IOException { @Test // TODO: Add Opioid Tests once $apply-cql is implemented. - public void testCdsServicesRequest() throws IOException { + void testCdsServicesRequest() throws IOException { // Server Load loadTransaction("HelloWorldPatientView-bundle.json"); loadResource("hello-world-patient-view-patient.json"); + + ResourceChangeEvent rce = new ResourceChangeEvent(); + rce.setUpdatedResourceIds(Collections.singletonList(new IdType("hello-world-patient-view"))); + cdsServicesCache.handleChange(rce); + Patient ourPatient = getClient().read().resource(Patient.class).withId("patient-hello-world-patient-view") .execute(); assertNotNull(ourPatient); @@ -64,7 +72,7 @@ public void testCdsServicesRequest() throws IOException { PlanDefinition ourPlanDefinition = getClient().read().resource(PlanDefinition.class) .withId("hello-world-patient-view").execute(); assertNotNull(ourPlanDefinition); - Bundle getPlanDefinitions = null; + Bundle getPlanDefinitions; int tries = 0; do { // Can take up to 10 seconds for HAPI to reindex searches diff --git a/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/r4/CdsHooksServletIT.java b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/r4/CdsHooksServletIT.java index a6d66652c..ef83eb741 100644 --- a/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/r4/CdsHooksServletIT.java +++ b/plugin/cds-hooks/src/test/java/org/opencds/cqf/ruler/plugin/cdshooks/r4/CdsHooksServletIT.java @@ -1,10 +1,7 @@ package org.opencds.cqf.ruler.plugin.cdshooks.r4; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; +import java.util.Collections; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -14,6 +11,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.PlanDefinition; import org.junit.jupiter.api.BeforeEach; @@ -21,7 +19,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.opencds.cqf.ruler.Application; import org.opencds.cqf.ruler.cdshooks.CdsHooksConfig; +import org.opencds.cqf.ruler.cdshooks.CdsServicesCache; +import org.opencds.cqf.ruler.plugin.cdshooks.ResourceChangeEvent; import org.opencds.cqf.ruler.test.RestIntegrationTest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -29,39 +30,80 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import static org.junit.jupiter.api.Assertions.*; + @ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class, - CdsHooksConfig.class }, properties = { - "hapi.fhir.fhir_version=r4", "hapi.fhir.security.basic_auth.enabled=false" - }) -public class CdsHooksServletIT extends RestIntegrationTest { - String ourCdsBase; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class, CdsHooksConfig.class }, properties = {"hapi.fhir.fhir_version=r4", "hapi.fhir.security.basic_auth.enabled=false"}) +class CdsHooksServletIT extends RestIntegrationTest { + @Autowired + CdsServicesCache cdsServicesCache; + private String ourCdsBase; @BeforeEach - public void beforeEach() { + void beforeEach() { ourCdsBase = "http://localhost:" + getPort() + "/cds-services"; } @Test - public void testGetCdsServices() throws IOException { + void testGetCdsServices() throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet request = new HttpGet(ourCdsBase); request.addHeader("Content-Type", "application/json"); assertEquals(200, httpClient.execute(request).getStatusLine().getStatusCode()); } + @Test + void testCdsServicesCache() { + loadTransaction("Screening-bundle-r4.json"); + loadTransaction("HelloWorldPatientView-bundle.json"); + PlanDefinition p1 = (PlanDefinition) loadResource("Screening-plandefinition.json"); + PlanDefinition p2 = (PlanDefinition) loadResource("HelloWorld-plandefinition.json"); + + ResourceChangeEvent rce = new ResourceChangeEvent(); + rce.setCreatedResourceIds(Collections.singletonList(p1.getIdElement())); + + cdsServicesCache.clearCache(); + + cdsServicesCache.handleChange(rce); + assertEquals(1, cdsServicesCache.getCdsServiceCache().get().size()); + + rce.setCreatedResourceIds(Collections.singletonList(p2.getIdElement())); + cdsServicesCache.handleChange(rce); + assertEquals(2, cdsServicesCache.getCdsServiceCache().get().size()); + + rce.setCreatedResourceIds(null); + rce.setDeletedResourceIds(Collections.singletonList(p1.getIdElement())); + cdsServicesCache.handleChange(rce); + assertEquals(1, cdsServicesCache.getCdsServiceCache().get().size()); + + assertEquals("HelloWorldPatientView", cdsServicesCache.getCdsServiceCache().get().get(0).getAsJsonObject().get("name").getAsString()); + PlanDefinition updatedP2 = new PlanDefinition(); + p2.copyValues(updatedP2); + updatedP2.setName("HelloWorldPatientView-updated"); + update(updatedP2); + rce.setDeletedResourceIds(null); + rce.setUpdatedResourceIds(Collections.singletonList(updatedP2.getIdElement())); + cdsServicesCache.handleChange(rce); + assertEquals("HelloWorldPatientView-updated", cdsServicesCache.getCdsServiceCache().get().get(0).getAsJsonObject().get("name").getAsString()); + } + @Test // TODO: Debug delay in Client.search(). - public void testCdsServicesRequest() throws IOException { + void testCdsServicesRequest() throws IOException { // Server Load loadTransaction("Screening-bundle-r4.json"); + + ResourceChangeEvent rce = new ResourceChangeEvent(); + rce.setUpdatedResourceIds(Collections.singletonList(new IdType("plandefinition-Screening"))); + cdsServicesCache.handleChange(rce); + Patient ourPatient = getClient().read().resource(Patient.class).withId("HighRiskIDUPatient").execute(); assertNotNull(ourPatient); assertEquals("HighRiskIDUPatient", ourPatient.getIdElement().getIdPart()); PlanDefinition ourPlanDefinition = getClient().read().resource(PlanDefinition.class) .withId("plandefinition-Screening").execute(); assertNotNull(ourPlanDefinition); - Bundle getPlanDefinitions = null; + Bundle getPlanDefinitions; int tries = 0; do { // Can take up to 10 seconds for HAPI to reindex searches @@ -129,5 +171,4 @@ public void testCdsServicesRequest() throws IOException { .getAsString(); assertEquals("Patient/" + expectedPatientID, actualPatientID); } - } diff --git a/plugin/cds-hooks/src/test/resources/HelloWorld-plandefinition.json b/plugin/cds-hooks/src/test/resources/HelloWorld-plandefinition.json new file mode 100644 index 000000000..05ae6a04b --- /dev/null +++ b/plugin/cds-hooks/src/test/resources/HelloWorld-plandefinition.json @@ -0,0 +1,74 @@ +{ + "resourceType": "PlanDefinition", + "id": "hello-world-patient-view", + "url": "http://fhir.org/guides/cdc/opioid-cds/PlanDefinition/hello-world-patient-view", + "identifier": [ { + "use": "official", + "value": "helloworld-patient-view-sample" + } ], + "version": "1.0.0", + "name": "HelloWorldPatientView", + "title": "Hello World (patient-view)", + "type": { + "coding": [ { + "system": "http://hl7.org/fhir/plan-definition-type", + "code": "eca-rule", + "display": "ECA Rule" + } ] + }, + "status": "draft", + "date": "2021-05-26T00:00:00-08:00", + "publisher": "Alphora", + "description": "This PlanDefinition defines a simple Hello World recommendation that triggers on patient-view.", + "purpose": "The purpose of this is to test the system to make sure we have complete end-to-end functionality", + "usage": "This is to be used in conjunction with a patient-facing FHIR application.", + "useContext": [ { + "code": { + "system": "http://hl7.org/fhir/usage-context-type", + "version": "4.0.1", + "code": "focus", + "display": "Clinical Focus" + } + } ], + "jurisdiction": [ { + "coding": [ { + "system": "http://hl7.org/fhir/ValueSet/iso3166-1-3", + "version": "4.0.1", + "code": "USA", + "display": "United States of America" + } ] + } ], + "library": [ { + "reference": "http://fhir.org/guides/cdc/opioid-cds/Library/HelloWorldPatientView" + } ], + "action": [ { + "label": "Hello World!", + "title": "Hello World!", + "description": "A simple Hello World (patient-view) recommendation", + "triggerDefinition": [ { + "type": "named-event", + "eventName": "patient-view" + } ], + "condition": [ { + "kind": "start", + "description": "Whether or not a Hello World! card should be returned", + "language": "text/cql", + "expression": "Main Action Condition Expression Is True" + } ], + "type": { + "system": "http://terminology.hl7.org/CodeSystem/action-type", + "code": "create", + "display": "Create" + }, + "dynamicValue": [ { + "path": "action.title", + "expression": "Get Title" + }, { + "path": "action.description", + "expression": "Get Description" + }, { + "path": "activity.extension", + "expression": "Get Indicator" + } ] + } ] +} diff --git a/plugin/cds-hooks/src/test/resources/Screening-plandefinition.json b/plugin/cds-hooks/src/test/resources/Screening-plandefinition.json new file mode 100644 index 000000000..d30927528 --- /dev/null +++ b/plugin/cds-hooks/src/test/resources/Screening-plandefinition.json @@ -0,0 +1,404 @@ +{ + "resourceType": "PlanDefinition", + "id": "plandefinition-Screening", + "url": "http://fhir.org/guides/nachc/hiv-cds/PlanDefinition/plandefinition-Screening", + "identifier": [ { + "use": "official", + "value": "nachc-Screening" + } ], + "version": "1.0.0", + "name": "Screening", + "title": "CDC HIV Screening", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "eca-rule", + "display": "ECA Rule" + } ] + }, + "status": "draft", + "date": "2021-07-31T00:00:00-08:00", + "publisher": "National Association of Community Health Centers, Inc. (NACHC)", + "description": "This PlanDefinition defines a a Clinical Decision Support CDC Recommendation for HIV Screening", + "useContext": [ { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "version": "4.0.1", + "code": "focus", + "display": "Clinical Focus" + }, + "valueCodeableConcept": { + "coding": [ { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "2021", + "code": "B20", + "display": "Human immunodeficiency virus [HIV] disease" + } ] + } + } ], + "jurisdiction": [ { + "coding": [ { + "system": "http://hl7.org/fhir/ValueSet/iso3166-1-3", + "version": "4.0.1", + "code": "USA", + "display": "United States of America" + } ] + } ], + "purpose": "The purpose of this is to identify and build CDS support for HIV Screening.", + "usage": "This is to be used in conjunction with a patient-facing FHIR application.", + "copyright": "© Copyright National Association of Community Health Centers, Inc. (NACHC) 2021+.", + "topic": [ { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/definition-topic", + "version": "4.0.1", + "code": "assessment", + "display": "Assessment" + } ], + "text": "HIV Management" + } ], + "library": [ "http://fhir.org/guides/nachc/hiv-cds/Library/Screening" ], + "action": [ { + "title": "HIV Screening", + "documentation": [ { + "type": "documentation", + "display": "Info for those with HIV", + "url": "https://www.cdc.gov/hiv/guidelines/testing.html" + } ], + "trigger": [ { + "type": "named-event", + "name": "patient-view" + } ], + "condition": [ { + "kind": "start", + "expression": { + "language": "text/cql", + "expression": "'true'" + } + } ], + "dynamicValue": [ { + "path": "action.description", + "expression": { + "description": "Patient Name.", + "language": "text/cql.identifier", + "expression": "Patient Name" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Info" + } + } ], + "action": [ { + "prefix": "Recommend HIV Screening Test.", + "title": "Never Tested Recommendation", + "description": "Perform CDC Recommendation for Never Tested Treatment if conditions are met.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in msm population.", + "language": "text/cql.identifier", + "expression": "Never Tested Condition" + } + } ], + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "create", + "display": "Create" + } ] + }, + "selectionBehavior": "any", + "definitionCanonical": "http://fhir.org/guides/nachc/hiv-cds/ActivityDefinition/activitydefinition-hiv-screening-request", + "dynamicValue": [ { + "path": "action.title", + "expression": { + "description": "Provides Recommendation for Never Tested screenings.", + "language": "text/cql.identifier", + "expression": "Never Tested Recommendation" + } + }, { + "path": "action.description", + "expression": { + "description": "Provides Rationale for Never Tested screenings.", + "language": "text/cql.identifier", + "expression": "Never Tested Rationale" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Never Tested Indicator" + } + }, { + "path": "asNeededBoolean", + "expression": { + "language": "text/cql.identifier", + "expression": "Never Tested Condition" + } + } ], + "action": [ { + "description": "Will perform HIV screening" + }, { + "description": "Will not perform HIV screening at this time - Snooze 1 month." + }, { + "description": "Will not perform HIV screening at this time - Snooze 12 months." + }, { + "description": "Will not perform HIV screening at this time - patient declined." + } ] + }, { + "prefix": "Recommend HIV Screening Test.", + "title": "MSM Recommendation", + "description": "Perform CDC Recommendation for MSM if conditions are met for either 3 month or annual testing.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in msm population.", + "language": "text/cql.identifier", + "expression": "MSM Condition" + } + } ], + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "create", + "display": "Create" + } ] + }, + "selectionBehavior": "any", + "definitionCanonical": "http://fhir.org/guides/nachc/hiv-cds/ActivityDefinition/activitydefinition-hiv-screening-request", + "dynamicValue": [ { + "path": "action.title", + "expression": { + "description": "Provides Recommendation for screening.", + "language": "text/cql.identifier", + "expression": "MSM Recommendation" + } + }, { + "path": "action.description", + "expression": { + "description": "Provides Rationale for screening.", + "language": "text/cql.identifier", + "expression": "MSM Rationale" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "MSM Indicator" + } + }, { + "path": "asNeededBoolean", + "expression": { + "language": "text/cql.identifier", + "expression": "MSM Condition" + } + } ], + "action": [ { + "description": "Will perform HIV screening" + }, { + "description": "Will not perform HIV screening at this time - Snooze 1 month." + }, { + "description": "Will not perform HIV screening at this time - Snooze 12 months." + }, { + "description": "Will not perform HIV screening at this time - patient declined." + } ] + }, { + "prefix": "Recommend HIV Screening Test.", + "title": "Pregnancy Recommendation", + "description": "Perform CDC Recommendation for Pregnancy if conditions are met for first prenatal visit or third trimester high risk testing.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in msm population.", + "language": "text/cql.identifier", + "expression": "Pregnant Condition" + } + } ], + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "create", + "display": "Create" + } ] + }, + "selectionBehavior": "any", + "definitionCanonical": "http://fhir.org/guides/nachc/hiv-cds/ActivityDefinition/activitydefinition-hiv-screening-request", + "dynamicValue": [ { + "path": "action.title", + "expression": { + "description": "Provides Recommendation for Pregnancy screenings.", + "language": "text/cql.identifier", + "expression": "Pregnant Recommendation" + } + }, { + "path": "action.description", + "expression": { + "description": "Provides Rationale for Pregnancy screenings.", + "language": "text/cql.identifier", + "expression": "Pregnant Rationale" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Pregnant Indicator" + } + }, { + "path": "asNeededBoolean", + "expression": { + "language": "text/cql.identifier", + "expression": "Pregnant Condition" + } + } ], + "action": [ { + "description": "Will perform HIV screening" + }, { + "description": "Will not perform HIV screening at this time - Snooze 1 month." + }, { + "description": "Will not perform HIV screening at this time - Snooze 12 months." + }, { + "description": "Will not perform HIV screening at this time - patient declined." + } ] + }, { + "prefix": "Recommend HIV Screening Test.", + "title": "Seeking Treatment Recommendation", + "description": "Perform CDC Recommendation for Seeking STD Treatment if conditions are met.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in msm population.", + "language": "text/cql.identifier", + "expression": "Seeking STD Treatment Condition" + } + } ], + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "create", + "display": "Create" + } ] + }, + "selectionBehavior": "any", + "definitionCanonical": "http://fhir.org/guides/nachc/hiv-cds/ActivityDefinition/activitydefinition-hiv-screening-request", + "dynamicValue": [ { + "path": "action.title", + "expression": { + "description": "Provides Recommendation for Seeking STD Treatment screenings.", + "language": "text/cql.identifier", + "expression": "Seeking STD Treatment Recommendation" + } + }, { + "path": "action.description", + "expression": { + "description": "Provides Rationale for Seeking STD Treatment screenings.", + "language": "text/cql.identifier", + "expression": "Seeking STD Treatment Rationale" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Seeking STD Treatment Indicator" + } + }, { + "path": "asNeededBoolean", + "expression": { + "language": "text/cql.identifier", + "expression": "Seeking STD Treatment Condition" + } + } ], + "action": [ { + "description": "Will perform HIV screening" + }, { + "description": "Will not perform HIV screening at this time - Snooze 1 month." + }, { + "description": "Will not perform HIV screening at this time - Snooze 12 months." + }, { + "description": "Will not perform HIV screening at this time - patient declined." + } ] + }, { + "prefix": "Recommend HIV Screening Test.", + "title": "Risk Level Recommendation", + "description": "Determines type of recommendation based on risk level regarding status of HIV Screening.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in screening population.", + "language": "text/cql.identifier", + "expression": "Risk Level Condition" + } + } ], + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/plan-definition-type", + "code": "create", + "display": "Create" + } ] + }, + "selectionBehavior": "any", + "definitionCanonical": "http://fhir.org/guides/nachc/hiv-cds/ActivityDefinition/activitydefinition-hiv-screening-request", + "dynamicValue": [ { + "path": "action.title", + "expression": { + "description": "Determines what recommendation patient should be provided.", + "language": "text/cql.identifier", + "expression": "Risk Level Recommendation" + } + }, { + "path": "action.description", + "expression": { + "description": "Rationale for recommendation type.", + "language": "text/cql.identifier", + "expression": "Risk Level Rationale" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Risk Level Indicator Status" + } + }, { + "path": "asNeededBoolean", + "expression": { + "language": "text/cql.identifier", + "expression": "Risk Level Condition" + } + } ], + "action": [ { + "description": "Will perform HIV screening" + }, { + "description": "Will not perform HIV screening at this time - Snooze 1 month." + }, { + "description": "Will not perform HIV screening at this time - Snooze 12 months." + }, { + "description": "Will not perform HIV screening at this time - patient declined." + } ] + }, { + "title": "Exclusion from HIV Screening", + "description": "Determines if patient was excluded from HIV Screening Recommendation.", + "condition": [ { + "kind": "applicability", + "expression": { + "description": "Determine if Patient is in hiv exclusion population.", + "language": "text/cql.identifier", + "expression": "Meets Exclusion Criteria" + } + } ], + "dynamicValue": [ { + "path": "action.description", + "expression": { + "description": "Rationale for why patient was excluded from the hiv screening.", + "language": "text/cql.identifier", + "expression": "Exclusion Reason" + } + }, { + "path": "action.extension", + "expression": { + "language": "text/cql.identifier", + "expression": "Info" + } + } ] + } ] + } ] +}