parent
f55be0b6d0
commit
10c59fceeb
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue