Freetext search source parameter support (#3641)

* Handle uri, _tag, _security and _profile freetext search parameters

* Fix combined 'and' and 'or' clauses and add tests
Use collection instead of typed array to eliminate warnings

* Eliminate predicate nesting level

* Implement _source freetext search

* Implement suggestions

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2022-05-25 16:07:29 -04:00 committed by GitHub
parent fb6e5cf6ce
commit 4eb099b900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 232 additions and 22 deletions

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.param.QuantityParam;
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.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.DateUtils;
import ca.uhn.fhir.util.StringUtil;
@ -50,6 +51,7 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@ -70,6 +72,7 @@ import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_SYSTEM
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ExtendedLuceneClauseBuilder {
@ -623,4 +626,18 @@ public class ExtendedLuceneClauseBuilder {
}
public void addUriUnmodifiedSearch(String theParamName, List<List<IQueryParameterType>> theUriUnmodifiedAndOrTerms) {
for (List<IQueryParameterType> nextAnd : theUriUnmodifiedAndOrTerms) {
List<PredicateFinalStep> predicates = new ArrayList<>();
for (IQueryParameterType paramType : nextAnd) {
predicates.add(myPredicateFactory.match()
.field(String.join(".", SEARCH_PARAM_ROOT, theParamName, URI_VALUE))
.matching( ((UriParam) paramType).getValue() ));
}
myRootClause.must(orPredicateOrSingle(predicates));
}
}
}

View File

@ -22,14 +22,16 @@ package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.MetaUtil;
import com.google.common.base.Strings;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding;
@ -42,7 +44,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* Extract search params for advanced lucene indexing.
@ -91,6 +94,20 @@ public class ExtendedLuceneIndexExtractor {
theNewParams.myQuantityParams.forEach(nextParam ->
retVal.addQuantityIndexData(nextParam.getParamName(), nextParam.getUnits(), nextParam.getSystem(), nextParam.getValue().doubleValue()));
theResource.getMeta().getTag().forEach(tag ->
retVal.addTokenIndexData("_tag", new CodingDt(tag.getSystem(), tag.getCode()).setDisplay(tag.getDisplay())));
theResource.getMeta().getSecurity().forEach(sec ->
retVal.addTokenIndexData("_security", new CodingDt(sec.getSystem(), sec.getCode()).setDisplay(sec.getDisplay())));
theResource.getMeta().getProfile().forEach(prof ->
retVal.addUriIndexData("_profile", prof.getValue()));
String source = MetaUtil.getSource(myContext, theResource.getMeta());
if (isNotBlank(source)) {
retVal.addUriIndexData("_source", source);
}
if (!theNewParams.myLinks.isEmpty()) {

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.rest.param.QuantityParam;
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.UriParam;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@ -48,7 +49,7 @@ public class ExtendedLuceneSearchBuilder {
/**
* These params have complicated semantics, or are best resolved at the JPA layer for now.
*/
public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_tag", "_meta");
public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta");
/**
* Are any of the queries supported by our indexing?
@ -111,6 +112,11 @@ public class ExtendedLuceneSearchBuilder {
return true;
}
return false;
} else if (param instanceof UriParam) {
if (EMPTY_MODIFIER.equals(modifier)) {
return true;
}
return false;
} else {
return false;
}
@ -137,8 +143,8 @@ public class ExtendedLuceneSearchBuilder {
List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
break;
case STRING:
List<List<IQueryParameterType>> stringTextAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
builder.addStringTextSearch(nextParam, stringTextAndOrTerms);
@ -168,6 +174,10 @@ public class ExtendedLuceneSearchBuilder {
builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
break;
case URI:
List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
default:
// ignore unsupported param types/modifiers. They will be processed up in SearchBuilder.
}

View File

@ -48,6 +48,7 @@ public class ExtendedLuceneIndexData {
final SetMultimap<String, String> mySearchParamStrings = HashMultimap.create();
final SetMultimap<String, IBaseCoding> mySearchParamTokens = HashMultimap.create();
final SetMultimap<String, String> mySearchParamLinks = HashMultimap.create();
final SetMultimap<String, String> mySearchParamUri = HashMultimap.create();
final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
final SetMultimap<String, QuantitySearchIndexData> mySearchParamQuantities = HashMultimap.create();
private String myForcedId;
@ -94,6 +95,7 @@ public class ExtendedLuceneIndexData {
Multimaps.asMap(mySearchParamQuantities).forEach(ifNotContained(indexWriter::writeQuantityIndex));
// TODO MB Use RestSearchParameterTypeEnum to define templates.
mySearchParamDates.forEach(ifNotContained(indexWriter::writeDateIndex));
Multimaps.asMap(mySearchParamUri).forEach(ifNotContained(indexWriter::writeUriIndex));
}
public void addStringIndexData(String theSpName, String theText) {
@ -115,6 +117,10 @@ public class ExtendedLuceneIndexData {
mySearchParamTokens.put(theSpName, theNextValue);
}
public void addUriIndexData(String theSpName, String theValue) {
mySearchParamUri.put(theSpName, theValue);
}
public void addResourceLinkIndexData(String theSpName, String theTargetResourceId) {
mySearchParamLinks.put(theSpName, theTargetResourceId);
}

View File

@ -33,8 +33,6 @@ import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.Collection;
import static org.hl7.fhir.r4.model.Observation.SP_VALUE_QUANTITY;
public class HibernateSearchIndexWriter {
private static final Logger ourLog = LoggerFactory.getLogger(HibernateSearchIndexWriter.class);
public static final String IDX_STRING_NORMALIZED = "norm";
@ -50,6 +48,8 @@ public class HibernateSearchIndexWriter {
public static final String QTY_CODE_NORM = "code-norm";
public static final String QTY_VALUE_NORM = "value-norm";
public static final String URI_VALUE = "uri-value";
final HibernateSearchElementCache myNodeCache;
@ -151,5 +151,11 @@ public class HibernateSearchIndexWriter {
}
public void writeUriIndex(String theParamName, Collection<String> theUriValueCollection) {
DocumentElement uriNode = myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName);
for (String uriSearchIndexValue : theUriValueCollection) {
ourLog.trace("Adding Search Param Uri: {} -- {}", theParamName, uriSearchIndexValue);
uriNode.addValue(URI_VALUE, uriSearchIndexValue);
}
}
}

View File

@ -48,6 +48,7 @@ import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE_N
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_SYSTEM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
/**
* Allows hibernate search to index
@ -175,6 +176,9 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
// reference
spfield.fieldTemplate("reference-value", keywordFieldType).matchingPathGlob("*.reference.value").multiValued();
// uri
spfield.fieldTemplate("uriValueTemplate", keywordFieldType).matchingPathGlob("*." + URI_VALUE).multiValued();
//quantity
String quantityPathGlob = "*.quantity";
nestedSpField.objectFieldTemplate("quantityTemplate", ObjectStructure.FLATTENED).matchingPathGlob(quantityPathGlob);

View File

@ -140,7 +140,7 @@ public class MatchUrlService {
type.setValuesAsQueryTokens(myFhirContext, nextParamName, (paramList));
paramMap.add(nextParamName, type);
} else if (Constants.PARAM_SOURCE.equals(nextParamName)) {
IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList);
IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(myFhirContext, RestSearchParameterTypeEnum.URI, nextParamName, paramList);
paramMap.add(nextParamName, param);
} else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) {
paramMap.setDeleteExpunge(true);

View File

@ -68,7 +68,6 @@ import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
@ -92,6 +91,7 @@ import javax.persistence.EntityManager;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -597,8 +597,8 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
*/
@Test
void testStringCaseFolding() {
IIdType kelly = myTestDataBuilder.createPatient(myTestDataBuilder.withGiven("Kelly"));
IIdType keely = myTestDataBuilder.createPatient(myTestDataBuilder.withGiven("Kélly"));
IIdType kelly = myTestDataBuilder.createPatient(asArray(myTestDataBuilder.withGiven("Kelly")));
IIdType keely = myTestDataBuilder.createPatient(asArray(myTestDataBuilder.withGiven("Kélly")));
// un-modified, :contains, and :text are all ascii normalized, and case-folded
myTestDaoSearch.assertSearchFinds("lowercase matches capitalized", "/Patient?name=kelly", kelly, keely);
@ -915,7 +915,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Test
public void simpleTokenSkipsSql() {
IIdType id = myTestDataBuilder.createObservation(myTestDataBuilder.withObservationCode("http://example.com/", "theCode"));
IIdType id = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode")));
myCaptureQueriesListener.clear();
List<String> ids = myTestDaoSearch.searchForIds("Observation?code=theCode");
@ -929,7 +929,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Test
public void sortStillRequiresSql() {
IIdType id = myTestDataBuilder.createObservation(myTestDataBuilder.withObservationCode("http://example.com/", "theCode"));
IIdType id = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode")));
myCaptureQueriesListener.clear();
List<String> ids = myTestDaoSearch.searchForIds("Observation?code=theCode&_sort=code");
@ -944,7 +944,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Test
public void deletedResourceNotFound() {
IIdType id = myTestDataBuilder.createObservation(myTestDataBuilder.withObservationCode("http://example.com/", "theCode"));
IIdType id = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode")));
myObservationDao.delete(id);
myCaptureQueriesListener.clear();
@ -958,9 +958,9 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Test
public void forcedIdSurvivesWithNoSql() {
IIdType id = myTestDataBuilder.createObservation(
IIdType id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withId("forcedid"));
myTestDataBuilder.withId("forcedid")));
assertThat(id.getIdPart(), equalTo("forcedid"));
myCaptureQueriesListener.clear();
@ -981,9 +981,9 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
*/
@Test
public void tagsSurvive() {
IIdType id = myTestDataBuilder.createObservation(
IIdType id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withTag("http://example.com", "aTag"));
myTestDataBuilder.withTag("http://example.com", "aTag")));
myCaptureQueriesListener.clear();
List<IBaseResource> observations = myTestDaoSearch.searchForResources("Observation?code=theCode");
@ -1476,10 +1476,10 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
// myLogbackLevelOverrideExtension.setLogLevel(DaoTestDataBuilder.class, Level.DEBUG);
// this configuration must generate a combo-value-quantity entry with both quantity objects
myResourceId = myTestDataBuilder.createObservation(
myResourceId = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withQuantityAtPath("valueQuantity", 0.02, UCUM_CODESYSTEM_URL, "10*6/L"),
myTestDataBuilder.withQuantityAtPath("component.valueQuantity", 0.06, UCUM_CODESYSTEM_URL, "10*6/L")
);
));
// myLogbackLevelOverrideExtension.resetLevel(DaoTestDataBuilder.class);
@ -1532,16 +1532,148 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
private IIdType withObservationWithValueQuantity(double theValue) {
myResourceId = myTestDataBuilder.createObservation(myTestDataBuilder.withElementAt("valueQuantity",
myResourceId = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withElementAt("valueQuantity",
myTestDataBuilder.withPrimitiveAttribute("value", theValue),
myTestDataBuilder.withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
myTestDataBuilder.withPrimitiveAttribute("code", "mm[Hg]")
));
)));
return myResourceId;
}
}
@Nested
public class TagTypesSearch {
@BeforeEach
public void enableResourceStorage() {
myDaoConfig.setStoreResourceInLuceneIndex(true);
}
@AfterEach
public void resetResourceStorage() {
myDaoConfig.setStoreResourceInLuceneIndex(new DaoConfig().isStoreResourceInLuceneIndex());
}
@Test
public void tagTagSearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withTag("http://example.com", "aTag"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_tag=http://example.com|aTag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
@Test
public void tagSecuritySearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withSecurity("http://example.com", "security-label"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_security=http://example.com|security-label");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
@Test
public void tokenAndOrCombinedSearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withSecurity("http://example.com", "security-label"),
myTestDataBuilder.withSecurity("http://example.com", "other-security-label"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation" +
"?_security=http://example.com|non-existing-security-label,http://example.com|security-label" +
"&_security=http://example.com|other-non-existing-security-label,http://example.com|other-security-label");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
@Test
public void tokenAndOrCombinedSearch_failing_and_with_multiple_or() {
myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withTag("http://example.com", "aTag"),
myTestDataBuilder.withTag("http://example.com", "anotherTag"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation" +
"?_tag=http://example.com|not-existing-tag,http://example.com|one-more-not-existing-tag" +
"&_tag=http://example.com|other-not-existing-tag,http://example.com|anotherTag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, empty());
}
@Test
public void tokenAndOrCombinedSearch_failing_and_with_single_or() {
myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withTag("http://example.com", "aTag"),
myTestDataBuilder.withTag("http://example.com", "anotherTag"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation" +
"?_tag=http://example.com|not-existing-tag" +
"&_tag=http://example.com|other-not-existing-tag,http://example.com|anotherTag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, empty());
}
@Test
public void uriAndOrCombinedSearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withProfile("http://example.com/theProfile"),
myTestDataBuilder.withProfile("http://example.com/anotherProfile"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation" +
"?_profile=http://example.com/non-existing-profile,http://example.com/theProfile" +
"&_profile=http://example.com/other-non-existing-profile,http://example.com/anotherProfile");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
@Test
public void tagProfileSearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withProfile("http://example.com/theProfile"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_profile=http://example.com/theProfile");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
@Test
public void tagSourceSearch() {
String id = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCode"),
myTestDataBuilder.withSource(myFhirContext, "http://example.com/theSource"))).getIdPart();
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_source=http://example.com/theSource");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(id));
}
}
@Nested

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.MetaUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseReference;
@ -34,6 +35,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.function.Consumer;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -145,10 +147,26 @@ public interface ITestDataBuilder {
return t -> t.getMeta().addTag().setSystem(theSystem).setCode(theCode).setDisplay(theCode);
}
default Consumer<IBaseResource> withSecurity(String theSystem, String theCode) {
return t -> t.getMeta().addSecurity().setSystem(theSystem).setCode(theCode).setDisplay(theCode);
}
default Consumer<IBaseResource> withProfile(String theProfile) {
return t -> t.getMeta().addProfile(theProfile);
}
default Consumer<IBaseResource> withSource(FhirContext theContext, String theSource) {
return t -> MetaUtil.setSource(theContext, t.getMeta(), theSource);
}
default IIdType createObservation(Consumer<IBaseResource>... theModifiers) {
return createResource("Observation", theModifiers);
}
default IIdType createObservation(Collection<Consumer<IBaseResource>> theModifiers) {
return createResource("Observation", theModifiers.toArray(new Consumer[0]));
}
default IBaseResource buildPatient(Consumer<IBaseResource>... theModifiers) {
return buildResource("Patient", theModifiers);
}