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; 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.model.primitive.IdDt;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.escape.Escaper; import com.google.common.escape.Escaper;
@ -172,8 +175,13 @@ public class UrlUtil {
return true; return true;
} }
public static void main(String[] args) { public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException {
System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005")); 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) { 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.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; 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.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.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -77,7 +77,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private void refreshNow(WarmCacheEntry theCacheEntry) { private void refreshNow(WarmCacheEntry theCacheEntry) {
String nextUrl = theCacheEntry.getUrl(); String nextUrl = theCacheEntry.getUrl();
RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl); RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myCtx, nextUrl);
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName()); IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName());
String queryPart = parseWarmUrlParamPart(nextUrl); String queryPart = parseWarmUrlParamPart(nextUrl);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef); SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef);
@ -93,20 +93,6 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
return theNextUrl.substring(paramIndex); 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 @PostConstruct
public void start() { public void start() {
initCacheMap(); initCacheMap();
@ -120,7 +106,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
// Validate // Validate
parseWarmUrlParamPart(next.getUrl()); parseWarmUrlParamPart(next.getUrl());
parseUrlResourceType(myCtx, next.getUrl()); UrlUtil.parseUrlResourceType(myCtx, next.getUrl());
myCacheEntryToNextRefresh.put(next, 0L); 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.Hook;
import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor;
import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; 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.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.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionCanonicalizer; 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.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.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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.SubscriptionUtil; import ca.uhn.fhir.util.SubscriptionUtil;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.Subscription; import org.hl7.fhir.instance.model.Subscription;
@ -96,6 +96,8 @@ public class SubscriptionActivatingInterceptor {
private MatchUrlService myMatchUrlService; private MatchUrlService myMatchUrlService;
@Autowired @Autowired
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Autowired
private SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) { public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
// Grab the value for "Subscription.channel.type" so we can see if this // 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) { private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement()); IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement());
@ -185,6 +186,36 @@ public class SubscriptionActivatingInterceptor {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); 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) @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED)
public void resourceCreated(IBaseResource theResource) { public void resourceCreated(IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
@ -195,46 +226,31 @@ public class SubscriptionActivatingInterceptor {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
} }
@Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED) private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { if (isSubscription(theNewResource)) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType));
}
} }
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) { private boolean isSubscription(IBaseResource theNewResource) {
submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType)); RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theNewResource);
return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(resourceDefinition.getName());
} }
private void submitResourceModified(final ResourceModifiedMessage theMsg) { private void submitResourceModified(final ResourceModifiedMessage theMsg) {
IIdType id = theMsg.getId(myFhirContext);
if (!id.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) {
return;
}
switch (theMsg.getOperationType()) { switch (theMsg.getOperationType()) {
case DELETE: case DELETE:
mySubscriptionRegistry.unregisterSubscription(id); mySubscriptionRegistry.unregisterSubscription(theMsg.getId(myFhirContext));
break; break;
case CREATE: case CREATE:
case UPDATE: case UPDATE:
final IBaseResource subscription = theMsg.getNewPayload(myFhirContext); activateAndRegisterSubscriptionIfRequiredInTransaction(theMsg.getNewPayload(myFhirContext));
validateCriteria(subscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(subscription);
break; break;
default: default:
break; 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) { private void activateAndRegisterSubscriptionIfRequiredInTransaction(IBaseResource theSubscription) {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.execute(new TransactionCallbackWithoutResult() { 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.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; 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.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; 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.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.ValidateUtil;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate; 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 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) { if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) {
String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0); 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 queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?'));
String resourceType = resourceDef.getName(); 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.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; 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.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.InMemorySubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -48,7 +48,10 @@ public class CompositeInMemoryDaoSubscriptionMatcher implements ISubscriptionMat
SubscriptionMatchResult result; SubscriptionMatchResult result;
if (myDaoConfig.isEnableInMemorySubscriptionMatching()) { if (myDaoConfig.isEnableInMemorySubscriptionMatching()) {
result = myInMemorySubscriptionMatcher.match(theSubscription, theMsg); 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()); ourLog.info("Criteria {} for Subscription {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", theSubscription.getCriteriaString(), theSubscription.getIdElementString(), result.getUnsupportedReason());
result = myDaoSubscriptionMatcher.match(theSubscription, theMsg); 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.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao; 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.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; 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.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import ca.uhn.fhir.rest.api.server.IBundleProvider; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -63,13 +61,13 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher {
ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria); 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 * Search based on a query criteria
*/ */
protected IBundleProvider performSearch(String theCriteria) { private IBundleProvider performSearch(String theCriteria) {
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao(); IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao();
RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria); RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); 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.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 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.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -36,28 +35,41 @@ public class InMemorySubscriptionMatcherTestR4 {
@Autowired @Autowired
InMemorySubscriptionMatcher myInMemorySubscriptionMatcher; InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
@Autowired @Autowired
SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
@Autowired
FhirContext myContext; FhirContext myContext;
private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) { private void assertMatched(Resource 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) {
SubscriptionMatchResult result = match(resource, params); SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported()); assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched()); 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); SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported()); assertTrue(result.getUnsupportedReason(), result.supported());
assertFalse(result.matched()); 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(); SearchParameterMap params = new SearchParameterMap();
params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO"));
String criteria = params.toNormalizedQueryString(myContext);
assertUnsupported(patient, params); assertUnsupported(patient, params);
} }
@ -130,7 +141,7 @@ public class InMemorySubscriptionMatcherTestR4 {
TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01");
StringParam v1 = new StringParam("testSearchCompositeParamS01"); 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); SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val);
assertUnsupported(o1, params); assertUnsupported(o1, params);
} }
@ -170,11 +181,11 @@ public class InMemorySubscriptionMatcherTestR4 {
} }
@Test @Test
public void testIdNotSupported() { public void testIdSupported() {
Observation o1 = new Observation(); Observation o1 = new Observation();
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("_id", new StringParam("testSearchForUnknownAlphanumericId")); params.add("_id", new StringParam("testSearchForUnknownAlphanumericId"));
assertUnsupported(o1, params); assertNotMatched(o1, params);
} }
@Test @Test
@ -190,7 +201,7 @@ public class InMemorySubscriptionMatcherTestR4 {
} }
@Test @Test
public void testSearchLastUpdatedParamUnsupported() throws InterruptedException { public void testSearchLastUpdatedParamUnsupported() {
String methodName = "testSearchLastUpdatedParam"; String methodName = "testSearchLastUpdatedParam";
DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY); DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY);
Patient patient = new Patient(); Patient patient = new Patient();
@ -294,12 +305,12 @@ public class InMemorySubscriptionMatcherTestR4 {
@Test @Test
public void testSearchQuantityWrongParam() { public void testSearchQuantityWrongParam() {
Condition c1 = new Condition(); 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")); SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1"));
assertMatched(c1, params); assertMatched(c1, params);
Condition c2 = new Condition(); 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")); params = new SearchParameterMap().add(Condition.SP_ONSET_AGE, new QuantityParam("1"));
assertMatched(c2, params); assertMatched(c2, params);
@ -414,7 +425,7 @@ public class InMemorySubscriptionMatcherTestR4 {
} }
@Test @Test
public void testSearchStringParam() throws Exception { public void testSearchStringParam() {
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001"); patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe");
@ -569,13 +580,11 @@ public class InMemorySubscriptionMatcherTestR4 {
@Test @Test
public void testSearchTokenWithNotModifierUnsupported() { public void testSearchTokenWithNotModifierUnsupported() {
String male, female;
Patient patient = new Patient(); Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001"); patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe"); patient.addName().setFamily("Tester").addGiven("Joe");
patient.setGender(Enumerations.AdministrativeGender.MALE); patient.setGender(Enumerations.AdministrativeGender.MALE);
List<String> patients;
SearchParameterMap params; SearchParameterMap params;
params = new SearchParameterMap(); params = new SearchParameterMap();
@ -640,7 +649,6 @@ public class InMemorySubscriptionMatcherTestR4 {
o2.setValue(q2); o2.setValue(q2);
SearchParameterMap map; SearchParameterMap map;
IBundleProvider found;
QuantityParam param; QuantityParam param;
map = new SearchParameterMap(); map = new SearchParameterMap();
@ -682,9 +690,7 @@ public class InMemorySubscriptionMatcherTestR4 {
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.addName().setFamily("ABCDEFGHIJK"); pt1.addName().setFamily("ABCDEFGHIJK");
List<String> ids;
SearchParameterMap map; SearchParameterMap map;
IBundleProvider results;
// Contains = true // Contains = true
map = new SearchParameterMap(); map = new SearchParameterMap();
@ -875,7 +881,7 @@ public class InMemorySubscriptionMatcherTestR4 {
map.add(Observation.SP_DATE, new DateParam("2011-01-02")); map.add(Observation.SP_DATE, new DateParam("2011-01-02"));
for (Observation obs : nlist) { for (Observation obs : nlist) {
// assertNotMatched(obs, map); assertNotMatched(obs, map);
} }
for (Observation obs : ylist) { for (Observation obs : ylist) {
ourLog.info("Obs {} has time {}", obs.getId(), obs.getEffectiveDateTimeType().getValue().toString()); 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.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.NotificationServlet; import ca.uhn.fhir.jpa.subscription.NotificationServlet;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; 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.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Update;
@ -106,12 +108,11 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
subscription.setChannel(channel); subscription.setChannel(channel);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId()); mySubscriptionIds.add(methodOutcome.getId());
waitForQueueToDrain(); waitForQueueToDrain();
return subscription; return (Subscription)methodOutcome.getResource();
} }
private Observation sendObservation(String code, String system) { private Observation sendObservation(String code, String system) {
@ -352,7 +353,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
waitForSize(0, ourCreatedObservations); waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations); waitForSize(5, ourUpdatedObservations);
Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); Assert.assertNotEquals(subscription1.getId(), subscription2.getId());
Assert.assertFalse(observation1.getId().isEmpty()); Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty()); Assert.assertFalse(observation2.getId().isEmpty());
} }
@ -414,6 +415,58 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
mySubscriptionTestUtil.waitForQueueToDrain(); 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 @BeforeClass
public static void startListenerServer() throws Exception { public static void startListenerServer() throws Exception {
ourListenerPort = PortUtil.findFreePort(); ourListenerPort = PortUtil.findFreePort();

View File

@ -86,7 +86,7 @@ public enum Pointcut {
SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"), 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. * is persisted to the database.
* <p> * <p>
* Hooks will have access to the contents of the resource being created * 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.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType; 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.ExtensionDt;
import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription;
import org.hl7.fhir.exceptions.FHIRException; 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.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Extension;
@ -261,4 +263,33 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> {
return retVal; 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"; 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 * The number of threads used in subscription channel processing
*/ */
@ -79,12 +86,8 @@ public class SubscriptionConstants {
public static final int MAX_SUBSCRIPTION_RESULTS = 1000; 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; 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.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 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.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
@ -54,79 +55,93 @@ public class CriteriaResourceMatcher {
@Autowired @Autowired
FhirContext myFhirContext; 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) { 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; SearchParameterMap searchParameterMap;
try { try {
searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, myFhirContext.getResourceDefinition(theResource)); searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, resourceDefinition);
} catch (UnsupportedOperationException e) { } catch (UnsupportedOperationException e) {
return new SubscriptionMatchResult(theCriteria, CRITERIA); return SubscriptionMatchResult.unsupportedFromReason(SubscriptionMatchResult.PARSE_FAIL);
} }
searchParameterMap.clean(); searchParameterMap.clean();
if (searchParameterMap.getLastUpdated() != null) { 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()) { for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) {
String theParamName = entry.getKey(); String theParamName = entry.getKey();
List<List<? extends IQueryParameterType>> theAndOrParams = entry.getValue(); 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()){ if (!result.matched()){
return result; return result;
} }
} }
return new SubscriptionMatchResult(true, CRITERIA); return SubscriptionMatchResult.successfulMatch();
} }
// This method is modelled from SearchBuilder.searchForIdsWithAndOr() // 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()) { if (theAndOrParams.isEmpty()) {
return new SubscriptionMatchResult(true, CRITERIA); return SubscriptionMatchResult.successfulMatch();
} }
if (hasQualifiers(theAndOrParams)) { if (hasQualifiers(theAndOrParams)) {
return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.STANDARD_PARAMETER);
return new SubscriptionMatchResult(theParamName, "Standard Parameters not supported.");
} }
if (hasPrefixes(theAndOrParams)) { if (hasPrefixes(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Prefixes not supported."); return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PREFIX);
} }
if (hasChain(theAndOrParams)) { if (hasChain(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Chained references are not supported"); return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.CHAIN);
} }
switch (theParamName) { switch (theParamName) {
case IAnyResource.SP_RES_ID: case IAnyResource.SP_RES_ID:
return new SubscriptionMatchResult(matchIdsAndOr(theAndOrParams, theResource), CRITERIA); return SubscriptionMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource));
case IAnyResource.SP_RES_LANGUAGE: case IAnyResource.SP_RES_LANGUAGE:
return new SubscriptionMatchResult(theParamName, CRITERIA);
case Constants.PARAM_HAS: case Constants.PARAM_HAS:
return new SubscriptionMatchResult(theParamName, CRITERIA);
case Constants.PARAM_TAG: case Constants.PARAM_TAG:
case Constants.PARAM_PROFILE: case Constants.PARAM_PROFILE:
case Constants.PARAM_SECURITY: case Constants.PARAM_SECURITY:
return new SubscriptionMatchResult(theParamName, CRITERIA); return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
default: default:
String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
} }
} }
private boolean matchIdsAndOr(List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource) { private boolean matchIdsAndOr(List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
}
return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource)); return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource));
} }
private boolean matchIdsOr(List<? extends IQueryParameterType> theOrParams, IBaseResource 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())); return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam)param).getValue(), theResource.getIdElement()));
} }
@ -144,16 +159,20 @@ public class CriteriaResourceMatcher {
case URI: case URI:
case DATE: case DATE:
case REFERENCE: 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 COMPOSITE:
case HAS: case HAS:
case SPECIAL: case SPECIAL:
default: default:
return new SubscriptionMatchResult(theParamName, CRITERIA); return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
} }
} else { } else {
if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
return new SubscriptionMatchResult(theParamName, CRITERIA); return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM);
} else { } else {
throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName); throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
} }

View File

@ -21,26 +21,47 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
*/ */
public class SubscriptionMatchResult { 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 myMatch;
private final boolean mySupported; private final boolean mySupported;
private final String myUnsupportedParameter; private final String myUnsupportedParameter;
private final String myUnsupportedReason; 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.myMatch = theMatch;
this.mySupported = true; this.mySupported = true;
this.myUnsupportedParameter = null; this.myUnsupportedParameter = null;
this.myUnsupportedReason = null; this.myUnsupportedReason = null;
this.myMatcherShortName = theMatcherShortName;
} }
public SubscriptionMatchResult(String theUnsupportedParameter, String theMatcherShortName) { private SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
this.myMatch = false; this.myMatch = false;
this.mySupported = false; this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter; this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = "Parameter not supported"; this.myUnsupportedReason = theUnsupportedReason;
this.myMatcherShortName = theMatcherShortName; }
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() { public boolean supported() {
@ -52,14 +73,17 @@ public class SubscriptionMatchResult {
} }
public String getUnsupportedReason() { public String getUnsupportedReason() {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason; if (myUnsupportedParameter != null) {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason;
}
return myUnsupportedReason;
} }
/** public boolean isInMemory() {
* Returns a short name of the matcher that generated this return myInMemory;
* response, for use in logging }
*/
public String matcherShortName() { public void setInMemory(boolean theInMemory) {
return myMatcherShortName; 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()) { if (!matchResult.matched()) {
continue; continue;
} }
ourLog.debug("Subscription {} was matched by resource {} {}",
ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), resourceId.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName()); nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(),
resourceId.toUnqualifiedVersionless().getValue(),
matchResult.isInMemory() ? "in-memory" : "by querying the repository");
IBaseResource payload = theMsg.getNewPayload(myFhirContext); IBaseResource payload = theMsg.getNewPayload(myFhirContext);
@ -161,7 +163,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
criteriaResource = criteriaResource.substring(0, criteriaResource.indexOf("?")); 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); ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, criteriaString);
return false; 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.dstu3.model.codesystems.MedicationRequestCategory;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue;
public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test {
@Autowired
SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;
@Autowired @Autowired
InMemorySubscriptionMatcher myInMemorySubscriptionMatcher; InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
private void assertUnsupported(IBaseResource resource, String criteria) { private void assertUnsupported(IBaseResource resource, String criteria) {
assertFalse(myInMemorySubscriptionMatcher.match(criteria, resource).supported()); assertFalse(myInMemorySubscriptionMatcher.match(criteria, resource).supported());
assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
} }
private void assertMatched(IBaseResource resource, String criteria) { private void assertMatched(IBaseResource resource, String criteria) {
@ -30,13 +30,20 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
assertTrue(result.supported()); assertTrue(result.supported());
assertTrue(result.matched()); assertTrue(result.matched());
assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
} }
private void assertNotMatched(IBaseResource resource, String 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); SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, resource);
assertTrue(result.supported()); assertTrue(result.supported());
assertFalse(result.matched()); assertFalse(result.matched());
assertEquals(theSubscriptionMatchingStrategy, mySubscriptionStrategyEvaluator.determineStrategy(criteria));
} }
@ -152,7 +159,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{ {
Observation obs = new Observation(); Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX"); obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria); assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE);
} }
{ {
Observation obs = new Observation(); Observation obs = new Observation();
@ -168,7 +175,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{ {
Observation obs = new Observation(); Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX"); obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria); assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE);
} }
{ {
Observation obs = new Observation(); Observation obs = new Observation();
@ -266,7 +273,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test
{ {
Observation obs = new Observation(); Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX"); 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. The casing of the base64Binary datatype was incorrect in the DSTU3 and R4 model classes.
This has been corrected. This has been corrected.
</action> </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>
<release version="3.6.0" date="2018-11-12" description="Food"> <release version="3.6.0" date="2018-11-12" description="Food">
<action type="add"> <action type="add">