Improve Subscription Retriggering Efficiency (#4742)

* Improve subscription efficiency

* Reduce number of queries

* Add changelog

* Fix ITs

* Review comments

* Test fix
This commit is contained in:
James Agnew 2023-04-17 08:18:57 -04:00 committed by GitHub
parent 83f216bcee
commit 7bdbda9ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 894 additions and 205 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 4742
title: "The in-memory matcher used by the JPA server subscription processor has been
optimized to reduce the number of FHIRPath expressions executed while processing
in-memory matching."

View File

@ -0,0 +1,6 @@
---
type: perf
issue: 4742
title: "The SQL generated for the JPA server `$trigger-subscription` operation has been
optimized in order to drastically reduce the number of database round trips for large
triggering jobs."

View File

@ -0,0 +1,6 @@
---
type: add
issue: 4742
title: "The JPA server in-memory resource matcher, which is used to improve the efficiency of
subscription processing on eligible criteria, now has support for the `_tag`,
`_tag:not`, `_security`, `_security:not` and `_profile` parameters."

View File

@ -97,7 +97,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.MetaUtil;
@ -160,7 +159,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
@ -914,9 +912,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer;
}
private void verifyMatchUrlForConditionalCreate(IBaseResource theResource, String theIfNoneExist, ResourceTable entity, ResourceIndexedSearchParams theParams) {
private void verifyMatchUrlForConditionalCreate(IBaseResource theResource, String theIfNoneExist, ResourceIndexedSearchParams theParams, RequestDetails theRequestDetails) {
// Make sure that the match URL was actually appropriate for the supplied resource
InMemoryMatchResult outcome = myInMemoryResourceMatcher.match(theIfNoneExist, theResource, theParams);
InMemoryMatchResult outcome = myInMemoryResourceMatcher.match(theIfNoneExist, theResource, theParams, theRequestDetails);
if (outcome.supported() && !outcome.matched()) {
throw new InvalidRequestException(Msg.code(929) + "Failed to process conditional create. The supplied resource did not satisfy the conditional URL.");
}
@ -1032,7 +1030,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
// matches. We could certainly make this configurable though in the
// future.
if (entity.getVersion() <= 1L && entity.getCreatedByMatchUrl() != null && thePerformIndexing) {
verifyMatchUrlForConditionalCreate(theResource, entity.getCreatedByMatchUrl(), entity, newParams);
verifyMatchUrlForConditionalCreate(theResource, entity.getCreatedByMatchUrl(), newParams, theRequest);
}
entity.setUpdated(theTransactionDetails.getTransactionDate());

View File

@ -60,6 +60,7 @@ import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -487,6 +488,7 @@ public class SearchParameterMap implements Serializable {
b.append(',');
}
String valueAsQueryToken = nextValueOr.getValueAsQueryToken(theCtx);
valueAsQueryToken = defaultString(valueAsQueryToken);
b.append(UrlUtil.escapeUrlParam(valueAsQueryToken));
}
}

View File

@ -27,21 +27,25 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.MetaUtil;
@ -50,39 +54,47 @@ import com.google.common.collect.Sets;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.Location;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class InMemoryResourceMatcher {
private enum ValidationSupportInitializationState {NOT_INITIALIZED, INITIALIZED, FAILED}
public static final Set<String> UNSUPPORTED_PARAMETER_NAMES = Sets.newHashSet(Constants.PARAM_HAS, Constants.PARAM_TAG, Constants.PARAM_PROFILE, Constants.PARAM_SECURITY);
public static final Set<String> UNSUPPORTED_PARAMETER_NAMES = Sets.newHashSet(Constants.PARAM_HAS);
private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(InMemoryResourceMatcher.class);
@Autowired
ApplicationContext myApplicationContext;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
ISearchParamRegistry mySearchParamRegistry;
@Autowired
StorageSettings myStorageSettings;
@Autowired
FhirContext myFhirContext;
@Autowired
SearchParamExtractorService mySearchParamExtractorService;
@Autowired
IndexedSearchParamExtractor myIndexedSearchParamExtractor;
@Autowired
private MatchUrlService myMatchUrlService;
private ValidationSupportInitializationState validationSupportState = ValidationSupportInitializationState.NOT_INITIALIZED;
private IValidationSupport myValidationSupport = null;
public InMemoryResourceMatcher() {}
public InMemoryResourceMatcher() {
}
/**
* Lazy loads a {@link IValidationSupport} implementation just-in-time.
@ -106,15 +118,30 @@ public class InMemoryResourceMatcher {
}
/**
* 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.
* <p>
* 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.
* @deprecated Use {@link #match(String, IBaseResource, ResourceIndexedSearchParams, RequestDetails)}
*/
@Deprecated
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, @Nullable ResourceIndexedSearchParams theIndexedSearchParams) {
return match(theCriteria, theResource, theIndexedSearchParams, null);
}
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
/**
* 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.
* <p>
* 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.
*
* @param theIndexedSearchParams If the search params have already been calculated for the given resource,
* they can be passed in. Passing in {@literal null} is also fine, in which
* case they will be calculated for the resource. It can be preferable to
* pass in {@literal null} unless you already actually had to calculate the
* indexes for another reason, since we can be efficient here and only calculate
* the params that are actually relevant for the given search expression.
*/
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, @Nullable ResourceIndexedSearchParams theIndexedSearchParams, RequestDetails theRequestDetails) {
RuntimeResourceDefinition resourceDefinition;
if (theResource == null) {
Validate.isTrue(!theCriteria.startsWith("?"), "Invalid match URL format (must match \"[resourceType]?[params]\")");
@ -129,25 +156,32 @@ public class InMemoryResourceMatcher {
} catch (UnsupportedOperationException e) {
return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.PARSE_FAIL);
}
// wipjv consider merging InMemoryMatchResult with IAuthorizationSearchParamMatcher.Match match type
// } catch (MatchUrlService.UnrecognizedSearchParameterException e) {
// return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.PARAM);
searchParameterMap.clean();
return match(searchParameterMap, theResource, resourceDefinition, theSearchParams);
ResourceIndexedSearchParams relevantSearchParams = null;
if (theIndexedSearchParams != null) {
relevantSearchParams = theIndexedSearchParams;
} else if (theResource != null) {
// Don't index search params we don't actully need for the given criteria
ISearchParamExtractor.ISearchParamFilter filter = theSearchParams -> theSearchParams
.stream()
.filter(t -> searchParameterMap.containsKey(t.getName()))
.collect(Collectors.toList());
relevantSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequestDetails, filter);
}
return match(searchParameterMap, theResource, resourceDefinition, relevantSearchParams);
}
/**
*
* @param theCriteria
* @return result.supported() will be true if theCriteria can be evaluated in-memory
*/
public InMemoryMatchResult canBeEvaluatedInMemory(String theCriteria) {
return match(theCriteria, null, null);
return match(theCriteria, null, null, null);
}
/**
*
* @param theSearchParameterMap
* @param theResourceDefinition
* @return result.supported() will be true if theSearchParameterMap can be evaluated in-memory
@ -156,7 +190,6 @@ public class InMemoryResourceMatcher {
return match(theSearchParameterMap, null, theResourceDefinition, null);
}
@Nonnull
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
if (theSearchParameterMap.getLastUpdated() != null) {
@ -195,6 +228,12 @@ public class InMemoryResourceMatcher {
return InMemoryMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource));
case Constants.PARAM_SOURCE:
return InMemoryMatchResult.fromBoolean(matchSourcesAndOr(theAndOrParams, theResource));
case Constants.PARAM_TAG:
return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, true));
case Constants.PARAM_SECURITY:
return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, false));
case Constants.PARAM_PROFILE:
return InMemoryMatchResult.fromBoolean(matchProfilesAndOr(theAndOrParams, theResource));
default:
return matchResourceParam(myStorageSettings, theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
}
@ -225,7 +264,7 @@ public class InMemoryResourceMatcher {
InMemoryMatchResult checkUnsupportedResult = InMemoryMatchResult.successfulMatch();
if (hasChain(theParam)) {
checkUnsupportedResult = InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + "." + ((ReferenceParam)theParam).getChain(), InMemoryMatchResult.CHAIN);
checkUnsupportedResult = InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + "." + ((ReferenceParam) theParam).getChain(), InMemoryMatchResult.CHAIN);
}
if (checkUnsupportedResult.supported()) {
@ -239,6 +278,30 @@ public class InMemoryResourceMatcher {
return checkUnsupportedResult;
}
private boolean matchProfilesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
}
return theAndOrParams.stream().allMatch(nextAnd -> matchProfilesOr(nextAnd, theResource));
}
private boolean matchProfilesOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) {
return theOrParams.stream().anyMatch(param -> matchProfile(param, theResource));
}
private boolean matchProfile(IQueryParameterType theProfileParam, IBaseResource theResource) {
UriParam paramProfile = new UriParam(theProfileParam.getValueAsQueryToken(myFhirContext));
String paramProfileValue = paramProfile.getValue();
if (isBlank(paramProfileValue)) {
return false;
} else {
return theResource.getMeta().getProfile().stream()
.map(IPrimitiveType::getValueAsString)
.anyMatch(profileValue -> profileValue != null && profileValue.equals(paramProfileValue));
}
}
private boolean matchSourcesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
@ -263,6 +326,54 @@ public class InMemoryResourceMatcher {
return matches;
}
private boolean matchTagsOrSecurityAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource, boolean theTag) {
if (theResource == null) {
return true;
}
return theAndOrParams.stream().allMatch(nextAnd -> matchTagsOrSecurityOr(nextAnd, theResource, theTag));
}
private boolean matchTagsOrSecurityOr(List<IQueryParameterType> theOrParams, IBaseResource theResource, boolean theTag) {
return theOrParams.stream().anyMatch(param -> matchTagOrSecurity(param, theResource, theTag));
}
private boolean matchTagOrSecurity(IQueryParameterType theParam, IBaseResource theResource, boolean theTag) {
TokenParam param = (TokenParam) theParam;
List<? extends IBaseCoding> list;
if (theTag) {
list = theResource.getMeta().getTag();
} else {
list = theResource.getMeta().getSecurity();
}
boolean haveMatch = false;
boolean haveCandidate = false;
for (IBaseCoding next : list) {
if (param.getSystem() == null && param.getValue() == null) {
continue;
}
haveCandidate = true;
if (isNotBlank(param.getSystem())) {
if (!param.getSystem().equals(next.getSystem())) {
continue;
}
}
if (isNotBlank(param.getValue())) {
if (!param.getValue().equals(next.getCode())) {
continue;
}
}
haveMatch = true;
break;
}
if (param.getModifier() == TokenParamModifier.NOT) {
haveMatch = !haveMatch;
}
return haveMatch && haveCandidate;
}
private boolean matchIdsAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
@ -319,7 +430,9 @@ public class InMemoryResourceMatcher {
}
}
/** Some modifiers are negative, and must match NONE of their or-list */
/**
* Some modifiers are negative, and must match NONE of their or-list
*/
private boolean isNegative(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList) {
if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) {
TokenParam tokenParam = (TokenParam) theOrList.get(0);
@ -344,12 +457,13 @@ public class InMemoryResourceMatcher {
* The :not modifier is supported.
* The :in and :not-in qualifiers are supported only if a bean implementing IValidationSupport is available.
* Any other qualifier will be ignored and the match will be treated as unqualified.
*
* @param theStorageSettings a model configuration
* @param theResourceName the name of the resource type being matched
* @param theParamName the name of the parameter
* @param theParamDef the definition of the search parameter
* @param theSearchParams the search parameters derived from the target resource
* @param theQueryParam the query parameter to compare with theSearchParams
* @param theResourceName the name of the resource type being matched
* @param theParamName the name of the parameter
* @param theParamDef the definition of the search parameter
* @param theSearchParams the search parameters derived from the target resource
* @param theQueryParam the query parameter to compare with theSearchParams
* @return true if theQueryParam matches the collection of theSearchParams, otherwise false
*/
private boolean matchTokenParam(StorageSettings theStorageSettings, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, TokenParam theQueryParam) {
@ -458,4 +572,6 @@ public class InMemoryResourceMatcher {
}
}
private enum ValidationSupportInitializationState {NOT_INITIALIZED, INITIALIZED, FAILED}
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -28,19 +29,27 @@ import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
public class IndexedSearchParamExtractor {
@Autowired
private FhirContext myContext;
@Autowired
private SearchParamExtractorService mySearchParamExtractorService;
@Nonnull
public ResourceIndexedSearchParams extractIndexedSearchParams(IBaseResource theResource, RequestDetails theRequest) {
return extractIndexedSearchParams(theResource, theRequest, ISearchParamExtractor.ALL_PARAMS);
}
@Nonnull
public ResourceIndexedSearchParams extractIndexedSearchParams(IBaseResource theResource, RequestDetails theRequest, ISearchParamExtractor.ISearchParamFilter filter) {
ResourceTable entity = new ResourceTable();
TransactionDetails transactionDetails = new TransactionDetails();
String resourceType = myContext.getResourceType(theResource);
entity.setResourceType(resourceType);
ResourceIndexedSearchParams resourceIndexedSearchParams = new ResourceIndexedSearchParams();
mySearchParamExtractorService.extractFromResource(null, theRequest, resourceIndexedSearchParams, entity, theResource, transactionDetails, false);
mySearchParamExtractorService.extractFromResource(null, theRequest, resourceIndexedSearchParams, new ResourceIndexedSearchParams(), entity, theResource, transactionDetails, false, filter);
return resourceIndexedSearchParams;
}
}

View File

@ -38,8 +38,7 @@ public class SearchParamMatcher {
private InMemoryResourceMatcher myInMemoryResourceMatcher;
public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, RequestDetails theRequest) {
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequest);
return myInMemoryResourceMatcher.match(theCriteria, theResource, resourceIndexedSearchParams);
return myInMemoryResourceMatcher.match(theCriteria, theResource, null, theRequest);
}
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource) {

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
@ -28,6 +29,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcherR5Test.newRequest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
@ -47,6 +49,10 @@ public class InMemoryResourceMatcherConfigurationR5Test {
ISearchParamRegistry mySearchParamRegistry;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@MockBean
private SearchParamExtractorService mySearchParamExtractorService;
@MockBean
private IndexedSearchParamExtractor myIndexedSearchParamExtractor;
private Observation myObservation;
private ResourceIndexedSearchParams mySearchParams;
@ -71,7 +77,7 @@ public class InMemoryResourceMatcherConfigurationR5Test {
myInMemoryResourceMatcher.myApplicationContext = applicationContext;
for (int i = 0; i < 10; i++) {
myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
}
verify(applicationContext, times(1)).getBean(IValidationSupport.class);
@ -83,7 +89,7 @@ public class InMemoryResourceMatcherConfigurationR5Test {
Tests the case where the :in qualifier can not be supported because no bean implementing IValidationSupport was registered
*/
public void testUnsupportedIn() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
assertFalse(result.supported());
assertEquals("Parameter: <code:in> Reason: Qualified parameter not supported", result.getUnsupportedReason());
}
@ -91,7 +97,7 @@ public class InMemoryResourceMatcherConfigurationR5Test {
@Test
@Order(3)
public void testUnsupportedNotIn() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
assertFalse(result.supported());
assertEquals("Parameter: <code:not-in> Reason: Qualified parameter not supported", result.getUnsupportedReason());
}

View File

@ -9,9 +9,12 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.model.primitive.BaseDateTimeDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
@ -63,6 +66,10 @@ public class InMemoryResourceMatcherR5Test {
ISearchParamRegistry mySearchParamRegistry;
@MockBean
IValidationSupport myValidationSupport;
@MockBean
SearchParamExtractorService mySearchParamExtractorService;
@MockBean
IndexedSearchParamExtractor myIndexedSearchParamExtractor;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
private Observation myObservation;
@ -92,47 +99,51 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testSupportedSource() {
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + TEST_SOURCE, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + TEST_SOURCE, myObservation, mySearchParams, newRequest());
assertTrue(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + SOURCE_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + SOURCE_URI, myObservation, mySearchParams, newRequest());
assertTrue(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + REQUEST_ID, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + REQUEST_ID, myObservation, mySearchParams, newRequest());
assertFalse(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=#" + REQUEST_ID, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=#" + REQUEST_ID, myObservation, mySearchParams, newRequest());
assertTrue(result.matched());
}
}
static RequestDetails newRequest() {
return new SystemRequestDetails();
}
@Test
public void testSupportedSource_ResourceWithNoSourceValue() {
myObservation.getMeta().getSourceElement().setValue(null);
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + TEST_SOURCE, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + TEST_SOURCE, myObservation, mySearchParams, newRequest());
assertFalse(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + SOURCE_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + SOURCE_URI, myObservation, mySearchParams, newRequest());
assertFalse(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + REQUEST_ID, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=" + REQUEST_ID, myObservation, mySearchParams, newRequest());
assertFalse(result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=#" + REQUEST_ID, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(Constants.PARAM_SOURCE + "=#" + REQUEST_ID, myObservation, mySearchParams, newRequest());
assertFalse(result.matched());
}
}
@Test
public void testUnsupportedChained() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("encounter.class=FOO", myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("encounter.class=FOO", myObservation, mySearchParams, newRequest());
assertFalse(result.supported());
assertEquals("Parameter: <encounter.class> Reason: Chained parameters are not supported", result.getUnsupportedReason());
}
@ -140,11 +151,11 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testSupportedNot() {
String criteria = "code" + TokenParamModifier.NOT.getValue() + "=" + OBSERVATION_CODE + ",a_different_code";
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertFalse(result.matched(), ":not must not match any of the OR-list");
result = myInMemoryResourceMatcher.match("code:not=a_different_code,and_another", myObservation, mySearchParams);
result = myInMemoryResourceMatcher.match("code:not=a_different_code,and_another", myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertTrue(result.matched(), ":not matches when NONE match");
}
@ -154,7 +165,7 @@ public class InMemoryResourceMatcherR5Test {
IValidationSupport.CodeValidationResult codeValidationResult = new IValidationSupport.CodeValidationResult().setCode(OBSERVATION_CODE);
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), any())).thenReturn(codeValidationResult);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertTrue(result.matched());
@ -166,7 +177,7 @@ public class InMemoryResourceMatcherR5Test {
IValidationSupport.CodeValidationResult codeValidationResult = new IValidationSupport.CodeValidationResult();
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), any())).thenReturn(codeValidationResult);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertFalse(result.matched());
@ -178,7 +189,7 @@ public class InMemoryResourceMatcherR5Test {
IValidationSupport.CodeValidationResult codeValidationResult = new IValidationSupport.CodeValidationResult();
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), any())).thenReturn(codeValidationResult);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertTrue(result.matched());
@ -198,7 +209,7 @@ public class InMemoryResourceMatcherR5Test {
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), eq(otherValueSet))).thenReturn(noMatchResult);
String criteria = "code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI + "," + otherValueSet;
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams, newRequest());
assertTrue(result.supported());
assertFalse(result.matched(), ":not-in matches when NONE of the OR-list match");
@ -208,7 +219,7 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testUnrecognizedParam() {
try {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("foo=bar", myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("foo=bar", myObservation, mySearchParams, newRequest());
} catch (MatchUrlService.UnrecognizedSearchParameterException e) {
// expected
}
@ -223,7 +234,7 @@ public class InMemoryResourceMatcherR5Test {
}
private void testDateUnsupportedDateOp(ParamPrefixEnum theOperator) {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=" + theOperator.getValue() + OBSERVATION_DATETIME, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=" + theOperator.getValue() + OBSERVATION_DATETIME, myObservation, mySearchParams, newRequest());
assertFalse(result.supported());
assertEquals("Parameter: <date> Reason: The prefix " + theOperator + " is not supported for param type DATE", result.getUnsupportedReason());
}
@ -258,17 +269,17 @@ public class InMemoryResourceMatcherR5Test {
String equation = "date=" + theOperator.getValue();
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + earlyDate, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + earlyDate, myObservation, mySearchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertEquals(result.matched(), theEarly);
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + observationDate, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + observationDate, myObservation, mySearchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertEquals(result.matched(), theSame);
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + lateDate, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + lateDate, myObservation, mySearchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertEquals(result.matched(), theLater);
}
@ -276,7 +287,7 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testNowPast() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=lt" + BaseDateTimeDt.NOW_DATE_CONSTANT, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=lt" + BaseDateTimeDt.NOW_DATE_CONSTANT, myObservation, mySearchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertTrue(result.matched());
}
@ -288,7 +299,7 @@ public class InMemoryResourceMatcherR5Test {
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertTrue(result.matched());
}
@ -302,7 +313,7 @@ public class InMemoryResourceMatcherR5Test {
futureObservation.setEffective(new DateTimeType(Date.from(nextMinute)));
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertEquals(1, searchParams.myDateParams.size());
ResourceIndexedSearchParamDate searchParamDate = searchParams.myDateParams.iterator().next();
@ -313,7 +324,7 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testTodayPast() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=lt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, myObservation, mySearchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=lt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, myObservation, mySearchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertTrue(result.matched());
}
@ -325,7 +336,7 @@ public class InMemoryResourceMatcherR5Test {
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertTrue(result.matched());
}
@ -337,7 +348,7 @@ public class InMemoryResourceMatcherR5Test {
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams, newRequest());
assertTrue(result.supported(), result.getUnsupportedReason());
assertFalse(result.matched());
}
@ -354,11 +365,11 @@ public class InMemoryResourceMatcherR5Test {
String search = "date=gt" + EARLY_DATE + "&date=le" + LATE_DATE;
InMemoryMatchResult resultInsidePeriod = myInMemoryResourceMatcher.match(search, insidePeriodObservation, insidePeriodSearchParams);
InMemoryMatchResult resultInsidePeriod = myInMemoryResourceMatcher.match(search, insidePeriodObservation, insidePeriodSearchParams, newRequest());
assertTrue(resultInsidePeriod.supported(), resultInsidePeriod.getUnsupportedReason());
assertTrue(resultInsidePeriod.matched());
InMemoryMatchResult resultOutsidePeriod = myInMemoryResourceMatcher.match(search, outsidePeriodObservation, outsidePeriodSearchParams);
InMemoryMatchResult resultOutsidePeriod = myInMemoryResourceMatcher.match(search, outsidePeriodObservation, outsidePeriodSearchParams, newRequest());
assertTrue(resultOutsidePeriod.supported(), resultOutsidePeriod.getUnsupportedReason());
assertFalse(resultOutsidePeriod.matched());
}

View File

@ -4,19 +4,15 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
import ca.uhn.fhir.jpa.cache.*;
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.jpa.searchparam.matcher.IndexedSearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
@ -27,11 +23,7 @@ import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.HapiExtensions;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -46,36 +38,26 @@ import org.testcontainers.shaded.com.google.common.collect.Sets;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
@ExtendWith(SpringExtension.class)
public class SearchParamRegistryImplTest {
public static final int TEST_SEARCH_PARAMS = 3;
private static final FhirContext ourFhirContext = FhirContext.forR4();
private static final ReadOnlySearchParamCache ourBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(ourFhirContext, new SearchParameterCanonicalizer(ourFhirContext));
public static final int TEST_SEARCH_PARAMS = 3;
private static final List<ResourceTable> ourEntities;
private static final ResourceVersionMap ourResourceVersionMap;
private static int ourLastId;
private static final int ourBuiltinPatientSearchParamCount;
private static int ourLastId;
static {
ourEntities = new ArrayList<>();
@ -90,8 +72,6 @@ public class SearchParamRegistryImplTest {
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@Autowired
private ResourceChangeListenerCacheRefresherImpl myChangeListenerCacheRefresher;
@MockBean
private IResourceVersionSvc myResourceVersionSvc;
@ -103,55 +83,11 @@ public class SearchParamRegistryImplTest {
private SearchParamMatcher mySearchParamMatcher;
@MockBean
private MatchUrlService myMatchUrlService;
@Configuration
@Import(RegisteredResourceListenerFactoryConfig.class)
static class SpringConfig {
@Bean
FhirContext fhirContext() {
return ourFhirContext;
}
@Bean
StorageSettings storageSettings() {
StorageSettings storageSettings = new StorageSettings();
storageSettings.setDefaultSearchParamsCanBeOverridden(true);
return storageSettings;
}
@Bean
ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryImpl();
}
@Bean
SearchParameterCanonicalizer searchParameterCanonicalizer(FhirContext theFhirContext) {
return new SearchParameterCanonicalizer(theFhirContext);
}
@Bean
IResourceChangeListenerRegistry resourceChangeListenerRegistry(FhirContext theFhirContext, ResourceChangeListenerCacheFactory theResourceChangeListenerCacheFactory, InMemoryResourceMatcher theInMemoryResourceMatcher) {
return new ResourceChangeListenerRegistryImpl(theFhirContext, theResourceChangeListenerCacheFactory, theInMemoryResourceMatcher);
}
@Bean
ResourceChangeListenerCacheRefresherImpl resourceChangeListenerCacheRefresher() {
return new ResourceChangeListenerCacheRefresherImpl();
}
@Bean
InMemoryResourceMatcher inMemoryResourceMatcher() {
InMemoryResourceMatcher retval = mock(InMemoryResourceMatcher.class);
when(retval.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
return retval;
}
@Bean
IValidationSupport validationSupport() {
return mock(IValidationSupport.class);
}
}
@MockBean
private SearchParamExtractorService mySearchParamExtractorService;
@MockBean
private IndexedSearchParamExtractor myIndexedSearchParamExtractor;
private int myAnswerCount = 0;
@Nonnull
private static ResourceTable createEntity(long theId, int theVersion) {
@ -162,8 +98,6 @@ public class SearchParamRegistryImplTest {
return searchParamEntity;
}
private int myAnswerCount = 0;
@BeforeEach
public void before() {
myAnswerCount = 0;
@ -408,4 +342,53 @@ public class SearchParamRegistryImplTest {
return searchParameter;
}
@Configuration
@Import(RegisteredResourceListenerFactoryConfig.class)
static class SpringConfig {
@Bean
FhirContext fhirContext() {
return ourFhirContext;
}
@Bean
StorageSettings storageSettings() {
StorageSettings storageSettings = new StorageSettings();
storageSettings.setDefaultSearchParamsCanBeOverridden(true);
return storageSettings;
}
@Bean
ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryImpl();
}
@Bean
SearchParameterCanonicalizer searchParameterCanonicalizer(FhirContext theFhirContext) {
return new SearchParameterCanonicalizer(theFhirContext);
}
@Bean
IResourceChangeListenerRegistry resourceChangeListenerRegistry(FhirContext theFhirContext, ResourceChangeListenerCacheFactory theResourceChangeListenerCacheFactory, InMemoryResourceMatcher theInMemoryResourceMatcher) {
return new ResourceChangeListenerRegistryImpl(theFhirContext, theResourceChangeListenerCacheFactory, theInMemoryResourceMatcher);
}
@Bean
ResourceChangeListenerCacheRefresherImpl resourceChangeListenerCacheRefresher() {
return new ResourceChangeListenerCacheRefresherImpl();
}
@Bean
InMemoryResourceMatcher inMemoryResourceMatcher() {
InMemoryResourceMatcher retval = mock(InMemoryResourceMatcher.class);
when(retval.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
return retval;
}
@Bean
IValidationSupport validationSupport() {
return mock(IValidationSupport.class);
}
}
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber;
import ca.uhn.fhir.context.FhirContext;

View File

@ -28,6 +28,8 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.api.svc.ISearchSvc;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;
@ -38,7 +40,6 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
@ -65,6 +66,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
@ -110,9 +112,11 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
@Autowired
private ISearchSvc mySearchService;
@Autowired
private SearchBuilderFactory mySearchBuilderFactory;
@Override
public IBaseParameters triggerSubscription(List<IPrimitiveType<String>> theResourceIds, List<IPrimitiveType<String>> theSearchUrls, @IdParam IIdType theSubscriptionId) {
public IBaseParameters triggerSubscription(@Nullable List<IPrimitiveType<String>> theResourceIds, @Nullable List<IPrimitiveType<String>> theSearchUrls, @Nullable IIdType theSubscriptionId) {
if (myStorageSettings.getSupportedSubscriptionTypes().isEmpty()) {
throw new PreconditionFailedException(Msg.code(22) + "Subscription processing not active on this server");
@ -229,7 +233,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
// This is the job initial step where we set ourselves up to do the actual re-submitting of resources
// to the broker. Note that querying of resource can be done synchronously or asynchronously
if ( isInitialStep(theJobDetails) && isNotEmpty(theJobDetails.getRemainingSearchUrls()) && totalSubmitted < myMaxSubmitPerPass){
if (isInitialStep(theJobDetails) && isNotEmpty(theJobDetails.getRemainingSearchUrls()) && totalSubmitted < myMaxSubmitPerPass) {
String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0);
RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myFhirContext, nextSearchUrl);
@ -266,7 +270,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
String searchUrl = theJobDetails.getCurrentSearchUrl();
ourLog.info("Triggered job [{}] - Starting synchronous processing at offset {} and index {}", theJobDetails.getJobId(), theJobDetails.getCurrentOffset(), fromIndex );
ourLog.info("Triggered job [{}] - Starting synchronous processing at offset {} and index {}", theJobDetails.getJobId(), theJobDetails.getCurrentOffset(), fromIndex);
int submittableCount = myMaxSubmitPerPass - totalSubmitted;
int toIndex = fromIndex + submittableCount;
@ -339,6 +343,8 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
}
ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
List<IResourcePersistentId<?>> resourceIds;
RequestPartitionId requestPartitionId = RequestPartitionId.allPartitions();
resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex, null, requestPartitionId);
@ -346,8 +352,18 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size());
int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex();
for (IResourcePersistentId next : resourceIds) {
IBaseResource nextResource = resourceDao.readByPid(next);
String resourceType = myFhirContext.getResourceType(theJobDetails.getCurrentSearchResourceType());
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theJobDetails.getCurrentSearchResourceType());
ISearchBuilder searchBuilder = mySearchBuilderFactory.newSearchBuilder(resourceDao, resourceType, resourceDef.getImplementingClass());
List<IBaseResource> listToPopulate = new ArrayList<>();
myTransactionService
.withSystemRequest()
.execute(() -> {
searchBuilder.loadResourcesByPid(resourceIds, Collections.emptyList(), listToPopulate, false, new SystemRequestDetails());
});
for (IBaseResource nextResource : listToPopulate) {
Future<Void> future = submitResource(theJobDetails.getSubscriptionId(), nextResource);
futures.add(Pair.of(nextResource.getIdElement().getIdPart(), future));
totalSubmitted++;
@ -376,7 +392,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
return isBlank(theJobDetails.myCurrentSearchUuid) && isBlank(theJobDetails.myCurrentSearchUrl);
}
private boolean jobHasCompleted(SubscriptionTriggeringJobDetails theJobDetails){
private boolean jobHasCompleted(SubscriptionTriggeringJobDetails theJobDetails) {
return isInitialStep(theJobDetails);
}
@ -599,11 +615,11 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
myCurrentSearchLastUploadedIndex = theCurrentSearchLastUploadedIndex;
}
public void clearCurrentSearchUrl(){
public void clearCurrentSearchUrl() {
myCurrentSearchUrl = null;
}
public int getCurrentOffset(){
public int getCurrentOffset() {
return myCurrentOffset;
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import org.hl7.fhir.r5.model.SubscriptionTopic;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import org.hl7.fhir.r5.model.SubscriptionTopic;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.topic;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;

View File

@ -766,8 +766,8 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
try {
myPatientDao.create(pt1).getId().toUnqualifiedVersionless();
fail();
} catch (PreconditionFailedException e) {
assertEquals(Msg.code(1093) + "Can not create resource of type Patient as it would create a duplicate unique index matching query: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale (existing index belongs to Patient/" + id1.getIdPart() + ", new unique index created by SearchParameter/patient-gender-birthdate)", e.getMessage());
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), containsString("new unique index created by SearchParameter/patient-gender-birthdate"));
}
}
@ -786,7 +786,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
try {
myPatientDao.create(pt);
fail();
} catch (PreconditionFailedException e) {
} catch (ResourceVersionConflictException e) {
// good
}
@ -1536,7 +1536,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
try {
myPatientDao.create(pt1).getId().toUnqualifiedVersionless();
fail();
} catch (PreconditionFailedException e) {
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), containsString("new unique index created by SearchParameter/patient-gender-birthdate"));
}
@ -1551,7 +1551,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
try {
myPatientDao.update(pt2);
fail();
} catch (PreconditionFailedException e) {
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), containsString("new unique index created by SearchParameter/patient-gender-birthdate"));
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
@ -13,8 +14,11 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
import ca.uhn.fhir.jpa.term.TermReadSvcImpl;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -25,6 +29,8 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum;
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -49,6 +55,7 @@ import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Subscription;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType;
@ -56,8 +63,10 @@ import org.hl7.fhir.r4.model.Parameters;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
@ -103,6 +112,19 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@Autowired
private ISearchParamPresentDao mySearchParamPresentDao;
@Autowired
private ISubscriptionTriggeringSvc mySubscriptionTriggeringSvc;
@Autowired
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
@RegisterExtension
@Order(0)
public static final RestfulServerExtension ourServer = new RestfulServerExtension(FhirContext.forR4Cached())
.keepAliveBetweenTests();
@RegisterExtension
@Order(1)
public static final HashMapResourceProviderExtension<Patient> ourPatientProvider = new HashMapResourceProviderExtension<>(ourServer, Patient.class);
@AfterEach
public void afterResetDao() {
@ -120,6 +142,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
myStorageSettings.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new JpaStorageSettings().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
myStorageSettings.setResourceClientIdStrategy(new JpaStorageSettings().getResourceClientIdStrategy());
myStorageSettings.setTagStorageMode(new JpaStorageSettings().getTagStorageMode());
myStorageSettings.clearSupportedSubscriptionTypesForUnitTest();
TermReadSvcImpl.setForceDisableHibernateSearchForUnitTest(false);
}
@ -2864,6 +2887,46 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
/**
* See the class javadoc before changing the counts in this test!
*/
@SuppressWarnings("unchecked")
@Test
public void testTriggerSubscription() throws Exception {
// Setup
myStorageSettings.addSupportedSubscriptionType(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.RESTHOOK);
mySubscriptionMatcherInterceptor.startIfNeeded();
for (int i = 0; i < 10; i++) {
createPatient(withActiveTrue());
}
Subscription subscription = new Subscription();
subscription.getChannel().setEndpoint(ourServer.getBaseUrl());
subscription.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK);
subscription.getChannel().setPayload(Constants.CT_FHIR_JSON_NEW);
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
subscription.setCriteria("Patient?active=true");
IIdType subscriptionId = mySubscriptionDao.create(subscription, mySrd).getId().toUnqualifiedVersionless();
waitForActivatedSubscriptionCount(1);
mySubscriptionTriggeringSvc.triggerSubscription(null, List.of(new StringType("Patient?active=true")), subscriptionId);
// Test
myCaptureQueriesListener.clear();
mySubscriptionTriggeringSvc.runDeliveryPass();
ourPatientProvider.waitForUpdateCount(10);
// Validate
assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(11, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
/**
* See the class javadoc before changing the counts in this test!
*/

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
@ -19,6 +20,8 @@ import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig;
import ca.uhn.fhir.jpa.test.config.TestR4Config;
import ca.uhn.fhir.jpa.util.CoordCalculatorTestUtil;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
@ -33,10 +36,15 @@ import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.param.UriParamQualifierEnum;
import org.apache.commons.io.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Basic;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Condition;
@ -46,19 +54,24 @@ import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.HealthcareService;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.InsurancePlan;
import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.MedicationAdministration;
import org.hl7.fhir.r4.model.MolecularSequence;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.OrganizationAffiliation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Range;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.RiskAssessment;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.SimpleQuantity;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Subscription;
@ -67,16 +80,23 @@ import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -365,6 +385,100 @@ public class InMemorySubscriptionMatcherR4Test {
assertUnsupported(patient, params);
}
@ParameterizedTest
@CsvSource({
"true, http://profile1",
"true, http://profile2",
"false, http://profile99"
})
public void testSearchMetaProfile(boolean theExpectMatched, String theProfile) {
Patient patient = new Patient();
patient.getMeta().addProfile("http://profile1");
patient.getMeta().addProfile("http://profile2");
patient.getMeta().addProfile(null);
SearchParameterMap params = new SearchParameterMap();
params.add(Constants.PARAM_PROFILE, new UriParam(theProfile));
if (theExpectMatched) {
assertMatched(patient, params);
} else {
assertNotMatched(patient, params);
}
}
@ParameterizedTest
@CsvSource({
"true, http://system1, value1",
"true, , value1",
"true, http://system1, ",
"false, http://system1, value2",
"false, , value99",
"true, , ",
})
public void testSearchMetaTag(boolean theExpectMatched, String theSystem, String theValue) {
Patient patient = new Patient();
patient.getMeta().addTag("http://system1", "value1", "display1");
patient.getMeta().addTag("http://system2", "value2", "display2");
patient.getMeta().addTag(null, null, "display3");
SearchParameterMap params = new SearchParameterMap();
params.add(Constants.PARAM_TAG, new TokenParam(theSystem, theValue));
if (theExpectMatched) {
assertMatched(patient, params);
} else {
assertNotMatched(patient, params);
}
}
@ParameterizedTest
@CsvSource({
"false, http://system1, value1",
"false, , value1",
"false, http://system1, ",
"true, http://system1, value2",
"true, , value99",
"true, , ",
})
public void testSearchMetaTagNot(boolean theExpectMatched, String theSystem, String theValue) {
Patient patient = new Patient();
patient.getMeta().addTag("http://system1", "value1", "display1");
patient.getMeta().addTag("http://system2", "value2", "display2");
patient.getMeta().addTag(null, null, "display3");
SearchParameterMap params = new SearchParameterMap();
params.add(Constants.PARAM_TAG, new TokenParam(theSystem, theValue).setModifier(TokenParamModifier.NOT));
if (theExpectMatched) {
assertMatched(patient, params);
} else {
assertNotMatched(patient, params);
}
}
@ParameterizedTest
@CsvSource({
"true, http://system1, value1",
"true, , value1",
"true, http://system1, ",
"false, http://system1, value2",
"false, , value99",
"true, , ",
})
public void testSearchMetaSecurity(boolean theExpectMatched, String theSystem, String theValue) {
Patient patient = new Patient();
patient.getMeta().addSecurity("http://system1", "value1", "display1");
patient.getMeta().addSecurity("http://system2", "value2", "display2");
patient.getMeta().addSecurity(null, null, "display3");
SearchParameterMap params = new SearchParameterMap();
params.add(Constants.PARAM_SECURITY, new TokenParam(theSystem, theValue));
if (theExpectMatched) {
assertMatched(patient, params);
} else {
assertNotMatched(patient, params);
}
}
@Test
public void testSearchNameParam() {
Patient patient = new Patient();
@ -930,41 +1044,6 @@ public class InMemorySubscriptionMatcherR4Test {
}
}
@Test
public void testSearchWithSecurityAndProfileParamsUnsupported() {
String methodName = "testSearchWithSecurityAndProfileParams";
Organization org = new Organization();
org.getNameElement().setValue("FOO");
org.getMeta().addSecurity("urn:taglist", methodName + "1a", null);
{
SearchParameterMap params = new SearchParameterMap();
params.add("_security", new TokenParam("urn:taglist", methodName + "1a"));
assertUnsupported(org, params);
}
{
SearchParameterMap params = new SearchParameterMap();
params.add("_profile", new UriParam("http://" + methodName));
assertUnsupported(org, params);
}
}
@Test
public void testSearchWithTagParameterUnsupported() {
String methodName = "testSearchWithTagParameter";
Organization org = new Organization();
org.getNameElement().setValue("FOO");
org.getMeta().addTag("urn:taglist", methodName + "1a", null);
org.getMeta().addTag("urn:taglist", methodName + "1b", null);
{
// One tag
SearchParameterMap params = new SearchParameterMap();
params.add("_tag", new TokenParam("urn:taglist", methodName + "1a"));
assertUnsupported(org, params);
}
}
@Test
public void testSearchWithVeryLongUrlLonger() {
@ -1070,4 +1149,34 @@ public class InMemorySubscriptionMatcherR4Test {
map.add(Patient.SP_NAME, or);
assertMatched(p, map);
}
@Autowired
private IFhirResourceDao<SearchParameter> mySearchParameterDao;
@Test
public void testMatchCustomSearchParameter() {
SearchParameter sp = new SearchParameter();
sp.setId("display-contracted");
sp.setName("display-contracted");
sp.setCode("display-contracted");
sp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
sp.addBase("PractitionerRole");
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("PractitionerRole.active");
sp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
mySearchParameterDao.update(sp, new SystemRequestDetails());
PractitionerRole pr = new PractitionerRole();
pr.setActive(true);
SearchParameterMap params;
params = SearchParameterMap.newSynchronous("display-contracted", new TokenParam("true"));
assertMatched(pr, params);
params = SearchParameterMap.newSynchronous("display-contracted", new TokenParam("false"));
assertNotMatched(pr, params);
}
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.constant;
public class CareCapsConstants {

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.constant;
public class HtmlConstants {

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.enumeration;
import ca.uhn.fhir.i18n.Msg;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.model.api.annotation.Description;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.enumeration.CareGapsStatusCode;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.rest.api.server.RequestDetails;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.model.api.annotation.Description;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cache;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;

View File

@ -1581,7 +1581,7 @@ public abstract class BaseTransactionProcessor {
continue;
}
if (myInMemoryResourceMatcher.match(matchUrl, resource, indexedParams).matched()) {
if (myInMemoryResourceMatcher.match(matchUrl, resource, indexedParams, theRequest).matched()) {
counter++;
if (counter > 1) {
throw new InvalidRequestException(Msg.code(542) + "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");

View File

@ -19,7 +19,6 @@
*/
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -30,8 +29,6 @@ public class SearchBuilderFactory<T extends IResourcePersistentId<?>> {
@Autowired
private ApplicationContext myApplicationContext;
@Autowired
private JpaStorageSettings myStorageSettings;
public ISearchBuilder<T> newSearchBuilder(IDao theDao, String theResourceName, Class<? extends IBaseResource> theResourceType) {
return (ISearchBuilder<T>) myApplicationContext.getBean(ISearchBuilder.SEARCH_BUILDER_BEAN_NAME, theDao, theResourceName, theResourceType);

View File

@ -24,11 +24,12 @@ import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nullable;
import java.util.List;
public interface ISubscriptionTriggeringSvc {
IBaseParameters triggerSubscription(List<IPrimitiveType<String>> theResourceIds, List<IPrimitiveType<String>> theSearchUrls, @IdParam IIdType theSubscriptionId);
IBaseParameters triggerSubscription(@Nullable List<IPrimitiveType<String>> theResourceIds, @Nullable List<IPrimitiveType<String>> theSearchUrls, @Nullable IIdType theSubscriptionId);
void runDeliveryPass();
}