Mb implement token :not inmemory (#3784)

Implement token :not and fix :not-in in InMemoryMatcher

Both `:not` and `:not-in` require that NONE of the values in an OR-list match, so we need some machinery to do a none-match instead of any-match.
This commit is contained in:
michaelabuckley 2022-07-13 11:51:24 -04:00 committed by GitHub
parent 6ba84e1c51
commit 1785c07283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 85 additions and 28 deletions

View File

@ -95,4 +95,13 @@ public enum TokenParamModifier {
return VALUE_TO_ENUM.get(theValue);
}
public boolean isNegative() {
switch (this) {
case NOT:
case NOT_IN:
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,23 @@
package ca.uhn.fhir.rest.param;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import java.util.EnumSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TokenParamModifierTest {
@ParameterizedTest
@EnumSource()
void negativeModifiers(TokenParamModifier theTokenParamModifier) {
EnumSet<TokenParamModifier> negativeSet = EnumSet.of(
TokenParamModifier.NOT,
TokenParamModifier.NOT_IN
);
assertEquals(negativeSet.contains(theTokenParamModifier), theTokenParamModifier.isNegative());
}
}

View File

@ -0,0 +1,5 @@
---
type: add
issue: 3784
title: "The InMemoryMatcher used by Subscription matching not supports the token `:not` modifier.
The `:not-in` modifier was also corrected for cases of multiple values in a `,` separated or-list."

View File

@ -42,6 +42,7 @@ 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.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.MetaUtil;
@ -308,8 +309,27 @@ 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 -> matchParam(theModelConfig, theResourceName, theParamName, paramDef, theSearchParams, token));
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList, ResourceIndexedSearchParams theSearchParams) {
boolean isNegativeTest = isNegative(theParamDef, theOrList);
// negative tests like :not and :not-in must not match any or-clause, so we invert the quantifier.
if (isNegativeTest) {
return theOrList.stream().allMatch(token -> matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theSearchParams, token));
} else {
return theOrList.stream().anyMatch(token -> matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theSearchParams, token));
}
}
/** Some modifiers are negative, and must match NONE of their or-list */
private boolean isNegative(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList) {
if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) {
TokenParam tokenParam = (TokenParam) theOrList.get(0);
TokenParamModifier modifier = tokenParam.getModifier();
return modifier != null && modifier.isNegative();
} else {
return false;
}
}
private boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, IQueryParameterType theToken) {
@ -322,6 +342,7 @@ public class InMemoryResourceMatcher {
/**
* Checks whether a query parameter of type token matches one of the search parameters of an in-memory resource.
* 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 theModelConfig a model configuration
@ -343,6 +364,8 @@ public class InMemoryResourceMatcher {
return theSearchParams.myTokenParams.stream()
.filter(t -> t.getParamName().equals(theParamName))
.noneMatch(t -> systemContainsCode(theQueryParam, t));
case NOT:
return !theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
default:
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
}
@ -426,6 +449,8 @@ public class InMemoryResourceMatcher {
case NOT_IN:
// Support for these qualifiers is dependent on an implementation of IValidationSupport being available to delegate the check to
return getValidationSupportOrNull() != null;
case NOT:
return true;
default:
return false;
}

View File

@ -140,10 +140,15 @@ public class InMemoryResourceMatcherR5Test {
}
@Test
public void testUnsupportedNot() {
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT.getValue() + "=" + OBSERVATION_CODE, myObservation, mySearchParams);
assertFalse(result.supported());
assertEquals("Parameter: <code:not> Reason: Qualified parameter not supported", result.getUnsupportedReason());
public void testSupportedNot() {
String criteria = "code" + TokenParamModifier.NOT.getValue() + "=" + OBSERVATION_CODE + ",a_different_code";
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams);
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);
assertTrue(result.supported());
assertTrue(result.matched(), ":not matches when NONE match");
}
@Test
@ -184,12 +189,20 @@ public class InMemoryResourceMatcherR5Test {
@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);
IValidationSupport.CodeValidationResult matchResult = new IValidationSupport.CodeValidationResult().setCode(OBSERVATION_CODE);
IValidationSupport.CodeValidationResult noMatchResult = new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("not in");
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT_IN.getValue() + "=" + OBSERVATION_CODE_VALUE_SET_URI, myObservation, mySearchParams);
// mock 2 value sets. Once containing the code, and one not.
String otherValueSet = OBSERVATION_CODE_VALUE_SET_URI + "-different";
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), eq(OBSERVATION_CODE_VALUE_SET_URI))).thenReturn(matchResult);
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);
assertTrue(result.supported());
assertFalse(result.matched());
assertFalse(result.matched(), ":not-in matches when NONE of the OR-list match");
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
}

View File

@ -745,24 +745,6 @@ public class InMemorySubscriptionMatcherR4Test {
}
}
@Test
public void testSearchTokenWithNotModifierUnsupported() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe");
patient.setGender(Enumerations.AdministrativeGender.MALE);
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_GENDER, new TokenParam(null, "male"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_GENDER, new TokenParam(null, "male").setModifier(TokenParamModifier.NOT));
assertUnsupported(patient, params);
}
@Test
public void testSearchTokenWrongParam() {
Patient p1 = new Patient();