Fix for path parsing in STU3 SPs (#3503)
This commit is contained in:
parent
f88d2d63bd
commit
05bc208192
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 3469
|
||||||
|
title: "Previously, `or` expressions were not being properly validated in FHIRPath in STU3 due to a bug with expression path splitting. This has been corrected."
|
|
@ -64,8 +64,10 @@ public class FhirResourceDaoSearchParameterDstu3 extends BaseHapiFhirResourceDao
|
||||||
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
|
protected void validateResourceForStorage(SearchParameter theResource, ResourceTable theEntityToSave) {
|
||||||
super.validateResourceForStorage(theResource, theEntityToSave);
|
super.validateResourceForStorage(theResource, theEntityToSave);
|
||||||
|
|
||||||
|
org.hl7.fhir.r4.model.SearchParameter resource = (org.hl7.fhir.r4.model.SearchParameter) VersionConvertorFactory_30_40.convertResource(theResource, new BaseAdvisor_30_40(false));
|
||||||
|
|
||||||
FhirResourceDaoSearchParameterR4.validateSearchParam(
|
FhirResourceDaoSearchParameterR4.validateSearchParam(
|
||||||
(org.hl7.fhir.r4.model.SearchParameter) VersionConvertorFactory_30_40.convertResource(theResource, new BaseAdvisor_30_40(false)),
|
resource,
|
||||||
getContext(), getConfig(), mySearchParamRegistry, mySearchParamExtractor);
|
getContext(), getConfig(), mySearchParamRegistry, mySearchParamExtractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,9 +174,7 @@ public class FhirResourceDaoSearchParameterR4 extends BaseHapiFhirResourceDao<Se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (!isUnique && theResource.getType() != Enumerations.SearchParamType.COMPOSITE && theResource.getType() != Enumerations.SearchParamType.SPECIAL && !REGEX_SP_EXPRESSION_HAS_PATH.matcher(expression).matches()) {
|
if (!isUnique && theResource.getType() != Enumerations.SearchParamType.COMPOSITE && theResource.getType() != Enumerations.SearchParamType.SPECIAL && !REGEX_SP_EXPRESSION_HAS_PATH.matcher(expression).matches()) {
|
||||||
throw new UnprocessableEntityException(Msg.code(1120) + "SearchParameter.expression value \"" + expression + "\" is invalid");
|
throw new UnprocessableEntityException(Msg.code(1120) + "SearchParameter.expression value \"" + expression + "\" is invalid");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
|
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.model.api.Include;
|
import ca.uhn.fhir.model.api.Include;
|
||||||
|
@ -53,6 +54,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.hamcrest.Matchers.startsWith;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
@ -87,6 +90,34 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateSpWithMultiplePaths(){
|
||||||
|
SearchParameter sp = new SearchParameter();
|
||||||
|
sp.setCode("telephone-unformatted");
|
||||||
|
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
sp.setExpression("Patient.telecom.where(system='phone' or system='email')");
|
||||||
|
sp.getBase().add(new CodeType("Patient"));
|
||||||
|
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||||
|
|
||||||
|
DaoMethodOutcome daoMethodOutcome = mySearchParameterDao.create(sp);
|
||||||
|
assertThat(daoMethodOutcome.getId(), is(notNullValue()));
|
||||||
|
|
||||||
|
sp.setExpression("Patient.telecom.where(system='phone') or Patient.telecome.where(system='email')");
|
||||||
|
sp.setCode("telephone-unformatted-2");
|
||||||
|
daoMethodOutcome = mySearchParameterDao.create(sp);
|
||||||
|
assertThat(daoMethodOutcome.getId(), is(notNullValue()));
|
||||||
|
|
||||||
|
sp.setExpression("Patient.telecom.where(system='phone' or system='email') | Patient.telecome.where(system='email')");
|
||||||
|
sp.setCode("telephone-unformatted-3");
|
||||||
|
daoMethodOutcome = mySearchParameterDao.create(sp);
|
||||||
|
assertThat(daoMethodOutcome.getId(), is(notNullValue()));
|
||||||
|
|
||||||
|
sp.setExpression("Patient.telecom.where(system='phone' or system='email') | Patient.telecom.where(system='email') or Patient.telecom.where(system='mail' | system='phone')");
|
||||||
|
sp.setCode("telephone-unformatted-3");
|
||||||
|
daoMethodOutcome = mySearchParameterDao.create(sp);
|
||||||
|
assertThat(daoMethodOutcome.getId(), is(notNullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateInvalidParamInvalidResourceName() {
|
public void testCreateInvalidParamInvalidResourceName() {
|
||||||
SearchParameter fooSp = new SearchParameter();
|
SearchParameter fooSp = new SearchParameter();
|
||||||
|
|
|
@ -96,7 +96,6 @@ import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
|
@ -85,7 +85,6 @@ import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
@ -95,7 +94,6 @@ import static org.apache.commons.lang3.StringUtils.trim;
|
||||||
public abstract class BaseSearchParamExtractor implements ISearchParamExtractor {
|
public abstract class BaseSearchParamExtractor implements ISearchParamExtractor {
|
||||||
|
|
||||||
public static final Set<String> COORDS_INDEX_PATHS;
|
public static final Set<String> COORDS_INDEX_PATHS;
|
||||||
private static final Pattern SPLIT = Pattern.compile("\\||( or )");
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSearchParamExtractor.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSearchParamExtractor.class);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -1119,10 +1117,53 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
||||||
if (!thePaths.contains("|") && !thePaths.contains(" or ")) {
|
if (!thePaths.contains("|") && !thePaths.contains(" or ")) {
|
||||||
return new String[]{thePaths};
|
return new String[]{thePaths};
|
||||||
}
|
}
|
||||||
return SPLIT.split(thePaths);
|
return splitOutOfParensOrs(thePaths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iteratively splits a string on any ` or ` or | that is ** not** contained inside a set of parentheses. e.g.
|
||||||
|
*
|
||||||
|
* "Patient.select(a or b)" --> ["Patient.select(a or b)"]
|
||||||
|
* "Patient.select(a or b) or Patient.select(c or d )" --> ["Patient.select(a or b)", "Patient.select(c or d)"]
|
||||||
|
* "Patient.select(a|b) or Patient.select(c or d )" --> ["Patient.select(a|b)", "Patient.select(c or d)"]
|
||||||
|
* "Patient.select(b) | Patient.select(c)" --> ["Patient.select(b)", "Patient.select(c)"]
|
||||||
|
*
|
||||||
|
* @param thePaths The string to split
|
||||||
|
* @return The split string
|
||||||
|
|
||||||
|
*/
|
||||||
|
private String[] splitOutOfParensOrs(String thePaths) {
|
||||||
|
List<String> topLevelOrExpressions = splitOutOfParensToken(thePaths, " or ");
|
||||||
|
List<String> retVal = topLevelOrExpressions.stream()
|
||||||
|
.flatMap(s -> splitOutOfParensToken(s, "|").stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return retVal.toArray(new String[retVal.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitOutOfParensToken(String thePath, String theToken) {
|
||||||
|
int tokenLength = theToken.length();
|
||||||
|
int index = thePath.indexOf(theToken);
|
||||||
|
int rightIndex = 0;
|
||||||
|
List<String> retVal = new ArrayList<>();
|
||||||
|
while (index > -1 ) {
|
||||||
|
String left = thePath.substring(rightIndex, index);
|
||||||
|
if (allParensHaveBeenClosed(left)) {
|
||||||
|
retVal.add(left);
|
||||||
|
rightIndex = index + tokenLength;
|
||||||
|
}
|
||||||
|
index = thePath.indexOf(theToken, index + tokenLength);
|
||||||
|
}
|
||||||
|
retVal.add(thePath.substring(rightIndex));
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allParensHaveBeenClosed(String thePaths) {
|
||||||
|
int open = StringUtils.countMatches(thePaths, "(");
|
||||||
|
int close = StringUtils.countMatches(thePaths, ")");
|
||||||
|
return open == close;
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal normalizeQuantityContainingTimeUnitsIntoDaysForNumberParam(String theSystem, String theCode, BigDecimal theValue) {
|
private BigDecimal normalizeQuantityContainingTimeUnitsIntoDaysForNumberParam(String theSystem, String theCode, BigDecimal theValue) {
|
||||||
if (SearchParamConstants.UCUM_NS.equals(theSystem)) {
|
if (SearchParamConstants.UCUM_NS.equals(theSystem)) {
|
||||||
if (isNotBlank(theCode)) {
|
if (isNotBlank(theCode)) {
|
||||||
|
|
|
@ -43,6 +43,9 @@ import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class SearchParamExtractorDstu3Test {
|
public class SearchParamExtractorDstu3Test {
|
||||||
|
@ -105,6 +108,24 @@ public class SearchParamExtractorDstu3Test {
|
||||||
assertEquals("2", params.iterator().next().getValue().toPlainString());
|
assertEquals("2", params.iterator().next().getValue().toPlainString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPathSplitOnSpsWorks() {
|
||||||
|
ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry();
|
||||||
|
SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, searchParamRegistry);
|
||||||
|
String threeSegmentPath = "Patient.telecom.where(system='phone' or system='email') | Patient.telecom.where(system='email') or Patient.telecom.where(system='mail' | system='phone')";
|
||||||
|
|
||||||
|
String[] expressions = extractor.split(threeSegmentPath);
|
||||||
|
assertThat(expressions.length, is(equalTo(3)));
|
||||||
|
assertThat(expressions[0], containsString("Patient.telecom.where(system='phone' or system='email')"));
|
||||||
|
assertThat(expressions[1], containsString("Patient.telecom.where(system='email')"));
|
||||||
|
assertThat(expressions[2], containsString("Patient.telecom.where(system='mail' | system='phone')"));
|
||||||
|
|
||||||
|
String zeroPathSplit = "Patient.telecom.where(system='phone' or system='email')";
|
||||||
|
String[] singularExpression = extractor.split(zeroPathSplit);
|
||||||
|
assertThat(singularExpression.length, is(equalTo(1)));
|
||||||
|
assertThat(singularExpression[0], containsString("Patient.telecom.where(system='phone' or system='email')"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncounterDuration_NotNormalized() {
|
public void testEncounterDuration_NotNormalized() {
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.util.TestUtil;
|
||||||
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
||||||
import org.hl7.fhir.dstu3.model.Base;
|
import org.hl7.fhir.dstu3.model.Base;
|
||||||
import org.hl7.fhir.dstu3.model.BooleanType;
|
import org.hl7.fhir.dstu3.model.BooleanType;
|
||||||
|
import org.hl7.fhir.dstu3.model.ContactPoint;
|
||||||
import org.hl7.fhir.dstu3.model.DateTimeType;
|
import org.hl7.fhir.dstu3.model.DateTimeType;
|
||||||
import org.hl7.fhir.dstu3.model.Observation;
|
import org.hl7.fhir.dstu3.model.Observation;
|
||||||
import org.hl7.fhir.dstu3.model.Patient;
|
import org.hl7.fhir.dstu3.model.Patient;
|
||||||
|
|
Loading…
Reference in New Issue