Subscription fhirpath criteria (#5975)

* Added fhirpath-criteria evaluation

* Added test for valid fhirpath that does not evaluate to boolean

* Added resolving the variables %current and %previous

* Added test using only FP criteria

* Added test cases for valid FhirPath expressions that return non-booleans

* Added use of central cache

* Added more elaborate tests for non-sunshine scenarios

* Added changelog

* CheckStyle'd errorcode added.

* Added spotless formatting and converted FhirPathR5 expression to be Android compatible

* Applied more spotless
This commit is contained in:
Jens Kristian Villadsen 2024-06-22 01:46:39 +02:00 committed by GitHub
parent 0397b9ddc8
commit f767058239
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 399 additions and 20 deletions

View File

@ -24,6 +24,8 @@ import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List;
public interface IFhirPathEvaluationContext {
/**
@ -36,4 +38,15 @@ public interface IFhirPathEvaluationContext {
default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
return null;
}
/**
*
* @param appContext
* @param name The name of the constant(s) to be resolved
* @param beforeContext
* @return
*/
default List<IBase> resolveConstant(Object appContext, String name, boolean beforeContext) {
return null;
}
}

View File

@ -0,0 +1,4 @@
---
type: add
issue: 6031
title: "Subscriptions now support the evaluation use of FhirPath criteria and the use of the variables %current and %previous. Thanks to Jens Villadsen (@jkiddo) for the contribution!"

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.subscription.config.SubscriptionConfig;
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -33,11 +34,12 @@ import org.springframework.context.annotation.Lazy;
@Import(SubscriptionConfig.class)
public class SubscriptionTopicConfig {
@Bean
SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) {
SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(
FhirContext theFhirContext, MemoryCacheService memoryCacheService) {
switch (theFhirContext.getVersion().getVersion()) {
case R5:
case R4B:
return new SubscriptionTopicMatchingSubscriber(theFhirContext);
return new SubscriptionTopicMatchingSubscriber(theFhirContext, memoryCacheService);
default:
return null;
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.SubscriptionTopic;
@ -29,10 +30,15 @@ import java.util.List;
public class SubscriptionTopicMatcher {
private final SubscriptionTopicSupport mySubscriptionTopicSupport;
private final SubscriptionTopic myTopic;
private final MemoryCacheService myMemoryCacheService;
public SubscriptionTopicMatcher(SubscriptionTopicSupport theSubscriptionTopicSupport, SubscriptionTopic theTopic) {
public SubscriptionTopicMatcher(
SubscriptionTopicSupport theSubscriptionTopicSupport,
SubscriptionTopic theTopic,
MemoryCacheService memoryCacheService) {
mySubscriptionTopicSupport = theSubscriptionTopicSupport;
myTopic = theTopic;
myMemoryCacheService = memoryCacheService;
}
public InMemoryMatchResult match(ResourceModifiedMessage theMsg) {
@ -43,7 +49,7 @@ public class SubscriptionTopicMatcher {
for (SubscriptionTopic.SubscriptionTopicResourceTriggerComponent next : triggers) {
if (resourceName.equals(next.getResource())) {
SubscriptionTriggerMatcher matcher =
new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next);
new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next, myMemoryCacheService);
InMemoryMatchResult result = matcher.match();
if (result.matched()) {
// as soon as one trigger matches, we're done

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.topic.filter.InMemoryTopicFilterMatcher;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc;
import ca.uhn.fhir.util.Logs;
@ -67,8 +68,11 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
@Autowired
private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc;
public SubscriptionTopicMatchingSubscriber(FhirContext theFhirContext) {
private MemoryCacheService myMemoryCacheService;
public SubscriptionTopicMatchingSubscriber(FhirContext theFhirContext, MemoryCacheService memoryCacheService) {
myFhirContext = theFhirContext;
this.myMemoryCacheService = memoryCacheService;
}
@Override
@ -110,7 +114,8 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
Collection<SubscriptionTopic> topics = mySubscriptionTopicRegistry.getAll();
for (SubscriptionTopic topic : topics) {
SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic);
SubscriptionTopicMatcher matcher =
new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic, myMemoryCacheService);
InMemoryMatchResult result = matcher.match(theMsg);
if (result.matched()) {
int deliveries = deliverToTopicSubscriptions(theMsg, topic, result);

View File

@ -19,17 +19,26 @@
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage;
import ca.uhn.fhir.storage.PreviousVersionReader;
import ca.uhn.fhir.util.Logs;
import com.google.common.base.Strings;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.slf4j.Logger;
import org.slf4j.helpers.MessageFormatter;
import java.util.List;
import java.util.Optional;
@ -45,11 +54,13 @@ public class SubscriptionTriggerMatcher {
private final IFhirResourceDao myDao;
private final PreviousVersionReader myPreviousVersionReader;
private final SystemRequestDetails mySrd;
private final MemoryCacheService myMemoryCacheService;
public SubscriptionTriggerMatcher(
SubscriptionTopicSupport theSubscriptionTopicSupport,
ResourceModifiedMessage theMsg,
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent theTrigger) {
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent theTrigger,
MemoryCacheService theMemoryCacheService) {
mySubscriptionTopicSupport = theSubscriptionTopicSupport;
myOperation = theMsg.getOperationType();
myResource = theMsg.getPayload(theSubscriptionTopicSupport.getFhirContext());
@ -58,6 +69,7 @@ public class SubscriptionTriggerMatcher {
myTrigger = theTrigger;
myPreviousVersionReader = new PreviousVersionReader(myDao);
mySrd = new SystemRequestDetails();
myMemoryCacheService = theMemoryCacheService;
}
public InMemoryMatchResult match() {
@ -66,21 +78,22 @@ public class SubscriptionTriggerMatcher {
if (SubscriptionTopicUtil.matches(myOperation, supportedInteractions)) {
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent queryCriteria =
myTrigger.getQueryCriteria();
InMemoryMatchResult result = match(queryCriteria);
if (result.matched()) {
return result;
}
String fhirPathCriteria = myTrigger.getFhirPathCriteria();
return match(queryCriteria, fhirPathCriteria);
}
return InMemoryMatchResult.noMatch();
}
private InMemoryMatchResult match(
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) {
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria,
String theFhirPathCriteria) {
String previousCriteria = theQueryCriteria.getPrevious();
String currentCriteria = theQueryCriteria.getCurrent();
InMemoryMatchResult previousMatches = InMemoryMatchResult.fromBoolean(previousCriteria == null);
InMemoryMatchResult currentMatches = InMemoryMatchResult.fromBoolean(currentCriteria == null);
InMemoryMatchResult fhirPathCriteriaEvaluationResult = evaluateFhirPathCriteria(theFhirPathCriteria);
// WIP STR5 implement fhirPathCriteria per https://build.fhir.org/subscriptiontopic.html#fhirpath-criteria
if (currentCriteria != null) {
currentMatches = matchResource(myResource, currentCriteria);
@ -105,12 +118,89 @@ public class SubscriptionTriggerMatcher {
}
// WIP STR5 implement resultForCreate and resultForDelete
if (theQueryCriteria.getRequireBoth()) {
return InMemoryMatchResult.and(previousMatches, currentMatches);
return InMemoryMatchResult.and(
InMemoryMatchResult.and(previousMatches, currentMatches), fhirPathCriteriaEvaluationResult);
} else {
return InMemoryMatchResult.or(previousMatches, currentMatches);
return InMemoryMatchResult.and(
InMemoryMatchResult.or(previousMatches, currentMatches), fhirPathCriteriaEvaluationResult);
}
}
private InMemoryMatchResult evaluateFhirPathCriteria(String theFhirPathCriteria) {
if (!Strings.isNullOrEmpty(theFhirPathCriteria)) {
IFhirPath fhirPathEngine =
mySubscriptionTopicSupport.getFhirContext().newFhirPath();
fhirPathEngine.setEvaluationContext(new IFhirPathEvaluationContext() {
@Override
public List<IBase> resolveConstant(Object appContext, String name, boolean beforeContext) {
if ("current".equalsIgnoreCase(name)) return List.of(myResource);
if ("previous".equalsIgnoreCase(name)) {
Optional previousResource = myPreviousVersionReader.readPreviousVersion(myResource);
if (previousResource.isPresent()) return List.of((IBase) previousResource.get());
}
return null;
}
});
try {
IFhirPath.IParsedExpression expression = myMemoryCacheService.get(
MemoryCacheService.CacheEnum.FHIRPATH_EXPRESSION, theFhirPathCriteria, exp -> {
try {
return fhirPathEngine.parse(exp);
} catch (FHIRException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(Msg.code(2534) + e.getMessage(), e);
}
});
List<IBase> result = fhirPathEngine.evaluate(myResource, expression, IBase.class);
return parseResult(theFhirPathCriteria, result);
} catch (FHIRException fhirException) {
ourLog.warn(
"Subscription topic {} has a fhirPathCriteria that is not valid: {}",
myTrigger.getId(),
theFhirPathCriteria,
fhirException);
return InMemoryMatchResult.unsupportedFromReason(fhirException.getMessage());
}
}
return InMemoryMatchResult.fromBoolean(true);
}
private InMemoryMatchResult parseResult(String theFhirPathCriteria, List<IBase> result) {
if (result == null) {
return InMemoryMatchResult.unsupportedFromReason(MessageFormatter.format(
"FhirPath evaluation criteria '{}' from Subscription topic: '{}' resulted in null results.",
theFhirPathCriteria,
myTrigger.getId())
.getMessage());
}
if (result.size() != 1) {
return InMemoryMatchResult.unsupportedFromReason(MessageFormatter.arrayFormat(
"FhirPath evaluation criteria '{}' from Subscription topic: '{}' resulted in '{}' results. Expected 1.",
new String[] {theFhirPathCriteria, myTrigger.getId(), String.valueOf(result.size())})
.getMessage());
}
if (!(result.get(0) instanceof BooleanType)) {
return InMemoryMatchResult.unsupportedFromReason(MessageFormatter.arrayFormat(
"FhirPath evaluation criteria '{}' from Subscription topic: '{}' resulted in a non-boolean result: '{}'",
new String[] {
theFhirPathCriteria,
myTrigger.getId(),
result.get(0).getClass().getName()
})
.getMessage());
}
return InMemoryMatchResult.fromBoolean(((BooleanType) result.get(0)).booleanValue());
}
private InMemoryMatchResult matchResource(IBaseResource theResource, String theCriteria) {
InMemoryMatchResult result =
mySubscriptionTopicSupport.getSearchParamMatcher().match(theCriteria, theResource, mySrd);

View File

@ -1,12 +1,15 @@
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import org.hl7.fhir.r5.model.Encounter;
import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.junit.jupiter.api.BeforeEach;
@ -15,7 +18,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -30,11 +36,14 @@ class SubscriptionTriggerMatcherTest {
@Mock
SearchParamMatcher mySearchParamMatcher;
MemoryCacheService myMemoryCacheService;
private SubscriptionTopicSupport mySubscriptionTopicSupport;
private Encounter myEncounter;
@BeforeEach
public void before() {
myMemoryCacheService = new MemoryCacheService(new JpaStorageSettings());
mySubscriptionTopicSupport = new SubscriptionTopicSupport(ourFhirContext, myDaoRegistry, mySearchParamMatcher);
myEncounter = new Encounter();
myEncounter.setIdElement(new IdType("Encounter", "123", "2"));
@ -48,7 +57,7 @@ class SubscriptionTriggerMatcherTest {
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger);
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
@ -65,7 +74,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.CREATE);
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger);
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
@ -82,7 +91,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger);
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
@ -99,7 +108,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger);
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
@ -124,11 +133,253 @@ class SubscriptionTriggerMatcherTest {
when(mySearchParamMatcher.match(any(), any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger);
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
}
@Test
public void testFalseFhirPathCriteriaEvaluation() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("false");
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
}
@Test
public void testInvalidFhirPathCriteriaEvaluation() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("random text");
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
assertEquals("Error @1, 2: Premature ExpressionNode termination at unexpected token \"text\"", result.getUnsupportedReason());
}
@Test
public void testInvalidBooleanOutcomeOfFhirPathCriteriaEvaluation() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("id");
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
}
@Test
public void testValidFhirPathCriteriaEvaluation() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("id = " + myEncounter.getIdElement().getIdPart());
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
}
@Test
public void testValidFhirPathCriteriaEvaluationUsingCurrent() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("%current.id = " + myEncounter.getIdElement().getIdPart());
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
}
@Test
public void testValidFhirPathCriteriaEvaluationReturningNonBoolean() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setId("1");
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("%current.id");
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
assertEquals("FhirPath evaluation criteria '%current.id' from Subscription topic: '1' resulted in a non-boolean result: 'org.hl7.fhir.r5.model.IdType'", result.getUnsupportedReason());
}
@Test
public void testValidFhirPathReturningCollection() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setId("1");
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("%current | %previous");
IFhirResourceDao mockEncounterDao = mock(IFhirResourceDao.class);
when(myDaoRegistry.getResourceDao("Encounter")).thenReturn(mockEncounterDao);
Encounter encounterPreviousVersion = new Encounter();
when(mockEncounterDao.read(any(), any(), eq(false))).thenReturn(encounterPreviousVersion);
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
assertEquals("FhirPath evaluation criteria '%current | %previous' from Subscription topic: '1' resulted in '2' results. Expected 1.", result.getUnsupportedReason());
}
@Test
public void testUpdateWithPrevCriteriaMatchAndFailingFhirPathCriteria() {
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.getQueryCriteria().setPrevious("Encounter?status=in-progress");
trigger.setFhirPathCriteria("random text");
IFhirResourceDao mockEncounterDao = mock(IFhirResourceDao.class);
when(myDaoRegistry.getResourceDao("Encounter")).thenReturn(mockEncounterDao);
Encounter encounterPreviousVersion = new Encounter();
when(mockEncounterDao.read(any(), any(), eq(false))).thenReturn(encounterPreviousVersion);
when(mySearchParamMatcher.match(any(), any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertFalse(result.matched());
assertEquals("Error @1, 2: Premature ExpressionNode termination at unexpected token \"text\"", result.getUnsupportedReason());
}
@Test
public void testUpdateWithPrevCriteriaMatchAndFhirPathCriteriaUsingPreviousVersion() {
myEncounter.setStatus(Enumerations.EncounterStatus.INPROGRESS);
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.getQueryCriteria().setPrevious("Encounter?status=in-progress");
trigger.setFhirPathCriteria("%current.status='in-progress' and %previous.status.exists().not()");
IFhirResourceDao mockEncounterDao = mock(IFhirResourceDao.class);
when(myDaoRegistry.getResourceDao("Encounter")).thenReturn(mockEncounterDao);
Encounter encounterPreviousVersion = new Encounter();
when(mockEncounterDao.read(any(), any(), eq(false))).thenReturn(encounterPreviousVersion);
when(mySearchParamMatcher.match(any(), any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
}
@Test
public void testUpdateOnlyFhirPathCriteriaUsingPreviousVersion() {
myEncounter.setStatus(Enumerations.EncounterStatus.INPROGRESS);
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
trigger.setFhirPathCriteria("%current.status='in-progress' and %previous.status.exists().not()");
IFhirResourceDao mockEncounterDao = mock(IFhirResourceDao.class);
when(myDaoRegistry.getResourceDao("Encounter")).thenReturn(mockEncounterDao);
Encounter encounterPreviousVersion = new Encounter();
when(mockEncounterDao.read(any(), any(), eq(false))).thenReturn(encounterPreviousVersion);
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
}
@Test
public void testCacheUsage() {
myEncounter.setStatus(Enumerations.EncounterStatus.INPROGRESS);
ResourceModifiedMessage msg = new ResourceModifiedMessage(ourFhirContext, myEncounter, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
// setup
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
trigger.setResource("Encounter");
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
String fhirPathCriteria = "%current.status='in-progress'";
trigger.setFhirPathCriteria(fhirPathCriteria);
IFhirResourceDao mockEncounterDao = mock(IFhirResourceDao.class);
when(myDaoRegistry.getResourceDao("Encounter")).thenReturn(mockEncounterDao);
assertNull(myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FHIRPATH_EXPRESSION, fhirPathCriteria));
// run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match();
// verify
assertTrue(result.matched());
assertNotNull(myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FHIRPATH_EXPRESSION, fhirPathCriteria));
}
}

View File

@ -77,6 +77,7 @@ public class MemoryCacheService {
case HISTORY_COUNT:
case TAG_DEFINITION:
case RESOURCE_CONDITIONAL_CREATE_VERSION:
case FHIRPATH_EXPRESSION:
default:
timeoutSeconds = SECONDS.convert(1, MINUTES);
maximumSize = 10000;
@ -193,6 +194,7 @@ public class MemoryCacheService {
TAG_DEFINITION(TagDefinitionCacheKey.class),
RESOURCE_LOOKUP(String.class),
FORCED_ID_TO_PID(String.class),
FHIRPATH_EXPRESSION(String.class),
/**
* Key type: {@literal Long}
* Value type: {@literal Optional<String>}

View File

@ -19,8 +19,10 @@ import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.ValueSet;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class FhirPathR5 implements IFhirPath {
@ -99,7 +101,11 @@ public class FhirPathR5 implements IFhirPath {
boolean beforeContext,
boolean explicitConstant)
throws PathEngineException {
return null;
return Collections.unmodifiableList(
theEvaluationContext.resolveConstant(appContext, name, beforeContext).stream()
.map(Base.class::cast)
.collect(Collectors.toList()));
}
@Override