add support for in and not-in qualifiers (#3680)

* add support for in and not-in qualifiers

* fix failing test

* fix broken test

* fix broken tests

* code review comments

* Make dependency on IValidationSupport optional

* create IValidationSupport dependency only if needed

* small design improvements

* changelog

* rework validation support initialization

* Update hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java

Co-authored-by: Ken Stevens <khstevens@gmail.com>

Co-authored-by: Ken Stevens <khstevens@gmail.com>
This commit is contained in:
JasonRoberts-smile 2022-06-16 16:17:57 -04:00 committed by GitHub
parent ac8f4f1e56
commit cedeaa04f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 406 additions and 62 deletions

View File

@ -25,7 +25,7 @@ public final class Msg {
/**
* IMPORTANT: Please update the following comment after you add a new code
* Last code value: 2094
* Last code value: 2096
*/
private Msg() {}

View File

@ -0,0 +1,4 @@
---
type: add
issue: 3620
title: "Add support for the `:in` and `:not-in` qualifiers for use in SMART on FHIR v2 granular scope definition."

View File

@ -2389,7 +2389,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
@Nonnull
private IValidationSupport provideValidationSupport() {
protected IValidationSupport provideValidationSupport() {
IValidationSupport validationSupport = myValidationSupport;
if (validationSupport == null) {
validationSupport = myApplicationContext.getBean(IValidationSupport.class);

View File

@ -45,9 +45,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class TermReadSvcDstu2 extends BaseTermReadSvcImpl {
@Autowired
private IValidationSupport myValidationSupport;
private void addAllChildren(String theSystemString, ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept theCode, List<FhirVersionIndependentConcept> theListToPopulate) {
if (isNotBlank(theCode.getCode())) {
theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theCode.getCode()));
@ -106,7 +103,7 @@ public class TermReadSvcDstu2 extends BaseTermReadSvcImpl {
@Override
public List<FhirVersionIndependentConcept> findCodesAboveUsingBuiltInSystems(String theSystem, String theCode) {
ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
ca.uhn.fhir.model.dstu2.resource.ValueSet system = (ca.uhn.fhir.model.dstu2.resource.ValueSet) myValidationSupport.fetchCodeSystem(theSystem);
ca.uhn.fhir.model.dstu2.resource.ValueSet system = (ca.uhn.fhir.model.dstu2.resource.ValueSet) provideValidationSupport().fetchCodeSystem(theSystem);
if (system != null) {
findCodesAbove(system, theSystem, theCode, retVal);
}
@ -131,7 +128,7 @@ public class TermReadSvcDstu2 extends BaseTermReadSvcImpl {
@Override
public List<FhirVersionIndependentConcept> findCodesBelowUsingBuiltInSystems(String theSystem, String theCode) {
ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
ca.uhn.fhir.model.dstu2.resource.ValueSet system = (ca.uhn.fhir.model.dstu2.resource.ValueSet) myValidationSupport.fetchCodeSystem(theSystem);
ca.uhn.fhir.model.dstu2.resource.ValueSet system = (ca.uhn.fhir.model.dstu2.resource.ValueSet) provideValidationSupport().fetchCodeSystem(theSystem);
if (system != null) {
findCodesBelow(system, theSystem, theCode, retVal);
}

View File

@ -20,11 +20,16 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
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.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
@ -36,24 +41,35 @@ 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.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.MetaUtil;
import ca.uhn.fhir.util.UrlUtil;
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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
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 java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class InMemoryResourceMatcher {
private enum ValidationSupportInitializationState {NOT_INITIALIZED, INITIALIZED, FAILED}
public static final Set<String> UNSUPPORTED_PARAMETER_NAMES = Sets.newHashSet(Constants.PARAM_HAS, Constants.PARAM_TAG, Constants.PARAM_PROFILE, Constants.PARAM_SECURITY);
private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(InMemoryResourceMatcher.class);
@Autowired
ApplicationContext myApplicationContext;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
@ -63,8 +79,32 @@ public class InMemoryResourceMatcher {
@Autowired
FhirContext myFhirContext;
private ValidationSupportInitializationState validationSupportState = ValidationSupportInitializationState.NOT_INITIALIZED;
private IValidationSupport myValidationSupport = null;
public InMemoryResourceMatcher() {}
/**
* Lazy loads a {@link IValidationSupport} implementation just-in-time.
* If no suitable bean is available, or if a {@link ca.uhn.fhir.context.ConfigurationException} is thrown, matching
* can proceed, but the qualifiers that depend on the validation support will be disabled.
*
* @return A bean implementing {@link IValidationSupport} if one is available, otherwise null
*/
private IValidationSupport getValidationSupportOrNull() {
if (validationSupportState == ValidationSupportInitializationState.NOT_INITIALIZED) {
try {
myValidationSupport = myApplicationContext.getBean(IValidationSupport.class);
validationSupportState = ValidationSupportInitializationState.INITIALIZED;
} catch (BeansException | ConfigurationException ignore) {
// We couldn't get a validation support bean, and we don't want to waste cycles trying again
ourLog.warn(Msg.code(2095) + "No bean satisfying IValidationSupport could be initialized. Qualifiers dependent on IValidationSupport will not be supported.");
validationSupportState = ValidationSupportInitializationState.FAILED;
}
}
return myValidationSupport;
}
/**
* 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.
@ -139,26 +179,9 @@ public class InMemoryResourceMatcher {
return InMemoryMatchResult.successfulMatch();
}
if (hasQualifiers(theAndOrParams)) {
Optional<IQueryParameterType> optionalParameter = theAndOrParams.stream().flatMap(List::stream).filter(param -> param.getQueryParameterQualifier() != null).findAny();
if (optionalParameter.isPresent()) {
IQueryParameterType parameter = optionalParameter.get();
if (parameter instanceof ReferenceParam) {
ReferenceParam referenceParam = (ReferenceParam) parameter;
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + "." + referenceParam.getChain(), InMemoryMatchResult.CHAIN);
}
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + parameter.getQueryParameterQualifier(), InMemoryMatchResult.QUALIFIER);
}
}
if (hasChain(theAndOrParams)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.CHAIN);
}
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
InMemoryMatchResult checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, paramDef, theAndOrParams);
InMemoryMatchResult checkUnsupportedResult = checkForUnsupportedParameters(theParamName, paramDef, theAndOrParams);
if (!checkUnsupportedResult.supported()) {
return checkUnsupportedResult;
}
@ -166,12 +189,6 @@ public class InMemoryResourceMatcher {
switch (theParamName) {
case IAnyResource.SP_RES_ID:
return InMemoryMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource));
case Constants.PARAM_HAS:
case Constants.PARAM_TAG:
case Constants.PARAM_PROFILE:
case Constants.PARAM_SECURITY:
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM);
case Constants.PARAM_SOURCE:
return InMemoryMatchResult.fromBoolean(matchSourcesAndOr(theAndOrParams, theResource));
default:
@ -179,6 +196,45 @@ public class InMemoryResourceMatcher {
}
}
private InMemoryMatchResult checkForUnsupportedParameters(String theParamName, RuntimeSearchParam theParamDef, List<List<IQueryParameterType>> theAndOrParams) {
if (UNSUPPORTED_PARAMETER_NAMES.contains(theParamName)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM);
}
for (List<IQueryParameterType> orParams : theAndOrParams) {
// The list should never be empty, but better safe than sorry
if (orParams.size() > 0) {
// The params in each OR list all share the same qualifier, prefix, etc., so we only need to check the first one
InMemoryMatchResult checkUnsupportedResult = checkOneParameterForUnsupportedModifiers(theParamName, theParamDef, orParams.get(0));
if (!checkUnsupportedResult.supported()) {
return checkUnsupportedResult;
}
}
}
return InMemoryMatchResult.successfulMatch();
}
private InMemoryMatchResult checkOneParameterForUnsupportedModifiers(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
// Assume we're ok until we find evidence we aren't
InMemoryMatchResult checkUnsupportedResult = InMemoryMatchResult.successfulMatch();
if (hasChain(theParam)) {
checkUnsupportedResult = InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + "." + ((ReferenceParam)theParam).getChain(), InMemoryMatchResult.CHAIN);
}
if (checkUnsupportedResult.supported()) {
checkUnsupportedResult = checkUnsupportedQualifiers(theParamName, theParamDef, theParam);
}
if (checkUnsupportedResult.supported()) {
checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, theParamDef, theParam);
}
return checkUnsupportedResult;
}
private boolean matchSourcesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
if (theResource == null) {
return true;
@ -249,29 +305,77 @@ public class InMemoryResourceMatcher {
}
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token));
return theNextAnd.stream().anyMatch(token -> matchParam(theModelConfig, theResourceName, theParamName, paramDef, theSearchParams, token));
}
private boolean hasChain(List<List<IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param instanceof ReferenceParam && ((ReferenceParam) param).getChain() != null);
private boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, IQueryParameterType theToken) {
if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) {
return matchTokenParam(theModelConfig, theResourceName, theParamName, theParamDef, theSearchParams, (TokenParam) theToken);
} else {
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theToken);
}
}
private boolean hasQualifiers(List<List<IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param.getQueryParameterQualifier() != null);
/**
* Checks whether a query parameter of type token matches one of the search parameters of an in-memory resource.
* 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 theModelConfig 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
* @return true if theQueryParam matches the collection of theSearchParams, otherwise false
*/
private boolean matchTokenParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, TokenParam theQueryParam) {
if (theQueryParam.getModifier() != null) {
switch (theQueryParam.getModifier()) {
case IN:
return theSearchParams.myTokenParams.stream()
.filter(t -> t.getParamName().equals(theParamName))
.anyMatch(t -> systemContainsCode(theQueryParam, t));
case NOT_IN:
return theSearchParams.myTokenParams.stream()
.filter(t -> t.getParamName().equals(theParamName))
.noneMatch(t -> systemContainsCode(theQueryParam, t));
default:
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
}
} else {
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
}
}
private InMemoryMatchResult checkUnsupportedPrefixes(String theParamName, RuntimeSearchParam theParamDef, List<List<IQueryParameterType>> theAndOrParams) {
if (theParamDef != null) {
for (List<IQueryParameterType> theAndOrParam : theAndOrParams) {
for (IQueryParameterType param : theAndOrParam) {
if (param instanceof BaseParamWithPrefix) {
ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) param).getPrefix();
RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
if (!supportedPrefix(prefix, paramType)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, String.format("The prefix %s is not supported for param type %s", prefix, paramType));
}
}
}
private boolean systemContainsCode(TokenParam theQueryParam, ResourceIndexedSearchParamToken theSearchParamToken) {
IValidationSupport validationSupport = getValidationSupportOrNull();
if (validationSupport == null) {
ourLog.error(Msg.code(2096) + "Attempting to evaluate an unsupported qualifier. This should not happen.");
return false;
}
IValidationSupport.CodeValidationResult codeValidationResult = validationSupport.validateCode(new ValidationSupportContext(validationSupport), new ConceptValidationOptions(), theSearchParamToken.getSystem(), theSearchParamToken.getValue(), null, theQueryParam.getValue());
if (codeValidationResult != null) {
return codeValidationResult.isOk();
} else {
return false;
}
}
private boolean hasChain(IQueryParameterType theParam) {
return theParam instanceof ReferenceParam && ((ReferenceParam) theParam).getChain() != null;
}
private boolean hasQualifiers(IQueryParameterType theParam) {
return theParam.getQueryParameterQualifier() != null;
}
private InMemoryMatchResult checkUnsupportedPrefixes(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
if (theParamDef != null && theParam instanceof BaseParamWithPrefix) {
ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) theParam).getPrefix();
RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
if (!supportedPrefix(prefix, paramType)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, String.format("The prefix %s is not supported for param type %s", prefix, paramType));
}
}
return InMemoryMatchResult.successfulMatch();
@ -298,4 +402,32 @@ public class InMemoryResourceMatcher {
}
return false;
}
private InMemoryMatchResult checkUnsupportedQualifiers(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
if (hasQualifiers(theParam) && !supportedQualifier(theParamDef, theParam)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + theParam.getQueryParameterQualifier(), InMemoryMatchResult.QUALIFIER);
}
return InMemoryMatchResult.successfulMatch();
}
private boolean supportedQualifier(RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
if (theParamDef == null || theParam == null) {
return true;
}
switch (theParamDef.getParamType()) {
case TOKEN:
TokenParam tokenParam = (TokenParam) theParam;
switch (tokenParam.getModifier()) {
case IN:
case NOT_IN:
// Support for these qualifiers is dependent on an implementation of IValidationSupport being available to delegate the check to
return getValidationSupportOrNull() != null;
default:
return false;
}
default:
return false;
}
}
}

View File

@ -0,0 +1,133 @@
package ca.uhn.fhir.jpa.searchparam.matcher;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
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.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Observation;
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.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class InMemoryResourceMatcherConfigurationR5Test {
public static final String OBSERVATION_CODE = "MATCH";
public static final String OBSERVATION_CODE_SYSTEM = "http://hl7.org/some-cs";
public static final String OBSERVATION_CODE_DISPLAY = "Match";
public static final String OBSERVATION_CODE_VALUE_SET_URI = "http://hl7.org/some-vs";
@MockBean
ISearchParamRegistry mySearchParamRegistry;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
private Observation myObservation;
private ResourceIndexedSearchParams mySearchParams;
@BeforeEach
public void before() {
RuntimeSearchParam codeSearchParam = new RuntimeSearchParam(null, null, null, null, "Observation.code", RestSearchParameterTypeEnum.TOKEN, null, null, RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, null, null, null);
when(mySearchParamRegistry.getActiveSearchParam("Observation", "code")).thenReturn(codeSearchParam);
myObservation = new Observation();
CodeableConcept codeableConcept = new CodeableConcept();
codeableConcept.addCoding().setCode(OBSERVATION_CODE)
.setSystem(OBSERVATION_CODE_SYSTEM).setDisplay(OBSERVATION_CODE_DISPLAY);
myObservation.setCode(codeableConcept);
mySearchParams = extractSearchParams(myObservation);
}
@Test
@Order(1) // We have to do this one first, because the InMemoryResourceMatcher is stateful regarding whether it has been initialized yet
public void testValidationSupportInitializedOnlyOnce() {
ApplicationContext applicationContext = mock(ApplicationContext.class);
when(applicationContext.getBean(IValidationSupport.class)).thenThrow(new ConfigurationException());
myInMemoryResourceMatcher.myApplicationContext = applicationContext;
for (int i = 0; i < 10; i++) {
myInMemoryResourceMatcher.match("code" + TokenParamModifier.IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
}
verify(applicationContext, times(1)).getBean(IValidationSupport.class);
}
@Test
@Order(2)
/*
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);
assertFalse(result.supported());
assertEquals("Parameter: <code:in> Reason: Qualified parameter not supported", result.getUnsupportedReason());
}
@Test
@Order(3)
public void testUnsupportedNotIn() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
assertFalse(result.supported());
assertEquals("Parameter: <code:not-in> Reason: Qualified parameter not supported", result.getUnsupportedReason());
}
private ResourceIndexedSearchParams extractSearchParams(Observation theObservation) {
ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams();
retval.myTokenParams.add(extractCodeTokenParam(theObservation));
return retval;
}
private ResourceIndexedSearchParamToken extractCodeTokenParam(Observation theObservation) {
Coding coding = theObservation.getCode().getCodingFirstRep();
return new ResourceIndexedSearchParamToken(new PartitionSettings(), "Observation", "code", coding.getSystem(), coding.getCode());
}
@Configuration
public static class SpringConfig {
@Bean
InMemoryResourceMatcher inMemoryResourceMatcher() {
return new InMemoryResourceMatcher();
}
@Bean
MatchUrlService matchUrlService() {
return new MatchUrlService();
}
@Bean
FhirContext fhirContext() {
return FhirContext.forR5();
}
@Bean
ModelConfig modelConfig() {
return new ModelConfig();
}
}
}

View File

@ -2,9 +2,11 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
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.model.primitive.BaseDateTimeDt;
@ -15,6 +17,7 @@ import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.r5.model.BaseDateTimeType;
import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.Observation;
import org.junit.jupiter.api.BeforeEach;
@ -27,6 +30,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
@ -35,6 +39,10 @@ import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@ -42,6 +50,9 @@ public class InMemoryResourceMatcherR5Test {
public static final String OBSERVATION_DATE = "1970-10-17";
public static final String OBSERVATION_DATETIME = OBSERVATION_DATE + "T01:00:00-08:30";
public static final String OBSERVATION_CODE = "MATCH";
public static final String OBSERVATION_CODE_SYSTEM = "http://hl7.org/some-cs";
public static final String OBSERVATION_CODE_DISPLAY = "Match";
public static final String OBSERVATION_CODE_VALUE_SET_URI = "http://hl7.org/some-vs";
private static final String EARLY_DATE = "1965-08-09";
private static final String LATE_DATE = "2000-06-29";
private static final String EARLY_DATETIME = EARLY_DATE + "T12:00:00Z";
@ -52,6 +63,8 @@ public class InMemoryResourceMatcherR5Test {
@MockBean
ISearchParamRegistry mySearchParamRegistry;
@MockBean
IValidationSupport myValidationSupport;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
private Observation myObservation;
@ -72,9 +85,10 @@ public class InMemoryResourceMatcherR5Test {
myObservation.getMeta().setSource(TEST_SOURCE);
myObservation.setEffective(new DateTimeType(OBSERVATION_DATETIME));
CodeableConcept codeableConcept = new CodeableConcept();
codeableConcept.addCoding().setCode(OBSERVATION_CODE);
codeableConcept.addCoding().setCode(OBSERVATION_CODE)
.setSystem(OBSERVATION_CODE_SYSTEM).setDisplay(OBSERVATION_CODE_DISPLAY);
myObservation.setCode(codeableConcept);
mySearchParams = extractDateSearchParam(myObservation);
mySearchParams = extractSearchParams(myObservation);
}
@Test
@ -132,6 +146,54 @@ public class InMemoryResourceMatcherR5Test {
assertEquals("Parameter: <code:not> Reason: Qualified parameter not supported", result.getUnsupportedReason());
}
@Test
public void testSupportedIn() {
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);
assertTrue(result.supported());
assertTrue(result.matched());
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
}
@Test
public void testSupportedIn_NoMatch() {
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);
assertTrue(result.supported());
assertFalse(result.matched());
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
}
@Test
public void testSupportedNotIn() {
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);
assertTrue(result.supported());
assertTrue(result.matched());
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
}
@Test
public void testSupportedNotIn_NoMatch() {
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.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
assertTrue(result.supported());
assertFalse(result.matched());
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
}
@Test
public void testDateUnsupportedDateOps() {
testDateUnsupportedDateOp(ParamPrefixEnum.APPROXIMATE);
@ -204,7 +266,7 @@ public class InMemoryResourceMatcherR5Test {
Observation futureObservation = new Observation();
Instant nextWeek = Instant.now().plus(Duration.ofDays(7));
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.supported(), result.getUnsupportedReason());
@ -218,7 +280,7 @@ public class InMemoryResourceMatcherR5Test {
Observation futureObservation = new Observation();
Instant nextMinute = Instant.now().plus(Duration.ofMinutes(1));
futureObservation.setEffective(new DateTimeType(Date.from(nextMinute)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.NOW_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.supported(), result.getUnsupportedReason());
@ -237,7 +299,7 @@ public class InMemoryResourceMatcherR5Test {
Observation futureObservation = new Observation();
Instant nextWeek = Instant.now().plus(Duration.ofDays(7));
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.supported(), result.getUnsupportedReason());
@ -249,7 +311,7 @@ public class InMemoryResourceMatcherR5Test {
Observation futureObservation = new Observation();
Instant nextWeek = Instant.now().minus(Duration.ofDays(1));
futureObservation.setEffective(new DateTimeType(Date.from(nextWeek)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.supported(), result.getUnsupportedReason());
@ -269,7 +331,7 @@ public class InMemoryResourceMatcherR5Test {
}
Instant nextMinute = now.toInstant().plus(Duration.ofMinutes(1));
futureObservation.setEffective(new DateTimeType(Date.from(nextMinute)));
ResourceIndexedSearchParams searchParams = extractDateSearchParam(futureObservation);
ResourceIndexedSearchParams searchParams = extractSearchParams(futureObservation);
InMemoryMatchResult result = myInMemoryResourceMatcher.match("date=gt" + BaseDateTimeDt.TODAY_DATE_CONSTANT, futureObservation, searchParams);
assertTrue(result.supported(), result.getUnsupportedReason());
@ -280,11 +342,11 @@ public class InMemoryResourceMatcherR5Test {
public void testInPeriod() {
Observation insidePeriodObservation = new Observation();
insidePeriodObservation.setEffective(new DateTimeType("1985-01-01T00:00:00Z"));
ResourceIndexedSearchParams insidePeriodSearchParams = extractDateSearchParam(insidePeriodObservation);
ResourceIndexedSearchParams insidePeriodSearchParams = extractSearchParams(insidePeriodObservation);
Observation outsidePeriodObservation = new Observation();
outsidePeriodObservation.setEffective(new DateTimeType("2010-01-01T00:00:00Z"));
ResourceIndexedSearchParams outsidePeriodSearchParams = extractDateSearchParam(outsidePeriodObservation);
ResourceIndexedSearchParams outsidePeriodSearchParams = extractSearchParams(outsidePeriodObservation);
String search = "date=gt" + EARLY_DATE + "&date=le" + LATE_DATE;
@ -298,14 +360,24 @@ public class InMemoryResourceMatcherR5Test {
}
private ResourceIndexedSearchParams extractDateSearchParam(Observation theObservation) {
private ResourceIndexedSearchParams extractSearchParams(Observation theObservation) {
ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams();
BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective();
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValueAsString());
retval.myDateParams.add(dateParam);
retval.myDateParams.add(extractEffectiveDateParam(theObservation));
retval.myTokenParams.add(extractCodeTokenParam(theObservation));
return retval;
}
@Nonnull
private ResourceIndexedSearchParamDate extractEffectiveDateParam(Observation theObservation) {
BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective();
return new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValueAsString());
}
private ResourceIndexedSearchParamToken extractCodeTokenParam(Observation theObservation) {
Coding coding = theObservation.getCode().getCodingFirstRep();
return new ResourceIndexedSearchParamToken(new PartitionSettings(), "Observation", "code", coding.getSystem(), coding.getCode());
}
@Configuration
public static class SpringConfig {
@Bean

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.searchparam.registry;
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;
@ -143,6 +144,11 @@ public class SearchParamRegistryImplTest {
return retval;
}
@Bean
IValidationSupport validationSupport() {
return mock(IValidationSupport.class);
}
}
@Nonnull