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.IBase;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List;
public interface IFhirPathEvaluationContext { public interface IFhirPathEvaluationContext {
/** /**
@ -36,4 +38,15 @@ public interface IFhirPathEvaluationContext {
default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) {
return null; 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.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.subscription.config.SubscriptionConfig; import ca.uhn.fhir.jpa.subscription.config.SubscriptionConfig;
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -33,11 +34,12 @@ import org.springframework.context.annotation.Lazy;
@Import(SubscriptionConfig.class) @Import(SubscriptionConfig.class)
public class SubscriptionTopicConfig { public class SubscriptionTopicConfig {
@Bean @Bean
SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) { SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(
FhirContext theFhirContext, MemoryCacheService memoryCacheService) {
switch (theFhirContext.getVersion().getVersion()) { switch (theFhirContext.getVersion().getVersion()) {
case R5: case R5:
case R4B: case R4B:
return new SubscriptionTopicMatchingSubscriber(theFhirContext); return new SubscriptionTopicMatchingSubscriber(theFhirContext, memoryCacheService);
default: default:
return null; 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.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; 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.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.SubscriptionTopic; import org.hl7.fhir.r5.model.SubscriptionTopic;
@ -29,10 +30,15 @@ import java.util.List;
public class SubscriptionTopicMatcher { public class SubscriptionTopicMatcher {
private final SubscriptionTopicSupport mySubscriptionTopicSupport; private final SubscriptionTopicSupport mySubscriptionTopicSupport;
private final SubscriptionTopic myTopic; private final SubscriptionTopic myTopic;
private final MemoryCacheService myMemoryCacheService;
public SubscriptionTopicMatcher(SubscriptionTopicSupport theSubscriptionTopicSupport, SubscriptionTopic theTopic) { public SubscriptionTopicMatcher(
SubscriptionTopicSupport theSubscriptionTopicSupport,
SubscriptionTopic theTopic,
MemoryCacheService memoryCacheService) {
mySubscriptionTopicSupport = theSubscriptionTopicSupport; mySubscriptionTopicSupport = theSubscriptionTopicSupport;
myTopic = theTopic; myTopic = theTopic;
myMemoryCacheService = memoryCacheService;
} }
public InMemoryMatchResult match(ResourceModifiedMessage theMsg) { public InMemoryMatchResult match(ResourceModifiedMessage theMsg) {
@ -43,7 +49,7 @@ public class SubscriptionTopicMatcher {
for (SubscriptionTopic.SubscriptionTopicResourceTriggerComponent next : triggers) { for (SubscriptionTopic.SubscriptionTopicResourceTriggerComponent next : triggers) {
if (resourceName.equals(next.getResource())) { if (resourceName.equals(next.getResource())) {
SubscriptionTriggerMatcher matcher = SubscriptionTriggerMatcher matcher =
new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next); new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next, myMemoryCacheService);
InMemoryMatchResult result = matcher.match(); InMemoryMatchResult result = matcher.match();
if (result.matched()) { if (result.matched()) {
// as soon as one trigger matches, we're done // 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.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.topic.filter.InMemoryTopicFilterMatcher; 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.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc;
import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.util.Logs;
@ -67,8 +68,11 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
@Autowired @Autowired
private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc;
public SubscriptionTopicMatchingSubscriber(FhirContext theFhirContext) { private MemoryCacheService myMemoryCacheService;
public SubscriptionTopicMatchingSubscriber(FhirContext theFhirContext, MemoryCacheService memoryCacheService) {
myFhirContext = theFhirContext; myFhirContext = theFhirContext;
this.myMemoryCacheService = memoryCacheService;
} }
@Override @Override
@ -110,7 +114,8 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler {
Collection<SubscriptionTopic> topics = mySubscriptionTopicRegistry.getAll(); Collection<SubscriptionTopic> topics = mySubscriptionTopicRegistry.getAll();
for (SubscriptionTopic topic : topics) { for (SubscriptionTopic topic : topics) {
SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic); SubscriptionTopicMatcher matcher =
new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic, myMemoryCacheService);
InMemoryMatchResult result = matcher.match(theMsg); InMemoryMatchResult result = matcher.match(theMsg);
if (result.matched()) { if (result.matched()) {
int deliveries = deliverToTopicSubscriptions(theMsg, topic, result); int deliveries = deliverToTopicSubscriptions(theMsg, topic, result);

View File

@ -19,17 +19,26 @@
*/ */
package ca.uhn.fhir.jpa.topic; 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.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; 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.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage; import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage;
import ca.uhn.fhir.storage.PreviousVersionReader; import ca.uhn.fhir.storage.PreviousVersionReader;
import ca.uhn.fhir.util.Logs; 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.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.Enumeration; import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.SubscriptionTopic; import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.helpers.MessageFormatter;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -45,11 +54,13 @@ public class SubscriptionTriggerMatcher {
private final IFhirResourceDao myDao; private final IFhirResourceDao myDao;
private final PreviousVersionReader myPreviousVersionReader; private final PreviousVersionReader myPreviousVersionReader;
private final SystemRequestDetails mySrd; private final SystemRequestDetails mySrd;
private final MemoryCacheService myMemoryCacheService;
public SubscriptionTriggerMatcher( public SubscriptionTriggerMatcher(
SubscriptionTopicSupport theSubscriptionTopicSupport, SubscriptionTopicSupport theSubscriptionTopicSupport,
ResourceModifiedMessage theMsg, ResourceModifiedMessage theMsg,
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent theTrigger) { SubscriptionTopic.SubscriptionTopicResourceTriggerComponent theTrigger,
MemoryCacheService theMemoryCacheService) {
mySubscriptionTopicSupport = theSubscriptionTopicSupport; mySubscriptionTopicSupport = theSubscriptionTopicSupport;
myOperation = theMsg.getOperationType(); myOperation = theMsg.getOperationType();
myResource = theMsg.getPayload(theSubscriptionTopicSupport.getFhirContext()); myResource = theMsg.getPayload(theSubscriptionTopicSupport.getFhirContext());
@ -58,6 +69,7 @@ public class SubscriptionTriggerMatcher {
myTrigger = theTrigger; myTrigger = theTrigger;
myPreviousVersionReader = new PreviousVersionReader(myDao); myPreviousVersionReader = new PreviousVersionReader(myDao);
mySrd = new SystemRequestDetails(); mySrd = new SystemRequestDetails();
myMemoryCacheService = theMemoryCacheService;
} }
public InMemoryMatchResult match() { public InMemoryMatchResult match() {
@ -66,21 +78,22 @@ public class SubscriptionTriggerMatcher {
if (SubscriptionTopicUtil.matches(myOperation, supportedInteractions)) { if (SubscriptionTopicUtil.matches(myOperation, supportedInteractions)) {
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent queryCriteria = SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent queryCriteria =
myTrigger.getQueryCriteria(); myTrigger.getQueryCriteria();
InMemoryMatchResult result = match(queryCriteria); String fhirPathCriteria = myTrigger.getFhirPathCriteria();
if (result.matched()) { return match(queryCriteria, fhirPathCriteria);
return result;
}
} }
return InMemoryMatchResult.noMatch(); return InMemoryMatchResult.noMatch();
} }
private InMemoryMatchResult match( private InMemoryMatchResult match(
SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) { SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria,
String theFhirPathCriteria) {
String previousCriteria = theQueryCriteria.getPrevious(); String previousCriteria = theQueryCriteria.getPrevious();
String currentCriteria = theQueryCriteria.getCurrent(); String currentCriteria = theQueryCriteria.getCurrent();
InMemoryMatchResult previousMatches = InMemoryMatchResult.fromBoolean(previousCriteria == null); InMemoryMatchResult previousMatches = InMemoryMatchResult.fromBoolean(previousCriteria == null);
InMemoryMatchResult currentMatches = InMemoryMatchResult.fromBoolean(currentCriteria == null); InMemoryMatchResult currentMatches = InMemoryMatchResult.fromBoolean(currentCriteria == null);
InMemoryMatchResult fhirPathCriteriaEvaluationResult = evaluateFhirPathCriteria(theFhirPathCriteria);
// WIP STR5 implement fhirPathCriteria per https://build.fhir.org/subscriptiontopic.html#fhirpath-criteria // WIP STR5 implement fhirPathCriteria per https://build.fhir.org/subscriptiontopic.html#fhirpath-criteria
if (currentCriteria != null) { if (currentCriteria != null) {
currentMatches = matchResource(myResource, currentCriteria); currentMatches = matchResource(myResource, currentCriteria);
@ -105,12 +118,89 @@ public class SubscriptionTriggerMatcher {
} }
// WIP STR5 implement resultForCreate and resultForDelete // WIP STR5 implement resultForCreate and resultForDelete
if (theQueryCriteria.getRequireBoth()) { if (theQueryCriteria.getRequireBoth()) {
return InMemoryMatchResult.and(previousMatches, currentMatches); return InMemoryMatchResult.and(
InMemoryMatchResult.and(previousMatches, currentMatches), fhirPathCriteriaEvaluationResult);
} else { } 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) { private InMemoryMatchResult matchResource(IBaseResource theResource, String theCriteria) {
InMemoryMatchResult result = InMemoryMatchResult result =
mySubscriptionTopicSupport.getSearchParamMatcher().match(theCriteria, theResource, mySrd); mySubscriptionTopicSupport.getSearchParamMatcher().match(theCriteria, theResource, mySrd);

View File

@ -1,12 +1,15 @@
package ca.uhn.fhir.jpa.topic; package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext; 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.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; 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.Encounter;
import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.SubscriptionTopic; import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -15,7 +18,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
@ -30,11 +36,14 @@ class SubscriptionTriggerMatcherTest {
@Mock @Mock
SearchParamMatcher mySearchParamMatcher; SearchParamMatcher mySearchParamMatcher;
MemoryCacheService myMemoryCacheService;
private SubscriptionTopicSupport mySubscriptionTopicSupport; private SubscriptionTopicSupport mySubscriptionTopicSupport;
private Encounter myEncounter; private Encounter myEncounter;
@BeforeEach @BeforeEach
public void before() { public void before() {
myMemoryCacheService = new MemoryCacheService(new JpaStorageSettings());
mySubscriptionTopicSupport = new SubscriptionTopicSupport(ourFhirContext, myDaoRegistry, mySearchParamMatcher); mySubscriptionTopicSupport = new SubscriptionTopicSupport(ourFhirContext, myDaoRegistry, mySearchParamMatcher);
myEncounter = new Encounter(); myEncounter = new Encounter();
myEncounter.setIdElement(new IdType("Encounter", "123", "2")); myEncounter.setIdElement(new IdType("Encounter", "123", "2"));
@ -48,7 +57,7 @@ class SubscriptionTriggerMatcherTest {
SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent(); SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = new SubscriptionTopic.SubscriptionTopicResourceTriggerComponent();
// run // run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger); SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match(); InMemoryMatchResult result = svc.match();
// verify // verify
@ -65,7 +74,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.CREATE); trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.CREATE);
// run // run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger); SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match(); InMemoryMatchResult result = svc.match();
// verify // verify
@ -82,7 +91,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE); trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
// run // run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger); SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match(); InMemoryMatchResult result = svc.match();
// verify // verify
@ -99,7 +108,7 @@ class SubscriptionTriggerMatcherTest {
trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE); trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE);
// run // run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger); SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match(); InMemoryMatchResult result = svc.match();
// verify // verify
@ -124,11 +133,253 @@ class SubscriptionTriggerMatcherTest {
when(mySearchParamMatcher.match(any(), any(), any())).thenReturn(InMemoryMatchResult.successfulMatch()); when(mySearchParamMatcher.match(any(), any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
// run // run
SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger); SubscriptionTriggerMatcher svc = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, msg, trigger, myMemoryCacheService);
InMemoryMatchResult result = svc.match(); InMemoryMatchResult result = svc.match();
// verify // verify
assertTrue(result.matched()); 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 HISTORY_COUNT:
case TAG_DEFINITION: case TAG_DEFINITION:
case RESOURCE_CONDITIONAL_CREATE_VERSION: case RESOURCE_CONDITIONAL_CREATE_VERSION:
case FHIRPATH_EXPRESSION:
default: default:
timeoutSeconds = SECONDS.convert(1, MINUTES); timeoutSeconds = SECONDS.convert(1, MINUTES);
maximumSize = 10000; maximumSize = 10000;
@ -193,6 +194,7 @@ public class MemoryCacheService {
TAG_DEFINITION(TagDefinitionCacheKey.class), TAG_DEFINITION(TagDefinitionCacheKey.class),
RESOURCE_LOOKUP(String.class), RESOURCE_LOOKUP(String.class),
FORCED_ID_TO_PID(String.class), FORCED_ID_TO_PID(String.class),
FHIRPATH_EXPRESSION(String.class),
/** /**
* Key type: {@literal Long} * Key type: {@literal Long}
* Value type: {@literal Optional<String>} * 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.IdType;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
public class FhirPathR5 implements IFhirPath { public class FhirPathR5 implements IFhirPath {
@ -99,7 +101,11 @@ public class FhirPathR5 implements IFhirPath {
boolean beforeContext, boolean beforeContext,
boolean explicitConstant) boolean explicitConstant)
throws PathEngineException { throws PathEngineException {
return null;
return Collections.unmodifiableList(
theEvaluationContext.resolveConstant(appContext, name, beforeContext).stream()
.map(Base.class::cast)
.collect(Collectors.toList()));
} }
@Override @Override