From 4eb099b900d151c0e32d95b2c273c4d4d864cb99 Mon Sep 17 00:00:00 2001 From: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com> Date: Wed, 25 May 2022 16:07:29 -0400 Subject: [PATCH] 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 --- .../search/ExtendedLuceneClauseBuilder.java | 17 ++ .../search/ExtendedLuceneIndexExtractor.java | 21 ++- .../search/ExtendedLuceneSearchBuilder.java | 14 +- .../model/search/ExtendedLuceneIndexData.java | 6 + .../search/HibernateSearchIndexWriter.java | 12 +- .../search/SearchParamTextPropertyBinder.java | 4 + .../fhir/jpa/searchparam/MatchUrlService.java | 2 +- ...esourceDaoR4SearchWithElasticSearchIT.java | 160 ++++++++++++++++-- .../fhir/test/utilities/ITestDataBuilder.java | 18 ++ 9 files changed, 232 insertions(+), 22 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneClauseBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneClauseBuilder.java index 62eb83b70c4..8253f2f80b2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneClauseBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneClauseBuilder.java @@ -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> theUriUnmodifiedAndOrTerms) { + for (List nextAnd : theUriUnmodifiedAndOrTerms) { + + List 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)); + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneIndexExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneIndexExtractor.java index 0bddf3a276c..ff11c0ee0fe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneIndexExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneIndexExtractor.java @@ -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()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneSearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneSearchBuilder.java index 28e31abd797..c609850f912 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneSearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneSearchBuilder.java @@ -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 ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_tag", "_meta"); + public static final Set 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> tokenUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam); builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms); - break; + case STRING: List> 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> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam); + builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms); + default: // ignore unsupported param types/modifiers. They will be processed up in SearchBuilder. } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedLuceneIndexData.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedLuceneIndexData.java index 93dfc2443ac..cab34f2be9c 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedLuceneIndexData.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedLuceneIndexData.java @@ -48,6 +48,7 @@ public class ExtendedLuceneIndexData { final SetMultimap mySearchParamStrings = HashMultimap.create(); final SetMultimap mySearchParamTokens = HashMultimap.create(); final SetMultimap mySearchParamLinks = HashMultimap.create(); + final SetMultimap mySearchParamUri = HashMultimap.create(); final SetMultimap mySearchParamDates = HashMultimap.create(); final SetMultimap 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); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/HibernateSearchIndexWriter.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/HibernateSearchIndexWriter.java index b3b86e6b586..91a344eafc8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/HibernateSearchIndexWriter.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/HibernateSearchIndexWriter.java @@ -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 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); + } + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchParamTextPropertyBinder.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchParamTextPropertyBinder.java index 6ea0fe70e70..f588c18b97b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchParamTextPropertyBinder.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchParamTextPropertyBinder.java @@ -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); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java index e459e348f83..dc3dd18f035 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java @@ -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); diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index 17e3629b76e..2b5f3d66c22 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index 2f1185d2dc9..d35070c3804 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -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 withSecurity(String theSystem, String theCode) { + return t -> t.getMeta().addSecurity().setSystem(theSystem).setCode(theCode).setDisplay(theCode); + } + + default Consumer withProfile(String theProfile) { + return t -> t.getMeta().addProfile(theProfile); + } + + default Consumer withSource(FhirContext theContext, String theSource) { + return t -> MetaUtil.setSource(theContext, t.getMeta(), theSource); + } + default IIdType createObservation(Consumer... theModifiers) { return createResource("Observation", theModifiers); } + default IIdType createObservation(Collection> theModifiers) { + return createResource("Observation", theModifiers.toArray(new Consumer[0])); + } + default IBaseResource buildPatient(Consumer... theModifiers) { return buildResource("Patient", theModifiers); }