From 7bdbda9ef0256b1ff08cbde2e569ec179961cd34 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 17 Apr 2023 08:18:57 -0400 Subject: [PATCH] Improve Subscription Retriggering Efficiency (#4742) * Improve subscription efficiency * Reduce number of queries * Add changelog * Fix ITs * Review comments * Test fix --- ...rove-efficiency-of-in-memory-matching.yaml | 6 + ...ficiency-of-subscription-retriggering.yaml | 6 + ...2-support-meta-for-in-memory-matching.yaml | 6 + .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 8 +- .../jpa/searchparam/SearchParameterMap.java | 2 + .../matcher/InMemoryResourceMatcher.java | 180 ++++++++++++++---- .../matcher/IndexedSearchParamExtractor.java | 11 +- .../matcher/SearchParamMatcher.java | 3 +- ...oryResourceMatcherConfigurationR5Test.java | 12 +- .../InMemoryResourceMatcherR5Test.java | 67 ++++--- .../registry/SearchParamRegistryImplTest.java | 143 ++++++-------- .../SubscriptionMatchDeliverer.java | 19 ++ .../SubscriptionTriggeringSvcImpl.java | 34 +++- .../topic/ActiveSubscriptionTopicCache.java | 19 ++ .../jpa/topic/SubscriptionTopicConfig.java | 19 ++ .../jpa/topic/SubscriptionTopicMatcher.java | 19 ++ .../SubscriptionTopicMatchingSubscriber.java | 19 ++ .../SubscriptionTopicPayloadBuilder.java | 19 ++ .../jpa/topic/SubscriptionTopicRegistry.java | 19 ++ .../jpa/topic/SubscriptionTopicSupport.java | 19 ++ .../fhir/jpa/topic/SubscriptionTopicUtil.java | 19 ++ .../jpa/topic/SubscriptionTriggerMatcher.java | 19 ++ .../FhirResourceDaoR4ComboUniqueParamIT.java | 10 +- .../r4/FhirResourceDaoR4QueryCountTest.java | 63 ++++++ .../InMemorySubscriptionMatcherR4Test.java | 179 +++++++++++++---- .../fhir/cr/constant/CareCapsConstants.java | 19 ++ .../uhn/fhir/cr/constant/HtmlConstants.java | 19 ++ .../cr/enumeration/CareGapsStatusCode.java | 19 ++ .../r4/measure/CareGapsOperationProvider.java | 19 ++ .../fhir/cr/r4/measure/CareGapsService.java | 19 ++ .../cr/r4/measure/ISubmitDataService.java | 19 ++ .../cr/r4/measure/SubmitDataProvider.java | 19 ++ .../fhir/cr/r4/measure/SubmitDataService.java | 19 ++ .../cache/BaseResourceCacheSynchronizer.java | 19 ++ .../jpa/dao/BaseTransactionProcessor.java | 2 +- .../fhir/jpa/dao/SearchBuilderFactory.java | 3 - .../ISubscriptionTriggeringSvc.java | 3 +- 37 files changed, 894 insertions(+), 205 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-in-memory-matching.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-subscription-retriggering.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-support-meta-for-in-memory-matching.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-in-memory-matching.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-in-memory-matching.yaml new file mode 100644 index 00000000000..271de5528e3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-in-memory-matching.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-subscription-retriggering.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-subscription-retriggering.yaml new file mode 100644 index 00000000000..72e22eedad5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-improve-efficiency-of-subscription-retriggering.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-support-meta-for-in-memory-matching.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-support-meta-for-in-memory-matching.yaml new file mode 100644 index 00000000000..740ac3eff24 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4742-support-meta-for-in-memory-matching.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 462c31f1368..ed5e86aa464 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -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 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 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()); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index d06a3db65af..c1f84d6e388 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -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)); } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java index 8b203f15c7a..d73cb43f9fc 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java @@ -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 UNSUPPORTED_PARAMETER_NAMES = Sets.newHashSet(Constants.PARAM_HAS, Constants.PARAM_TAG, Constants.PARAM_PROFILE, Constants.PARAM_SECURITY); + public static final Set 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. - *

- * 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. + *

+ * 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> theAndOrParams, IBaseResource theResource) { + if (theResource == null) { + return true; + } + return theAndOrParams.stream().allMatch(nextAnd -> matchProfilesOr(nextAnd, theResource)); + } + + private boolean matchProfilesOr(List 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> theAndOrParams, IBaseResource theResource) { if (theResource == null) { return true; @@ -263,6 +326,54 @@ public class InMemoryResourceMatcher { return matches; } + private boolean matchTagsOrSecurityAndOr(List> theAndOrParams, IBaseResource theResource, boolean theTag) { + if (theResource == null) { + return true; + } + return theAndOrParams.stream().allMatch(nextAnd -> matchTagsOrSecurityOr(nextAnd, theResource, theTag)); + } + + private boolean matchTagsOrSecurityOr(List 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 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> 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 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} + } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/IndexedSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/IndexedSearchParamExtractor.java index e4499a20e63..525f7148aae 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/IndexedSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/IndexedSearchParamExtractor.java @@ -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; } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/SearchParamMatcher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/SearchParamMatcher.java index 272c404b492..7e5dbae7b02 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/SearchParamMatcher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/SearchParamMatcher.java @@ -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) { diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherConfigurationR5Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherConfigurationR5Test.java index d8bfac3c3c0..4b2e2bd103b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherConfigurationR5Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherConfigurationR5Test.java @@ -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: 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: Reason: Qualified parameter not supported", result.getUnsupportedReason()); } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java index 52b0be42ba9..88506c14106 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java @@ -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: 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: 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()); } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index e92fd7f5b7f..b8e6603b4c1 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -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 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); + } + + } + } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchDeliverer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchDeliverer.java index 5c6fd25b6e3..7bda78a60db 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchDeliverer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchDeliverer.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java index d9c8288f4aa..945269594ad 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java @@ -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> theResourceIds, List> theSearchUrls, @IdParam IIdType theSubscriptionId) { + public IBaseParameters triggerSubscription(@Nullable List> theResourceIds, @Nullable List> 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> 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 listToPopulate = new ArrayList<>(); + + myTransactionService + .withSystemRequest() + .execute(() -> { + searchBuilder.loadResourcesByPid(resourceIds, Collections.emptyList(), listToPopulate, false, new SystemRequestDetails()); + }); + + for (IBaseResource nextResource : listToPopulate) { Future 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; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java index c6fb134ab3a..7cdb4806e03 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java index 20bcd19b9b9..1f9bf66550f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java index 9d74424815f..d4458a02b09 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java index f583dc7b216..a9c1d5d7bc8 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java index 559c74ff185..9b689569d35 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java index e94ff0ef9fc..80ba64a0acd 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicSupport.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicSupport.java index a24bfcd0764..8798dd1339f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicSupport.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicSupport.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java index 66f9e8dd4cb..0250f3d05eb 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java @@ -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; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java index 4b8503c2fa4..77773083ba3 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java @@ -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; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamIT.java index 723d6c402ed..124678066b1 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamIT.java @@ -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")); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index ec4bda7de38..0a4f0e93432 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -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 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! */ diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java index 25e0401f23f..cc0e8cf137c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java @@ -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 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); + } + } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/CareCapsConstants.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/CareCapsConstants.java index a59e8bd20fb..c4789cbc952 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/CareCapsConstants.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/CareCapsConstants.java @@ -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 { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/HtmlConstants.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/HtmlConstants.java index 44e7137426a..ac0363c68ca 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/HtmlConstants.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/constant/HtmlConstants.java @@ -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 { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/enumeration/CareGapsStatusCode.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/enumeration/CareGapsStatusCode.java index f6cf9f97626..1cc35b4fab1 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/enumeration/CareGapsStatusCode.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/enumeration/CareGapsStatusCode.java @@ -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; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index f9a658027b3..8a9857361e8 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -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; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsService.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsService.java index 1522135fecf..8afbca73c93 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsService.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsService.java @@ -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; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ISubmitDataService.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ISubmitDataService.java index ee2bbcf1581..999c3933f3f 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ISubmitDataService.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/ISubmitDataService.java @@ -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; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataProvider.java index 94720acd921..1a630aa8cfb 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataProvider.java @@ -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; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataService.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataService.java index 9226c4dfbdb..28603c4abf5 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataService.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/SubmitDataService.java @@ -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; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java index ebcb329c82e..abd7e49f65c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java @@ -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; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index 7c7eba89a51..e6564abcd9e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -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?"); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java index 204417fb644..2684cbcb0cd 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java @@ -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> { @Autowired private ApplicationContext myApplicationContext; - @Autowired - private JpaStorageSettings myStorageSettings; public ISearchBuilder newSearchBuilder(IDao theDao, String theResourceName, Class theResourceType) { return (ISearchBuilder) myApplicationContext.getBean(ISearchBuilder.SEARCH_BUILDER_BEAN_NAME, theDao, theResourceName, theResourceType); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/ISubscriptionTriggeringSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/ISubscriptionTriggeringSvc.java index d8b7cdc3163..73c4f3c0bda 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/ISubscriptionTriggeringSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/ISubscriptionTriggeringSvc.java @@ -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> theResourceIds, List> theSearchUrls, @IdParam IIdType theSubscriptionId); + IBaseParameters triggerSubscription(@Nullable List> theResourceIds, @Nullable List> theSearchUrls, @Nullable IIdType theSubscriptionId); void runDeliveryPass(); }