mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
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:
parent
6ba84e1c51
commit
1785c07283
@ -95,4 +95,13 @@ public enum TokenParamModifier {
|
|||||||
return VALUE_TO_ENUM.get(theValue);
|
return VALUE_TO_ENUM.get(theValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isNegative() {
|
||||||
|
switch (this) {
|
||||||
|
case NOT:
|
||||||
|
case NOT_IN:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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."
|
@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
|||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
import ca.uhn.fhir.rest.param.StringParam;
|
import ca.uhn.fhir.rest.param.StringParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
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.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.util.MetaUtil;
|
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) {
|
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList, ResourceIndexedSearchParams theSearchParams) {
|
||||||
return theNextAnd.stream().anyMatch(token -> matchParam(theModelConfig, theResourceName, theParamName, paramDef, theSearchParams, token));
|
|
||||||
|
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) {
|
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.
|
* 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.
|
* 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.
|
* Any other qualifier will be ignored and the match will be treated as unqualified.
|
||||||
* @param theModelConfig a model configuration
|
* @param theModelConfig a model configuration
|
||||||
@ -343,6 +364,8 @@ public class InMemoryResourceMatcher {
|
|||||||
return theSearchParams.myTokenParams.stream()
|
return theSearchParams.myTokenParams.stream()
|
||||||
.filter(t -> t.getParamName().equals(theParamName))
|
.filter(t -> t.getParamName().equals(theParamName))
|
||||||
.noneMatch(t -> systemContainsCode(theQueryParam, t));
|
.noneMatch(t -> systemContainsCode(theQueryParam, t));
|
||||||
|
case NOT:
|
||||||
|
return !theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
|
||||||
default:
|
default:
|
||||||
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
|
return theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, theParamDef, theQueryParam);
|
||||||
}
|
}
|
||||||
@ -426,6 +449,8 @@ public class InMemoryResourceMatcher {
|
|||||||
case NOT_IN:
|
case NOT_IN:
|
||||||
// Support for these qualifiers is dependent on an implementation of IValidationSupport being available to delegate the check to
|
// Support for these qualifiers is dependent on an implementation of IValidationSupport being available to delegate the check to
|
||||||
return getValidationSupportOrNull() != null;
|
return getValidationSupportOrNull() != null;
|
||||||
|
case NOT:
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -140,10 +140,15 @@ public class InMemoryResourceMatcherR5Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUnsupportedNot() {
|
public void testSupportedNot() {
|
||||||
InMemoryMatchResult result = myInMemoryResourceMatcher.match("code" + TokenParamModifier.NOT.getValue() + "=" + OBSERVATION_CODE, myObservation, mySearchParams);
|
String criteria = "code" + TokenParamModifier.NOT.getValue() + "=" + OBSERVATION_CODE + ",a_different_code";
|
||||||
assertFalse(result.supported());
|
InMemoryMatchResult result = myInMemoryResourceMatcher.match(criteria, myObservation, mySearchParams);
|
||||||
assertEquals("Parameter: <code:not> Reason: Qualified parameter not supported", result.getUnsupportedReason());
|
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
|
@Test
|
||||||
@ -184,12 +189,20 @@ public class InMemoryResourceMatcherR5Test {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSupportedNotIn_NoMatch() {
|
public void testSupportedNotIn_NoMatch() {
|
||||||
IValidationSupport.CodeValidationResult codeValidationResult = new IValidationSupport.CodeValidationResult().setCode(OBSERVATION_CODE);
|
IValidationSupport.CodeValidationResult matchResult = new IValidationSupport.CodeValidationResult().setCode(OBSERVATION_CODE);
|
||||||
when(myValidationSupport.validateCode(any(), any(), any(), any(), any(), any())).thenReturn(codeValidationResult);
|
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());
|
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));
|
verify(myValidationSupport).validateCode(any(), any(), eq(OBSERVATION_CODE_SYSTEM), eq(OBSERVATION_CODE), isNull(), eq(OBSERVATION_CODE_VALUE_SET_URI));
|
||||||
}
|
}
|
||||||
|
@ -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
|
@Test
|
||||||
public void testSearchTokenWrongParam() {
|
public void testSearchTokenWrongParam() {
|
||||||
Patient p1 = new Patient();
|
Patient p1 = new Patient();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user