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:
parent
ac8f4f1e56
commit
cedeaa04f1
|
@ -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() {}
|
||||
|
|
|
@ -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."
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue