Subscription strategy tag (#1178)

tests pass
This commit is contained in:
Ken Stevens 2019-01-25 13:01:04 -05:00 committed by GitHub
parent f55be0b6d0
commit 10c59fceeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 390 additions and 142 deletions

View File

@ -1,6 +1,9 @@
package ca.uhn.fhir.util;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.escape.Escaper;
@ -172,8 +175,13 @@ public class UrlUtil {
return true;
}
public static void main(String[] args) {
System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005"));
public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException {
int paramIndex = theUrl.indexOf('?');
String resourceName = theUrl.substring(0, paramIndex);
if (resourceName.contains("/")) {
resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
}
return theCtx.getResourceDefinition(resourceName);
}
public static Map<String, String[]> parseQueryString(String theQueryString) {

View File

@ -26,9 +26,9 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.util.UrlUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@ -77,7 +77,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private void refreshNow(WarmCacheEntry theCacheEntry) {
String nextUrl = theCacheEntry.getUrl();
RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl);
RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myCtx, nextUrl);
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName());
String queryPart = parseWarmUrlParamPart(nextUrl);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef);
@ -93,20 +93,6 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
return theNextUrl.substring(paramIndex);
}
/**
* TODO: this method probably belongs in a utility class, not here
*
* @throws DataFormatException If the resource type is not known
*/
public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException {
int paramIndex = theUrl.indexOf('?');
String resourceName = theUrl.substring(0, paramIndex);
if (resourceName.contains("/")) {
resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
}
return theCtx.getResourceDefinition(resourceName);
}
@PostConstruct
public void start() {
initCacheMap();
@ -120,7 +106,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
// Validate
parseWarmUrlParamPart(next.getUrl());
parseUrlResourceType(myCtx, next.getUrl());
UrlUtil.parseUrlResourceType(myCtx, next.getUrl());
myCacheEntryToNextRefresh.put(next, 0L);
}

View File

@ -29,17 +29,17 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.interceptor.api.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionCanonicalizer;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionStrategyEvaluator;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.SubscriptionUtil;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.Subscription;
@ -96,6 +96,8 @@ public class SubscriptionActivatingInterceptor {
private MatchUrlService myMatchUrlService;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
// Grab the value for "Subscription.channel.type" so we can see if this
@ -160,7 +162,6 @@ public class SubscriptionActivatingInterceptor {
}
}
private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement());
@ -185,6 +186,36 @@ public class SubscriptionActivatingInterceptor {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
@Hook(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED)
public void addStrategyTagCreated(IBaseResource theResource) {
if (isSubscription(theResource)) {
validateCriteriaAndAddStrategy(theResource);
}
}
@Hook(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED)
public void addStrategyTagUpdated(IBaseResource theOldResource, IBaseResource theNewResource) {
if (isSubscription(theNewResource)) {
validateCriteriaAndAddStrategy(theNewResource);
}
}
// TODO KHS add third type of strategy DISABLED if that subscription type is disabled on this server
public void validateCriteriaAndAddStrategy(final IBaseResource theResource) {
String criteria = mySubscriptionCanonicalizer.getCriteria(theResource);
try {
SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(criteria);
mySubscriptionCanonicalizer.setMatchingStrategyTag(myFhirContext, theResource, strategy);
} catch (InvalidRequestException | DataFormatException e) {
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage());
}
}
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED)
public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void resourceCreated(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
@ -195,46 +226,31 @@ public class SubscriptionActivatingInterceptor {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
}
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED)
public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
if (isSubscription(theNewResource)) {
submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType));
}
}
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType));
private boolean isSubscription(IBaseResource theNewResource) {
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theNewResource);
return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(resourceDefinition.getName());
}
private void submitResourceModified(final ResourceModifiedMessage theMsg) {
IIdType id = theMsg.getId(myFhirContext);
if (!id.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) {
return;
}
switch (theMsg.getOperationType()) {
case DELETE:
mySubscriptionRegistry.unregisterSubscription(id);
mySubscriptionRegistry.unregisterSubscription(theMsg.getId(myFhirContext));
break;
case CREATE:
case UPDATE:
final IBaseResource subscription = theMsg.getNewPayload(myFhirContext);
validateCriteria(subscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(subscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(theMsg.getNewPayload(myFhirContext));
break;
default:
break;
}
}
public void validateCriteria(final IBaseResource theResource) {
CanonicalSubscription subscription = mySubscriptionCanonicalizer.canonicalize(theResource);
String criteria = subscription.getCriteriaString();
try {
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, criteria);
myMatchUrlService.translateMatchUrl(criteria, resourceDef);
} catch (InvalidRequestException e) {
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage());
}
}
private void activateAndRegisterSubscriptionIfRequiredInTransaction(IBaseResource theSubscription) {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {

View File

@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
@ -42,6 +41,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
@ -209,7 +209,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
// If we don't have an active search started, and one needs to be.. start it
if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) {
String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0);
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, nextSearchUrl);
RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myFhirContext, nextSearchUrl);
String queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?'));
String resourceType = resourceDef.getName();

View File

@ -24,8 +24,8 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -48,7 +48,10 @@ public class CompositeInMemoryDaoSubscriptionMatcher implements ISubscriptionMat
SubscriptionMatchResult result;
if (myDaoConfig.isEnableInMemorySubscriptionMatching()) {
result = myInMemorySubscriptionMatcher.match(theSubscription, theMsg);
if (!result.supported()) {
if (result.supported()) {
// TODO KHS test
result.setInMemory(true);
} else {
ourLog.info("Criteria {} for Subscription {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", theSubscription.getCriteriaString(), theSubscription.getIdElementString(), result.getUnsupportedReason());
result = myDaoSubscriptionMatcher.match(theSubscription, theMsg);
}

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
@ -32,7 +31,6 @@ import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
@ -63,13 +61,13 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher {
ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria);
return new SubscriptionMatchResult(results.size() > 0, "DATABASE");
return SubscriptionMatchResult.fromBoolean(results.size() > 0);
}
/**
* Search based on a query criteria
*/
protected IBundleProvider performSearch(String theCriteria) {
private IBundleProvider performSearch(String theCriteria) {
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao();
RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef);

View File

@ -7,7 +7,6 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.StringUtils;
@ -36,28 +35,41 @@ public class InMemorySubscriptionMatcherTestR4 {
@Autowired
InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
@Autowired
SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
@Autowired
FhirContext myContext;
private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) {
String criteria = params.toNormalizedQueryString(myContext);
ourLog.info("Criteria: <{}>", criteria);
return myInMemorySubscriptionMatcher.match(criteria, resource);
}
private void assertUnsupported(IBaseResource resource, SearchParameterMap params) {
assertFalse(match(resource, params).supported());
}
private void assertMatched(IBaseResource resource, SearchParameterMap params) {
private void assertMatched(Resource resource, SearchParameterMap params) {
SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, params)));
}
private void assertNotMatched(IBaseResource resource, SearchParameterMap params) {
private void assertNotMatched(Resource resource, SearchParameterMap params) {
SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported());
assertFalse(result.matched());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, params)));
}
private SubscriptionMatchResult match(Resource theResource, SearchParameterMap theParams) {
return match(getCriteria(theResource, theParams), theResource);
}
private String getCriteria(Resource theResource, SearchParameterMap theParams) {
return theResource.getResourceType().name() + theParams.toNormalizedQueryString(myContext);
}
private SubscriptionMatchResult match(String criteria, Resource theResource) {
ourLog.info("Criteria: <{}>", criteria);
return myInMemorySubscriptionMatcher.match(criteria, theResource);
}
private void assertUnsupported(Resource resource, SearchParameterMap theParams) {
SubscriptionMatchResult result = match(resource, theParams);
assertFalse(result.supported());
assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, theParams)));
}
/*
@ -93,7 +105,6 @@ public class InMemorySubscriptionMatcherTestR4 {
SearchParameterMap params = new SearchParameterMap();
params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO"));
String criteria = params.toNormalizedQueryString(myContext);
assertUnsupported(patient, params);
}
@ -130,7 +141,7 @@ public class InMemorySubscriptionMatcherTestR4 {
TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01");
StringParam v1 = new StringParam("testSearchCompositeParamS01");
CompositeParam<TokenParam, StringParam> val = new CompositeParam<TokenParam, StringParam>(v0, v1);
CompositeParam<TokenParam, StringParam> val = new CompositeParam<>(v0, v1);
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val);
assertUnsupported(o1, params);
}
@ -170,11 +181,11 @@ public class InMemorySubscriptionMatcherTestR4 {
}
@Test
public void testIdNotSupported() {
public void testIdSupported() {
Observation o1 = new Observation();
SearchParameterMap params = new SearchParameterMap();
params.add("_id", new StringParam("testSearchForUnknownAlphanumericId"));
assertUnsupported(o1, params);
assertNotMatched(o1, params);
}
@Test
@ -190,7 +201,7 @@ public class InMemorySubscriptionMatcherTestR4 {
}
@Test
public void testSearchLastUpdatedParamUnsupported() throws InterruptedException {
public void testSearchLastUpdatedParamUnsupported() {
String methodName = "testSearchLastUpdatedParam";
DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY);
Patient patient = new Patient();
@ -294,12 +305,12 @@ public class InMemorySubscriptionMatcherTestR4 {
@Test
public void testSearchQuantityWrongParam() {
Condition c1 = new Condition();
c1.setAbatement(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L)));
c1.setAbatement(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L)));
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1"));
assertMatched(c1, params);
Condition c2 = new Condition();
c2.setOnset(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L)));
c2.setOnset(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L)));
params = new SearchParameterMap().add(Condition.SP_ONSET_AGE, new QuantityParam("1"));
assertMatched(c2, params);
@ -414,7 +425,7 @@ public class InMemorySubscriptionMatcherTestR4 {
}
@Test
public void testSearchStringParam() throws Exception {
public void testSearchStringParam() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe");
@ -569,13 +580,11 @@ public class InMemorySubscriptionMatcherTestR4 {
@Test
public void testSearchTokenWithNotModifierUnsupported() {
String male, female;
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe");
patient.setGender(Enumerations.AdministrativeGender.MALE);
List<String> patients;
SearchParameterMap params;
params = new SearchParameterMap();
@ -640,7 +649,6 @@ public class InMemorySubscriptionMatcherTestR4 {
o2.setValue(q2);
SearchParameterMap map;
IBundleProvider found;
QuantityParam param;
map = new SearchParameterMap();
@ -682,9 +690,7 @@ public class InMemorySubscriptionMatcherTestR4 {
Patient pt1 = new Patient();
pt1.addName().setFamily("ABCDEFGHIJK");
List<String> ids;
SearchParameterMap map;
IBundleProvider results;
// Contains = true
map = new SearchParameterMap();
@ -875,7 +881,7 @@ public class InMemorySubscriptionMatcherTestR4 {
map.add(Observation.SP_DATE, new DateParam("2011-01-02"));
for (Observation obs : nlist) {
// assertNotMatched(obs, map);
assertNotMatched(obs, map);
}
for (Observation obs : ylist) {
ourLog.info("Obs {} has time {}", obs.getId(), obs.getEffectiveDateTimeType().getValue().toString());

View File

@ -6,6 +6,8 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.NotificationServlet;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
@ -106,12 +108,11 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
subscription.setChannel(channel);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId());
waitForQueueToDrain();
return subscription;
return (Subscription)methodOutcome.getResource();
}
private Observation sendObservation(String code, String system) {
@ -352,7 +353,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations);
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertNotEquals(subscription1.getId(), subscription2.getId());
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());
}
@ -414,6 +415,58 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
mySubscriptionTestUtil.waitForQueueToDrain();
}
@Test
public void testSubscriptionActivatesInMemoryTag() throws Exception {
String payload = "application/fhir+xml";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription subscriptionOrig = createSubscription(criteria1, payload, ourListenerServerBase);
IdType subscriptionId = subscriptionOrig.getIdElement();
assertEquals(Subscription.SubscriptionStatus.REQUESTED, subscriptionOrig.getStatus());
List<Coding> tags = subscriptionOrig.getMeta().getTag();
assertEquals(1, tags.size());
Coding tag = tags.get(0);
assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode());
assertEquals("In-memory", tag.getDisplay());
Subscription subscriptionActivated = ourClient.read().resource(Subscription.class).withId(subscriptionId.toUnqualifiedVersionless()).execute();
assertEquals(Subscription.SubscriptionStatus.ACTIVE, subscriptionActivated.getStatus());
tags = subscriptionActivated.getMeta().getTag();
assertEquals(1, tags.size());
tag = tags.get(0);
assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode());
assertEquals("In-memory", tag.getDisplay());
}
@Test
public void testSubscriptionActivatesDatabaseTag() throws Exception {
String payload = "application/fhir+xml";
Subscription subscriptionOrig = createSubscription("Observation?code=17861-6&context.type=IHD", payload, ourListenerServerBase);
IdType subscriptionId = subscriptionOrig.getIdElement();
List<Coding> tags = subscriptionOrig.getMeta().getTag();
assertEquals(1, tags.size());
Coding tag = tags.get(0);
assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode());
assertEquals("Database", tag.getDisplay());
Subscription subscription = ourClient.read().resource(Subscription.class).withId(subscriptionId.toUnqualifiedVersionless()).execute();
assertEquals(Subscription.SubscriptionStatus.ACTIVE, subscription.getStatus());
tags = subscription.getMeta().getTag();
assertEquals(1, tags.size());
tag = tags.get(0);
assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem());
assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode());
assertEquals("Database", tag.getDisplay());
}
@BeforeClass
public static void startListenerServer() throws Exception {
ourListenerPort = PortUtil.findFreePort();

View File

@ -86,7 +86,7 @@ public enum Pointcut {
SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"),
/**
* Invoked before a resource will be updated, immediately before the resource
* Invoked before a resource will be created, immediately before the resource
* is persisted to the database.
* <p>
* Hooks will have access to the contents of the resource being created

View File

@ -24,12 +24,14 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import org.hl7.fhir.dstu3.model.Subscription;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension;
@ -261,4 +263,33 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> {
return retVal;
}
public String getCriteria(IBaseResource theSubscription) {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU2:
return ((ca.uhn.fhir.model.dstu2.resource.Subscription)theSubscription).getCriteria();
case DSTU3:
return ((org.hl7.fhir.dstu3.model.Subscription)theSubscription).getCriteria();
case R4:
return ((org.hl7.fhir.r4.model.Subscription)theSubscription).getCriteria();
default:
throw new ConfigurationException("Subscription not supported for version: " + myFhirContext.getVersion().getVersion());
}
}
public void setMatchingStrategyTag(FhirContext theFhirContext, IBaseResource theSubscription, SubscriptionMatchingStrategy theStrategy) {
IBaseMetaType meta = theSubscription.getMeta();
String value = theStrategy.toString();
String display;
if (theStrategy == SubscriptionMatchingStrategy.DATABASE) {
display = "Database";
} else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) {
display = "In-memory";
} else {
throw new IllegalStateException("Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": "+theStrategy);
}
meta.addTag().setSystem(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display);
}
}

View File

@ -67,6 +67,13 @@ public class SubscriptionConstants {
*/
public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version";
/**
* Indicate which strategy will be used to match this subscription
*/
public static final String EXT_SUBSCRIPTION_MATCHING_STRATEGY = "http://hapifhir.io/fhir/StructureDefinition/subscription-matching-strategy";
/**
* The number of threads used in subscription channel processing
*/
@ -79,12 +86,8 @@ public class SubscriptionConstants {
public static final int MAX_SUBSCRIPTION_RESULTS = 1000;
/**
* The size of the queue used for sending resources to the subscription matching processor
* The size of the queue used for sending resources to the subscription matching processor and by each subscription delivery queue
*/
public static final int PROCESSING_EXECUTOR_QUEUE_SIZE = 1000;
/**
* The size of the queue used by each subscription delivery queue
*/
public static final int DELIVERY_EXECUTOR_QUEUE_SIZE = 1000;
}

View File

@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -54,79 +55,93 @@ public class CriteriaResourceMatcher {
@Autowired
FhirContext myFhirContext;
/**
* This method is called in two different scenarios. With a null theResource, it determines whether database matching might be required.
* Otherwise, it tries to perform the match in-memory, returning UNSUPPORTED if it's not possible.
*
* Note that there will be cases where it returns UNSUPPORTED with a null resource, but when a non-null resource it returns supported and no match.
* This is because an earlier parameter may be matchable in-memory in which case processing stops and we never get to the parameter
* that would have required a database call.
*
*/
public SubscriptionMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
RuntimeResourceDefinition resourceDefinition;
if (theResource == null) {
resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theCriteria);
} else {
resourceDefinition = myFhirContext.getResourceDefinition(theResource);
}
SearchParameterMap searchParameterMap;
try {
searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, myFhirContext.getResourceDefinition(theResource));
searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, resourceDefinition);
} catch (UnsupportedOperationException e) {
return new SubscriptionMatchResult(theCriteria, CRITERIA);
return SubscriptionMatchResult.unsupportedFromReason(SubscriptionMatchResult.PARSE_FAIL);
}
searchParameterMap.clean();
if (searchParameterMap.getLastUpdated() != null) {
return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Standard Parameters not supported");
return SubscriptionMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, SubscriptionMatchResult.STANDARD_PARAMETER);
}
for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) {
String theParamName = entry.getKey();
List<List<? extends IQueryParameterType>> theAndOrParams = entry.getValue();
SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResource, theSearchParams);
SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, resourceDefinition, theResource, theSearchParams);
if (!result.matched()){
return result;
}
}
return new SubscriptionMatchResult(true, CRITERIA);
return SubscriptionMatchResult.successfulMatch();
}
// This method is modelled from SearchBuilder.searchForIdsWithAndOr()
private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
if (theAndOrParams.isEmpty()) {
return new SubscriptionMatchResult(true, CRITERIA);
return SubscriptionMatchResult.successfulMatch();
}
if (hasQualifiers(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Standard Parameters not supported.");
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.STANDARD_PARAMETER);
}
if (hasPrefixes(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Prefixes not supported.");
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PREFIX);
}
if (hasChain(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Chained references are not supported");
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.CHAIN);
}
switch (theParamName) {
case IAnyResource.SP_RES_ID:
return new SubscriptionMatchResult(matchIdsAndOr(theAndOrParams, theResource), CRITERIA);
return SubscriptionMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource));
case IAnyResource.SP_RES_LANGUAGE:
return new SubscriptionMatchResult(theParamName, CRITERIA);
case Constants.PARAM_HAS:
return new SubscriptionMatchResult(theParamName, CRITERIA);
case Constants.PARAM_TAG:
case Constants.PARAM_PROFILE:
case Constants.PARAM_SECURITY:
return new SubscriptionMatchResult(theParamName, CRITERIA);
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
default:
String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
}
}
private boolean matchIdsAndOr(List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
}
return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource));
}
private boolean matchIdsOr(List<? extends IQueryParameterType> theOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
}
return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam)param).getValue(), theResource.getIdElement()));
}
@ -144,16 +159,20 @@ public class CriteriaResourceMatcher {
case URI:
case DATE:
case REFERENCE:
return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)), CRITERIA);
if (theSearchParams == null) {
return SubscriptionMatchResult.successfulMatch();
} else {
return SubscriptionMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
}
case COMPOSITE:
case HAS:
case SPECIAL:
default:
return new SubscriptionMatchResult(theParamName, CRITERIA);
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
}
} else {
if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
return new SubscriptionMatchResult(theParamName, CRITERIA);
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
} else {
throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -21,26 +21,47 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
*/
public class SubscriptionMatchResult {
public static final String PARSE_FAIL = "Failed to translate parse query string";
public static final String STANDARD_PARAMETER = "Standard parameters not supported";
public static final String PREFIX = "Prefixes not supported";
public static final String CHAIN = "Chained references are not supported";
public static final String PARAM = "Param not supported";
private final boolean myMatch;
private final boolean mySupported;
private final String myUnsupportedParameter;
private final String myUnsupportedReason;
private final String myMatcherShortName;
public SubscriptionMatchResult(boolean theMatch, String theMatcherShortName) {
private boolean myInMemory = false;
private SubscriptionMatchResult(boolean theMatch) {
this.myMatch = theMatch;
this.mySupported = true;
this.myUnsupportedParameter = null;
this.myUnsupportedReason = null;
this.myMatcherShortName = theMatcherShortName;
}
public SubscriptionMatchResult(String theUnsupportedParameter, String theMatcherShortName) {
private SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = "Parameter not supported";
this.myMatcherShortName = theMatcherShortName;
this.myUnsupportedReason = theUnsupportedReason;
}
public static SubscriptionMatchResult successfulMatch() {
return new SubscriptionMatchResult(true);
}
public static SubscriptionMatchResult fromBoolean(boolean theMatched) {
return new SubscriptionMatchResult(theMatched);
}
public static SubscriptionMatchResult unsupportedFromReason(String theUnsupportedReason) {
return new SubscriptionMatchResult(null, theUnsupportedReason);
}
public static SubscriptionMatchResult unsupportedFromParameterAndReason(String theUnsupportedParameter, String theUnsupportedReason) {
return new SubscriptionMatchResult(theUnsupportedParameter, theUnsupportedReason);
}
public boolean supported() {
@ -52,14 +73,17 @@ public class SubscriptionMatchResult {
}
public String getUnsupportedReason() {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason;
if (myUnsupportedParameter != null) {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason;
}
return myUnsupportedReason;
}
/**
* Returns a short name of the matcher that generated this
* response, for use in logging
*/
public String matcherShortName() {
return myMatcherShortName;
public boolean isInMemory() {
return myInMemory;
}
public void setInMemory(boolean theInMemory) {
myInMemory = theInMemory;
}
}

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.jpa.subscription.module.matcher;
public enum SubscriptionMatchingStrategy {
/**
* Resources can be matched against this subcription in-memory without needing to make a call out to a FHIR Repository
*/
IN_MEMORY,
/**
* Resources cannot be matched against this subscription in-memory. We need to make a call to a FHIR Repository to determine a match
*/
DATABASE
}

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.jpa.subscription.module.matcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SubscriptionStrategyEvaluator {
@Autowired
private CriteriaResourceMatcher myCriteriaResourceMatcher;
public SubscriptionMatchingStrategy determineStrategy(String theCriteria) {
SubscriptionMatchResult result = myCriteriaResourceMatcher.match(theCriteria, null, null);
if (result.supported()) {
return SubscriptionMatchingStrategy.IN_MEMORY;
}
return SubscriptionMatchingStrategy.DATABASE;
}
}

View File

@ -117,8 +117,10 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
if (!matchResult.matched()) {
continue;
}
ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), resourceId.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName());
ourLog.debug("Subscription {} was matched by resource {} {}",
nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(),
resourceId.toUnqualifiedVersionless().getValue(),
matchResult.isInMemory() ? "in-memory" : "by querying the repository");
IBaseResource payload = theMsg.getNewPayload(myFhirContext);
@ -161,7 +163,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
criteriaResource = criteriaResource.substring(0, criteriaResource.indexOf("?"));
}
if (resourceType != null && criteriaString != null && !criteriaResource.equals(resourceType)) {
if (resourceType != null && !criteriaResource.equals(resourceType)) {
ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, criteriaString);
return false;
}

View File

@ -7,22 +7,22 @@ import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.codesystems.MedicationRequestCategory;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test {
@Autowired
SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
@Autowired
InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
private void assertUnsupported(IBaseResource resource, String criteria) {
assertFalse(myInMemorySubscriptionMatcher.match(criteria, resource).supported());
assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
}
private void assertMatched(IBaseResource resource, String criteria) {
@ -30,13 +30,20 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
assertTrue(result.supported());
assertTrue(result.matched());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
}
private void assertNotMatched(IBaseResource resource, String criteria) {
assertNotMatched(resource, criteria, SubscriptionMatchingStrategy.IN_MEMORY);
}
private void assertNotMatched(IBaseResource resource, String criteria, SubscriptionMatchingStrategy theSubscriptionMatchingStrategy) {
SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, resource);
assertTrue(result.supported());
assertFalse(result.matched());
assertEquals(theSubscriptionMatchingStrategy, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
}
@ -152,7 +159,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE);
}
{
Observation obs = new Observation();
@ -168,7 +175,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE);
}
{
Observation obs = new Observation();
@ -266,7 +273,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE);
}
}

View File

@ -0,0 +1,53 @@
package ca.uhn.fhir.jpa.subscription.module.matcher;
import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.Assert.assertEquals;
import static org.junit.matchers.JUnitMatchers.containsString;
public class SubscriptionStrategyEvaluatorTest extends BaseSubscriptionDstu3Test {
@Autowired
SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void testInMemory() {
assertInMemory("Observation?");
assertInMemory("QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord");
assertInMemory("CommunicationRequest?occurrence==2018-10-17");
assertInMemory("ProcedureRequest?intent=original-order");
assertInMemory("MedicationRequest?intent=instance-order&category=outpatient&date==2018-10-19");
assertInMemory("MedicationRequest?intent=plan&category=outpatient&status=suspended,entered-in-error,cancelled,stopped");
assertDatabase("Observation?code=FR_Org1Blood2nd,FR_Org1Blood3rd,FR_Org%201BldCult,FR_Org2Blood2nd,FR_Org2Blood3rd,FR_Org%202BldCult,FR_Org3Blood2nd,FR_Org3Blood3rd,FR_Org3BldCult,FR_Org4Blood2nd,FR_Org4Blood3rd,FR_Org4BldCult,FR_Org5Blood2nd,FR_Org5Blood3rd,FR_Org%205BldCult,FR_Org6Blood2nd,FR_Org6Blood3rd,FR_Org6BldCult,FR_Org7Blood2nd,FR_Org7Blood3rd,FR_Org7BldCult,FR_Org8Blood2nd,FR_Org8Blood3rd,FR_Org8BldCult,FR_Org9Blood2nd,FR_Org9Blood3rd,FR_Org9BldCult,FR_Bld2ndCulture,FR_Bld3rdCulture,FR_Blood%20Culture,FR_Com1Bld3rd,FR_Com1BldCult,FR_Com2Bld2nd,FR_Com2Bld3rd,FR_Com2BldCult,FR_CultureBld2nd,FR_CultureBld3rd,FR_CultureBldCul,FR_GmStainBldCul,FR_GramStain2Bld,FR_GramStain3Bld,FR_GramStNegBac&context.type=IHD");
assertInMemory("Procedure?category=Hemodialysis");
assertInMemory("Procedure?code=HD_Standard&status=completed&location=Lab123");
assertInMemory("Procedure?code=HD_Standard&status=completed");
assertInMemory("QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord,FMCSWDepressionSymptomsScreener,FMCAKIComprehensiveSW,FMCSWIntensiveScreener,FMCESRDComprehensiveSW,FMCNutritionProgressNote,FMCAKIComprehensiveRN");
assertInMemory("EpisodeOfCare?status=active");
assertInMemory("Observation?code=111111111&_format=xml");
assertInMemory("Observation?code=SNOMED-CT|123&_format=xml");
assertDatabase("Observation?code=17861-6&context.type=IHD");
assertDatabase("Observation?context.type=IHD&code=17861-6");
exception.expect(InvalidRequestException.class);
exception.expectMessage(containsString("Resource type Observation does not have a parameter with name: codeee"));
assertInMemory("Observation?codeee=SNOMED-CT|123&_format=xml");
}
private void assertDatabase(String theCriteria) {
assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(theCriteria));
}
private void assertInMemory(String theCriteria) {
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(theCriteria));
}
}

View File

@ -327,6 +327,12 @@
The casing of the base64Binary datatype was incorrect in the DSTU3 and R4 model classes.
This has been corrected.
</action>
<action type="add">
Add a "subscription-matching-strategy" meta tag to incoming subscriptions with value of IN_MEMORY
or DATABASE indicating whether the subscription can be matched against new resources in-memory or
whether a call out to the database may be required. I say "may" because subscription matches fail fast
so a negative match may be performed in-memory, but a positive match will require a database call.
</action>
</release>
<release version="3.6.0" date="2018-11-12" description="Food">
<action type="add">