From 6035f78ddf765b6a759b9d538b0a8e48f4a918c4 Mon Sep 17 00:00:00 2001 From: Diederik Muylwyk Date: Fri, 5 Mar 2021 18:46:42 -0500 Subject: [PATCH 1/6] Fix typos --- ...nce-targets-need-to-be-resolved-placeholder-extension.yaml | 2 +- .../src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2446-issues-with-placeholder-reference-targets-need-to-be-resolved-placeholder-extension.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2446-issues-with-placeholder-reference-targets-need-to-be-resolved-placeholder-extension.yaml index 849ebfd76a1..7b305d5f9a6 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2446-issues-with-placeholder-reference-targets-need-to-be-resolved-placeholder-extension.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2446-issues-with-placeholder-reference-targets-need-to-be-resolved-placeholder-extension.yaml @@ -2,4 +2,4 @@ type: add issue: 2446 title: "Auto-created placeholder reference targets now have an extension with the URL - `http://hapifhir.io/fhir/StructureDefinition/resource-meta-placeholder` and a value of `true`." + `http://hapifhir.io/fhir/StructureDefinition/resource-placeholder` and a value of `true`." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index def5d13846f..97847d2ac44 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -1070,7 +1070,7 @@ public class DaoConfig { * Note however that references containing purely numeric IDs will not be auto-created * as they are never allowed to be client supplied in HAPI FHIR JPA. * - * All placeholder resources created in this way have a meta extension + * All placeholder resources created in this way have an extension * with the URL {@link HapiExtensions#EXT_RESOURCE_PLACEHOLDER} and the value "true". *

*/ @@ -1094,7 +1094,7 @@ public class DaoConfig { * Note however that references containing purely numeric IDs will not be auto-created * as they are never allowed to be client supplied in HAPI FHIR JPA. * - * All placeholder resources created in this way have a meta extension + * All placeholder resources created in this way have an extension * with the URL {@link HapiExtensions#EXT_RESOURCE_PLACEHOLDER} and the value "true". *

*/ From 11ee6dedd3378a041067a8ecf5922ea522abeb91 Mon Sep 17 00:00:00 2001 From: Diederik Muylwyk Date: Sat, 6 Mar 2021 16:33:05 -0500 Subject: [PATCH 2/6] Address CVE-2020-27223; bumped minor version of Jetty in root POM --- .../changelog/5_4_0/2454-address-cve-2020-27223.yaml | 9 +++++++++ pom.xml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2454-address-cve-2020-27223.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2454-address-cve-2020-27223.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2454-address-cve-2020-27223.yaml new file mode 100644 index 00000000000..a67999c435f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2454-address-cve-2020-27223.yaml @@ -0,0 +1,9 @@ +--- +type: security +issue: 2454 +title: "Addressed the following CVE report by bumping the minor version for Jetty in the root POM: + " diff --git a/pom.xml b/pom.xml index e65670c96e1..8672a79a738 100644 --- a/pom.xml +++ b/pom.xml @@ -782,7 +782,7 @@ 3.16.0 3.0.0 - 9.4.35.v20201120 + 9.4.38.v20210224 3.0.2 5.7.0 6.5.4 From 62df320cebe9992f07f9106ed5f6bd563d69bd1c Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sun, 7 Mar 2021 10:16:30 -0500 Subject: [PATCH 3/6] Avoid a crash reading an unknown ID on partitioned server (#2451) * Avoid a crash reading an unknown ID on partitioned server. Fix #1953. * Attempt 2 at fixing #1953 --- ...rming-partitioned-read-for-unknown-id.yaml | 5 ++ .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 2 +- .../jpa/dao/r4/PartitioningSqlR4Test.java | 52 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1953-avoid-npe-performing-partitioned-read-for-unknown-id.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1953-avoid-npe-performing-partitioned-read-for-unknown-id.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1953-avoid-npe-performing-partitioned-read-for-unknown-id.yaml new file mode 100644 index 00000000000..14413564566 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/1953-avoid-npe-performing-partitioned-read-for-unknown-id.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 1953 +title: "A crash was fixed when performing a FHIR read on a partitioned server, where the requested ID is not known. Thanks + to Umberto Cappellini for reporting!" diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index e140bef77d6..19dae05746c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -1149,7 +1149,7 @@ public abstract class BaseHapiFhirResourceDao extends B BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong()); // Verify that the resource is for the correct partition - if (!requestPartitionId.isAllPartitions()) { + if (entity != null && !requestPartitionId.isAllPartitions()) { if (entity.getPartitionId() != null && entity.getPartitionId().getPartitionId() != null) { if (!requestPartitionId.hasPartitionId(entity.getPartitionId().getPartitionId())) { ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 50390a69aa3..66d5a610083 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -975,6 +975,58 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { } } + + @Test + public void testRead_PidId_UnknownResourceId() { + // Read in specific Partition + { + addReadPartition(1); + try { + myPatientDao.read(new IdType("Patient/1"), mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // expected + } + } + + // Read in null Partition + { + addReadDefaultPartition(); + try { + myPatientDao.read(new IdType("Patient/1"), mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // expected + } + } + } + + @Test + public void testRead_PidId_ResourceIdOnlyExistsInDifferentPartition() { + IIdType id = createPatient(withPartition(2), withActiveTrue()); + // Read in specific Partition + { + addReadPartition(1); + try { + myPatientDao.read(id, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // expected + } + } + + // Read in null Partition + { + addReadDefaultPartition(); + try { + myPatientDao.read(id, mySrd); + fail(); + } catch (ResourceNotFoundException e) { + // expected + } + } + } + @Test public void testRead_ForcedId_SpecificPartition() { IIdType patientIdNull = createPatient(withPutPartition(null), withActiveTrue(), withId("NULL")); From 6cc8a5ce4dce108b238bc85baa2f22addbb8238a Mon Sep 17 00:00:00 2001 From: Diederik Muylwyk Date: Tue, 9 Mar 2021 06:28:40 -0500 Subject: [PATCH 4/6] Fix HasParam#doGetQueryParameterQualifier() (#2460) * Fix HasParam#doGetQueryParameterQualifier(); add changelog entry * Fix bad test in SearchParameterMapTest --- .../main/java/ca/uhn/fhir/rest/param/HasParam.java | 12 ++++++------ ...arameterqualifier-returns-malformed-modifier.yaml | 6 ++++++ .../uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java | 3 +-- .../uhn/fhir/jpa/searchparam/SearchParameterMap.java | 4 ---- 4 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2458-hasparam-dogetqueryparameterqualifier-returns-malformed-modifier.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/HasParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/HasParam.java index f85cb4e1ebf..3234103f141 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/HasParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/HasParam.java @@ -1,5 +1,10 @@ package ca.uhn.fhir.rest.param; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + import static org.apache.commons.lang3.StringUtils.defaultString; /* @@ -22,11 +27,6 @@ import static org.apache.commons.lang3.StringUtils.defaultString; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; - /** * Implementation of the _has method parameter */ @@ -55,7 +55,7 @@ public class HasParam extends BaseParam implements IQueryParameterType { @Override String doGetQueryParameterQualifier() { - return myTargetResourceType + ':' + myParameterName + ':' + myParameterValue; + return ':' + myTargetResourceType + ':' + myReferenceFieldName + ':' + myParameterName; } @Override diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2458-hasparam-dogetqueryparameterqualifier-returns-malformed-modifier.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2458-hasparam-dogetqueryparameterqualifier-returns-malformed-modifier.yaml new file mode 100644 index 00000000000..3512633c2e5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2458-hasparam-dogetqueryparameterqualifier-returns-malformed-modifier.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 2458 +title: "`HasParam#doGetQueryParameterQualifier()` returned a malformed modifier. For example, the modifier for + `_has:Observation:patient:code=123` was returned as `Observation:code:123` when it should be + `:Observation:patient:code`. This has been corrected." diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java index af4c2e9bfd6..fcb37e54fe9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java @@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.param.HasParam; import ca.uhn.fhir.test.BaseTest; import org.junit.jupiter.api.Test; @@ -19,6 +18,6 @@ public class SearchParameterMapTest extends BaseTest { SearchParameterMap params = new SearchParameterMap(); params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); String criteria = params.toNormalizedQueryString(myContext); - assertEquals(criteria, "?_has:Observation:identifier:urn:system|FOO=urn%3Asystem%7CFOO"); + assertEquals(criteria, "?_has:Observation:subject:identifier=urn%3Asystem%7CFOO"); } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index 6a2dbbc3ce0..4e6a31a8063 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -413,10 +413,6 @@ public class SearchParameterMap implements Serializable { IQueryParameterType firstValue = nextValuesAnd.get(0); b.append(UrlUtil.escapeUrlParam(nextKey)); - if (nextKey.equals(Constants.PARAM_HAS)) { - b.append(':'); - } - if (firstValue.getMissing() != null) { b.append(Constants.PARAMQUALIFIER_MISSING); b.append('='); From bf8e890a0c15d0e03e76a8a7a2c82f5f04a7e287 Mon Sep 17 00:00:00 2001 From: Frank Tao <38163583+frankjtao@users.noreply.github.com> Date: Tue, 9 Mar 2021 06:50:23 -0500 Subject: [PATCH 5/6] Supported contained resource search (#2441) * POC for indexing on contained resource - test case may failed * Test contained url * Add a spt to handle contained flag * Added search option for contained resource * Impl contained resource search * fixed typo * Reworked on creating index based on the review comments * Added changelog * Added more test cases Co-authored-by: jamesagnew --- .../5_4_0/2403-support-contained-search.yaml | 4 + .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 13 + .../fhir/jpa/search/builder/QueryStack.java | 297 ++++-- .../jpa/search/builder/SearchBuilder.java | 6 +- .../ResourceLinkPredicateBuilder.java | 3 +- .../predicate/StringPredicateBuilder.java | 5 +- .../predicate/TokenPredicateBuilder.java | 13 +- .../r4/FhirResourceDaoR4ContainedTest.java | 325 +++++- .../dao/r4/SearchParamExtractorR4Test.java | 6 +- .../r4/ResourceProviderHasParamR4Test.java | 4 +- ...ResourceProviderR4SearchContainedTest.java | 978 ++++++++++++++++++ .../fhir/jpa/model/entity/ModelConfig.java | 23 + .../jpa/searchparam/SearchContainedEnum.java | 40 + .../jpa/searchparam/SearchParameterMap.java | 12 +- .../extractor/BaseSearchParamExtractor.java | 79 +- .../extractor/ISearchParamExtractor.java | 2 +- .../ResourceIndexedSearchParams.java | 25 +- .../SearchParamExtractorService.java | 87 +- 18 files changed, 1749 insertions(+), 173 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2403-support-contained-search.yaml create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java create mode 100644 hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2403-support-contained-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2403-support-contained-search.yaml new file mode 100644 index 00000000000..a12b091e98f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2403-support-contained-search.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 2403 +title: "Optionally support '_contained' resource search by enabling the indexing on the contained resources in the ModelConfig." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 19dae05746c..f4288f05d8c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -54,6 +54,7 @@ import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -1291,6 +1292,18 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) { + if (theRequest != null) { + String[] contained = theRequest.getParameters().get(Constants.PARAM_CONTAINED); + if (contained != null && contained.length > 0) { + if (contained[0].equals("true")) { + theParams.setSearchContainedMode(SearchContainedEnum.TRUE); + ourLog.info("Search on contained resources only"); + } else if (contained[0].equals("both")) { + ourLog.warn("Search on both normal resources and contained resources are not support. set to default search on normal resources"); + } + } + } + if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { for (List> nextAnds : theParams.values()) { for (List nextOrs : nextAnds) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index a84d44bf5d4..a63f8b38c40 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -52,6 +52,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.SourceParam; @@ -109,6 +110,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -286,7 +288,7 @@ public class QueryStack { return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal); } - private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, RuntimeSearchParam theParamDef, List theNextAnd, RequestPartitionId theRequestPartitionId) { + private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List theNextAnd, RequestPartitionId theRequestPartitionId) { Condition orCondidtion = null; for (IQueryParameterType next : theNextAnd) { @@ -298,11 +300,11 @@ public class QueryStack { RuntimeSearchParam left = theParamDef.getCompositeOf().get(0); IQueryParameterType leftValue = cp.getLeftValue(); - Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, left, leftValue, theRequestPartitionId); + Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, left, leftValue, theRequestPartitionId); RuntimeSearchParam right = theParamDef.getCompositeOf().get(1); IQueryParameterType rightValue = cp.getRightValue(); - Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, right, rightValue, theRequestPartitionId); + Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, right, rightValue, theRequestPartitionId); Condition andCondition = toAndPredicate(leftPredicate, rightPredicate); @@ -316,20 +318,20 @@ public class QueryStack { return orCondidtion; } - private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId) { + private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId) { switch (theParam.getParamType()) { case STRING: { - return createPredicateString(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); } case TOKEN: { - return createPredicateToken(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); } case DATE: { - return createPredicateDate(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId); + return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId); } case QUANTITY: { - return createPredicateQuantity(theSourceJoinColumn, theResourceName, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); } } @@ -357,15 +359,12 @@ public class QueryStack { return predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); } - public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestPartitionId theRequestPartitionId) { - - String paramName = theSearchParam.getName(); + public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); + PredicateBuilderCacheLookupResult predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> mySqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); @@ -440,13 +439,13 @@ public class QueryStack { } RestSearchParameterTypeEnum typeEnum = searchParam.getParamType(); if (typeEnum == RestSearchParameterTypeEnum.URI) { - return theQueryStack3.createPredicateUri(null, theResourceName, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequest, theRequestPartitionId); + return theQueryStack3.createPredicateUri(null, theResourceName, null, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequest, theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.STRING) { - return theQueryStack3.createPredicateString(null, theResourceName, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); + return theQueryStack3.createPredicateString(null, theResourceName, null, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.DATE) { - return theQueryStack3.createPredicateDate(null, theResourceName, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); + return theQueryStack3.createPredicateDate(null, theResourceName, null, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) { - return theQueryStack3.createPredicateNumber(null, theResourceName, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); + return theQueryStack3.createPredicateNumber(null, theResourceName, null, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) { SearchFilterParser.CompareOperation operation = theFilter.getOperation(); String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here @@ -455,7 +454,7 @@ public class QueryStack { ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value); return theQueryStack3.createPredicateReference(null, theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) { - return theQueryStack3.createPredicateQuantity(null, theResourceName, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); + return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) { throw new InvalidRequestException("Composite search parameters not currently supported with _filter clauses"); } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) { @@ -464,7 +463,7 @@ public class QueryStack { null, null, theFilter.getValue()); - return theQueryStack3.createPredicateToken(null, theResourceName, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId); + return theQueryStack3.createPredicateToken(null, theResourceName, null, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId); } } return null; @@ -562,7 +561,7 @@ public class QueryStack { List paths = join.createResourceLinkPaths(targetResourceType, paramReference); Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType)); Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); - Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId); + Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedEnum.FALSE); andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); } @@ -607,17 +606,16 @@ public class QueryStack { return toAndPredicate(predicates); } - public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestPartitionId theRequestPartitionId) { + public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult(); + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); + + NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> mySqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { - return join.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); + return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); } List codePredicates = new ArrayList<>(); @@ -636,7 +634,8 @@ public class QueryStack { operation = toOperation(param.getPrefix()); } - Condition predicate = join.createPredicateNumeric(theResourceName, theSearchParam.getName(), operation, value, theRequestPartitionId, nextOr); + + Condition predicate = join.createPredicateNumeric(theResourceName, paramName, operation, value, theRequestPartitionId, nextOr); codePredicates.add(predicate); } else { @@ -648,16 +647,15 @@ public class QueryStack { return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); } - public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestPartitionId theRequestPartitionId) { + public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); if (theList.get(0).getMissing() != null) { QuantityBasePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); - return join.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); + return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); } List quantityParams = theList @@ -675,18 +673,18 @@ public class QueryStack { .collect(Collectors.toList()); if (normalizedQuantityParams.size() == quantityParams.size()) { - join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> mySqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); quantityParams = normalizedQuantityParams; } } if (join == null) { - join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); } List codePredicates = new ArrayList<>(); for (QuantityParam nextOr : quantityParams) { - Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, theSearchParam.getName(), null, join, theOperation, theRequestPartitionId); + Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId); codePredicates.add(singleCode); } @@ -720,6 +718,106 @@ public class QueryStack { return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theList, theOperation, theRequestPartitionId); } + private Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, + String theResourceName, String theParamName, RuntimeSearchParam theSearchParam, + List theList, SearchFilterParser.CompareOperation theOperation, + RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + + String spnamePrefix = theParamName; + + String targetChain = null; + String targetParamName = null; + String targetQualifier = null; + String targetValue = null; + + RuntimeSearchParam targetParamDefinition = null; + + ArrayList orValues = Lists.newArrayList(); + IQueryParameterType qp = null; + + for (int orIdx = 0; orIdx < theList.size(); orIdx++) { + + IQueryParameterType nextOr = theList.get(orIdx); + + if (nextOr instanceof ReferenceParam) { + + ReferenceParam referenceParam = (ReferenceParam) nextOr; + + // 1. Find out the parameter, qualifier and the value + targetChain = referenceParam.getChain(); + targetParamName = targetChain; + targetValue = nextOr.getValueAsQueryToken(myFhirContext); + + int qualifierIndex = targetChain.indexOf(':'); + if (qualifierIndex != -1) { + targetParamName = targetChain.substring(0, qualifierIndex); + targetQualifier = targetChain.substring(qualifierIndex); + } + + // 2. find out the data type + if (targetParamDefinition == null) { + Iterator it = theSearchParam.getTargets().iterator(); + while (it.hasNext()) { + targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(it.next(), targetParamName); + if (targetParamDefinition != null) + break; + } + } + + if (targetParamDefinition == null) { + throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + "."); + } + + qp = toParameterType(targetParamDefinition); + qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue); + orValues.add(qp); + } + } + + if (targetParamDefinition == null) { + throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); + } + + // 3. create the query + Condition containedCondition = null; + + switch (targetParamDefinition.getParamType()) { + case DATE: + containedCondition = createPredicateDate(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequestPartitionId); + break; + case NUMBER: + containedCondition = createPredicateNumber(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequestPartitionId); + break; + case QUANTITY: + containedCondition = createPredicateQuantity(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequestPartitionId); + break; + case STRING: + containedCondition = createPredicateString(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequestPartitionId); + break; + case TOKEN: + containedCondition = createPredicateToken(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequestPartitionId); + break; + case COMPOSITE: + containedCondition = createPredicateComposite(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theRequestPartitionId); + break; + case URI: + containedCondition = createPredicateUri(null, theResourceName, spnamePrefix, targetParamDefinition, + orValues, theOperation, theRequest, theRequestPartitionId); + break; + default: + throw new InvalidRequestException( + "The search type:" + targetParamDefinition.getParamType() + " is not supported."); + } + + return containedCondition; + } + @Nullable public Condition createPredicateResourceId(@Nullable DbColumn theSourceJoinColumn, List> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder(); @@ -762,22 +860,21 @@ public class QueryStack { return toOrPredicate(orPredicates); } - public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestPartitionId theRequestPartitionId) { + public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult(); + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); + + StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> mySqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { - return join.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); + return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); } List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { - Condition singleCode = join.createPredicateString(nextOr, theResourceName, theSearchParam, join, theOperation); + Condition singleCode = join.createPredicateString(nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation); codePredicates.add(singleCode); } @@ -873,12 +970,9 @@ public class QueryStack { return toAndPredicate(andPredicates); } - public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestPartitionId theRequestPartitionId) { + public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { List tokens = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { @@ -900,7 +994,7 @@ public class QueryStack { throw new MethodNotAllowedException(msg); } - return createPredicateString(theSourceJoinColumn, theResourceName, theSearchParam, theList, null, theRequestPartitionId); + return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId); } tokens.add(nextOr); @@ -917,26 +1011,26 @@ public class QueryStack { if (tokens.isEmpty()) { return null; } + + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); - TokenPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult(); + TokenPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> mySqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { - return join.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); + return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); } - Condition predicate = join.createPredicateToken(tokens, theResourceName, theSearchParam, theOperation, theRequestPartitionId); + Condition predicate = join.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId); return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); } - public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, - RuntimeSearchParam theSearchParam, - List theList, - SearchFilterParser.CompareOperation theOperation, - RequestDetails theRequestDetails, - RequestPartitionId theRequestPartitionId) { + public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, + RequestPartitionId theRequestPartitionId) { - String paramName = theSearchParam.getName(); + String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); + UriPredicateBuilder join = mySqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); if (theList.get(0).getMissing() != null) { @@ -952,7 +1046,7 @@ public class QueryStack { } @Nullable - public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchContainedEnum theSearchContainedMode) { if (theAndOrParams.isEmpty()) { return null; @@ -998,7 +1092,7 @@ public class QueryStack { DateParam param = (DateParam) nextAnd.get(0); operation = toOperation(param.getPrefix()); } - andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, operation, theRequestPartitionId)); + andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId)); //andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId)); } break; @@ -1009,17 +1103,20 @@ public class QueryStack { QuantityParam param = (QuantityParam) nextAnd.get(0); operation = toOperation(param.getPrefix()); } - andPredicates.add(createPredicateQuantity(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, operation, theRequestPartitionId)); + andPredicates.add(createPredicateQuantity(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId)); } break; case REFERENCE: for (List nextAnd : theAndOrParams) { - andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId)); + if (theSearchContainedMode.equals(SearchContainedEnum.TRUE)) + andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); + else + andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId)); } break; case STRING: for (List nextAnd : theAndOrParams) { - andPredicates.add(createPredicateString(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.sw, theRequestPartitionId)); + andPredicates.add(createPredicateString(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.sw, theRequestPartitionId)); } break; case TOKEN: @@ -1027,23 +1124,23 @@ public class QueryStack { if ("Location.position".equals(nextParamDef.getPath())) { andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId)); } else { - andPredicates.add(createPredicateToken(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId)); + andPredicates.add(createPredicateToken(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId)); } } break; case NUMBER: for (List nextAnd : theAndOrParams) { - andPredicates.add(createPredicateNumber(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId)); + andPredicates.add(createPredicateNumber(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId)); } break; case COMPOSITE: for (List nextAnd : theAndOrParams) { - andPredicates.add(createPredicateComposite(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId)); + andPredicates.add(createPredicateComposite(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, theRequestPartitionId)); } break; case URI: for (List nextAnd : theAndOrParams) { - andPredicates.add(createPredicateUri(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.eq, theRequest, theRequestPartitionId)); + andPredicates.add(createPredicateUri(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.eq, theRequest, theRequestPartitionId)); } break; case HAS: @@ -1246,4 +1343,48 @@ public class QueryStack { return parameter.substring(parameter.indexOf(".") + 1); } + private IQueryParameterType toParameterType(RuntimeSearchParam theParam) { + + IQueryParameterType qp; + switch (theParam.getParamType()) { + case DATE: + qp = new DateParam(); + break; + case NUMBER: + qp = new NumberParam(); + break; + case QUANTITY: + qp = new QuantityParam(); + break; + case STRING: + qp = new StringParam(); + break; + case TOKEN: + qp = new TokenParam(); + break; + case COMPOSITE: + List compositeOf = theParam.getCompositeOf(); + if (compositeOf.size() != 2) { + throw new InternalErrorException("Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this."); + } + IQueryParameterType leftParam = toParameterType(compositeOf.get(0)); + IQueryParameterType rightParam = toParameterType(compositeOf.get(1)); + qp = new CompositeParam<>(leftParam, rightParam); + break; + case URI: + qp = new UriParam(); + break; + default: + throw new InvalidRequestException("The search type: " + theParam.getParamType() + " is not supported."); + } + return qp; + } + + public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) { + + if (isBlank(theSpnamePrefix)) + return theParamName; + + return theSpnamePrefix + "." + theParamName; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index 4f3be1fecd0..0d5a086bea8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -60,6 +60,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper; import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper; +import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; import ca.uhn.fhir.jpa.util.BaseIterator; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; @@ -91,7 +92,6 @@ import com.healthmarketscience.sqlbuilder.Condition; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.IdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -216,6 +216,8 @@ public class SearchBuilder implements ISearchBuilder { attemptCompositeUniqueSpProcessing(theQueryStack, theParams, theRequest); } + SearchContainedEnum searchContainedMode = theParams.getSearchContainedMode(); + // Handle each parameter List paramNames = new ArrayList<>(myParams.keySet()); for (String nextParamName : paramNames) { @@ -224,7 +226,7 @@ public class SearchBuilder implements ISearchBuilder { continue; } List> andOrParams = myParams.get(nextParamName); - Condition predicate = theQueryStack.searchForIdsWithAndOr(null, myResourceName, nextParamName, andOrParams, theRequest, myRequestPartitionId); + Condition predicate = theQueryStack.searchForIdsWithAndOr(null, myResourceName, nextParamName, andOrParams, theRequest, myRequestPartitionId, searchContainedMode); if (predicate != null) { theSearchSqlBuilder.addPredicate(predicate); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index 7be7feb2077..c64e43ae503 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -43,6 +43,7 @@ import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; +import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -394,7 +395,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder { List andPredicates = new ArrayList<>(); List> chainParamValues = Collections.singletonList(orValues); - andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId)); + andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId, SearchContainedEnum.FALSE)); orPredicates.add(toAndPredicate(andPredicates)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java index 7a00148a86d..88fd7cdae18 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; +import ca.uhn.fhir.jpa.search.builder.QueryStack; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.StringParam; @@ -75,11 +76,13 @@ public class StringPredicateBuilder extends BaseSearchParamPredicateBuilder { public Condition createPredicateString(IQueryParameterType theParameter, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, StringPredicateBuilder theFrom, SearchFilterParser.CompareOperation operation) { String rawSearchTerm; - String paramName = theSearchParam.getName(); + String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); + if (theParameter instanceof TokenParam) { TokenParam id = (TokenParam) theParameter; if (!id.isText()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java index c183a401547..58282b1794b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.search.builder.QueryStack; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -93,11 +94,13 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { public Condition createPredicateToken(Collection theParameters, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, RequestPartitionId theRequestPartitionId) { return createPredicateToken( theParameters, theResourceName, + theSpnamePrefix, theSearchParam, null, theRequestPartitionId); @@ -105,11 +108,15 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { public Condition createPredicateToken(Collection theParameters, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + + final List codes = new ArrayList<>(); - String paramName = theSearchParam.getName(); + + String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); SearchFilterParser.CompareOperation operation = theOperation; @@ -197,12 +204,12 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, paramName); Condition hashIdentityPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hashIdentity)); - Condition hashValuePredicate = createPredicateOrList(theResourceName, theSearchParam.getName(), sortedCodesList, false); + Condition hashValuePredicate = createPredicateOrList(theResourceName, paramName, sortedCodesList, false); predicate = toAndPredicate(hashIdentityPredicate, hashValuePredicate); } else { - predicate = createPredicateOrList(theResourceName, theSearchParam.getName(), sortedCodesList, true); + predicate = createPredicateOrList(theResourceName, paramName, sortedCodesList, true); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java index f425ae6ca7b..17f0cf30afc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java @@ -1,57 +1,310 @@ package ca.uhn.fhir.jpa.dao.r4; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Reference; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.jupiter.api.AfterAll; +import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.Address.AddressUse; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Encounter.EncounterParticipantComponent; +import org.hl7.fhir.r4.model.Encounter.EncounterStatus; +import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestIntent; +import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ContainedTest.class); - - - @Test - public void before() { - myDaoConfig.setIndexContainedResources(true); + @BeforeEach + public void before() throws Exception { + myModelConfig.setIndexOnContainedResources(true); } - - @Test - public void testIndexContained() { - Patient p = new Patient(); - p.setId("#some_patient"); - p.addName().setFamily("MYFAMILY").addGiven("MYGIVEN"); - - Observation o1 = new Observation(); - o1.getCode().setText("Some Observation"); - o1.setSubject(new Reference(p)); - IIdType oid1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); - - Observation o2 = new Observation(); - o2.getCode().setText("Some Observation"); - o2.setSubject(new Reference(p)); - IIdType oid2 = myObservationDao.create(o2, mySrd).getId().toUnqualifiedVersionless(); - Patient p2 = new Patient(); - p2.addName().setFamily("MYFAMILY").addGiven("MYGIVEN"); - IIdType pid2 = myPatientDao.create(p2, mySrd).getId().toUnqualifiedVersionless(); + @AfterEach + public void after() throws Exception { + myModelConfig.setIndexOnContainedResources(false); + } + + @Test + public void testCreateSimpleContainedResourceIndexWithGeneratedId() { + + Patient p = new Patient(); + p.addName().setFamily("Smith").addGiven("John"); - ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(o2)); + Observation obs = new Observation(); + obs.getCode().setText("Some Observation"); + obs.setSubject(new Reference(p)); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + IIdType id = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + Observation createdObs = myObservationDao.read(id); + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs)); + + runInTransaction(()->{ + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'subject.family' AND s.myResourceType = 'Observation'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + }); SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("subject", new ReferenceParam("name", "Smith")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); -// map = new SearchParameterMap(); -// map.add(Observation.SP_CODE, new TokenParam(null, "some observation").setModifier(TokenParamModifier.TEXT)); -// assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id1, id2))); - + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id))); } + + @Test + public void testCreateSimpleContainedResourceIndexUserDefinedId() { + Patient p = new Patient(); + p.setId("fooId"); + p.addName().setFamily("Smith").addGiven("John"); + + Observation obs = new Observation(); + obs.getCode().setText("Some Observation"); + obs.getContained().add(p); + obs.getSubject().setReference("#fooId"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + IIdType id = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + Observation createdObs = myObservationDao.read(id); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs)); + + runInTransaction(()->{ + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'subject.family' AND s.myResourceType = 'Observation'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + }); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("subject", new ReferenceParam("name", "Smith")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); + + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id))); + } - // TODO: make sure match URLs don't delete -} + @Test + public void testCreateMultipleContainedResourceIndex() { + + Practitioner prac1 = new Practitioner(); + prac1.setId("prac1"); + prac1.setActive(true); + prac1.setGender(AdministrativeGender.FEMALE); + prac1.addName().setFamily("Smith").addGiven("John"); + Address address = prac1.addAddress(); + address.setUse(AddressUse.WORK); + address.addLine("534 Erewhon St"); + address.setCity("PleasantVille"); + address.setState("NY"); + address.setPostalCode("12345"); + + Organization org1 = new Organization(); + org1.setId("org1"); + org1.setActive(true); + org1.setName("org name 1"); + + Organization org2 = new Organization(); + org2.setId("org2"); + org2.setActive(false); + org2.setName("org name 2"); + + Patient patient = new Patient(); + patient.getContained().add(prac1); + patient.getContained().add(org1); + patient.getContained().add(org2); + patient.addName().setFamily("Doe").addGiven("Jane"); + patient.addGeneralPractitioner().setReference("#prac1"); + patient.addGeneralPractitioner().setReference("#org1"); + patient.getManagingOrganization().setReference("#org2"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + + IIdType id = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + + Patient createdPatient = myPatientDao.read(id); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdPatient)); + + runInTransaction(()->{ + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'general-practitioner.family' AND s.myResourceType = 'Patient'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'general-practitioner.name' AND s.myResourceType = 'Patient'", Long.class) + .getSingleResult(); + assertEquals(3L, i.longValue()); + + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'organization.name' AND s.myResourceType = 'Patient'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + }); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("general-practitioner", new ReferenceParam("family", "Smith")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); + + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), containsInAnyOrder(toValues(id))); + } + + @Test + public void testCreateComplexContainedResourceIndex() { + + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + ServiceRequest serviceRequest = new ServiceRequest(); + serviceRequest.setId("serviceRequest1"); + serviceRequest.setStatus(ServiceRequestStatus.ACTIVE); + serviceRequest.setIntent(ServiceRequestIntent.ORDER); + serviceRequest.setAuthoredOnElement(new DateTimeType("2021-02-23")); + encounter.addBasedOn().setReference("#serviceRequest1"); + encounter.getContained().add(serviceRequest); + + Practitioner prac1 = new Practitioner(); + prac1.setId("prac1"); + prac1.setActive(true); + prac1.setGender(AdministrativeGender.FEMALE); + prac1.addName().setFamily("Smith").addGiven("John"); + EncounterParticipantComponent participient = encounter.addParticipant(); + participient.getIndividual().setReference("#prac1"); + encounter.getContained().add(prac1); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType id = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(id); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + + runInTransaction(()->{ + // The practitioner + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'participant.family' AND s.myResourceType = 'Encounter'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + + // The Patient + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myParamName = 'subject.family' AND s.myResourceType = 'Encounter'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + + // The Observation + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamToken s WHERE s.myParamName = 'reason-reference.code' AND s.myResourceType = 'Encounter'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamToken s WHERE s.myParamName = 'reason-reference.combo-code' AND s.myResourceType = 'Encounter'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + + // The ServiceRequest + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamDate s WHERE s.myParamName = 'based-on.authored' AND s.myResourceType = 'Encounter'", Long.class) + .getSingleResult(); + assertEquals(1L, i.longValue()); + }); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("based-on", new ReferenceParam("authored", "2021-02-23")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); + + assertThat(toUnqualifiedVersionlessIdValues(myEncounterDao.search(map)), containsInAnyOrder(toValues(id))); + } + + @Test + public void testSearchWithNotSupportedSearchType() { + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("subject", new ReferenceParam("near", "toronto")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); + + try { + IBundleProvider outcome = myObservationDao.search(map); + outcome.getResources(0, 1).get(0); + fail(); + } catch (InvalidRequestException e) { + assertEquals(e.getMessage(), "The search type: SPECIAL is not supported."); + } + + } + + @Test + public void testSearchWithNotSupportedSearchParameter() { + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add("subject", new ReferenceParam("marital-status", "M")); + map.setSearchContainedMode(SearchContainedEnum.TRUE); + + try { + IBundleProvider outcome = myObservationDao.search(map); + outcome.getResources(0, 1).get(0); + fail(); + } catch (InvalidRequestException e) { + assertEquals(e.getMessage(), "Unknown search parameter name: subject.marital-status."); + } + + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index fa5c7a5c567..926a58716dd 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -284,7 +284,7 @@ public class SearchParamExtractorR4Test { SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Encounter", "location"); assertNotNull(param); - ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(enc); + ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(enc, false); assertEquals(1, links.size()); assertEquals("location", links.iterator().next().getSearchParamName()); assertEquals("Encounter.location.location", links.iterator().next().getPath()); @@ -299,7 +299,7 @@ public class SearchParamExtractorR4Test { SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Consent", Consent.SP_SOURCE_REFERENCE); assertNotNull(param); - ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(consent); + ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(consent, false); assertEquals(1, links.size()); assertEquals("Consent.source", links.iterator().next().getPath()); assertEquals("Consent/999", ((Reference) links.iterator().next().getRef()).getReference()); @@ -334,7 +334,7 @@ public class SearchParamExtractorR4Test { patient.addExtension("http://patext", new Reference("Organization/AAA")); SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, mySearchParamRegistry); - ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(patient); + ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(patient, false); assertEquals(1, links.size()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java index 30d6fb3d93c..36e6ad17a72 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java @@ -108,12 +108,14 @@ public class ResourceProviderHasParamR4Test extends BaseResourceProviderR4Test { obs.addIdentifier().setSystem("urn:system").setValue("NOLINK"); obs.setDevice(new Reference(devId)); myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } + String uri = ourServerBase + "/Patient?_has:Observation:subject:identifier=" + UrlUtil.escapeUrlParam("urn:system|FOO"); List ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); assertThat(ids, contains(pid0.getValue())); - } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java new file mode 100644 index 00000000000..217a97b4a34 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java @@ -0,0 +1,978 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.CarePlan.CarePlanIntent; +import org.hl7.fhir.r4.model.CarePlan.CarePlanStatus; +import org.hl7.fhir.r4.model.ClinicalImpression; +import org.hl7.fhir.r4.model.ClinicalImpression.ClinicalImpressionStatus; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Encounter.EncounterStatus; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.RiskAssessment; +import org.hl7.fhir.r4.model.RiskAssessment.RiskAssessmentStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; +import ca.uhn.fhir.util.UrlUtil; + + +public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4SearchContainedTest.class); + private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor(); + + @Autowired + @Qualifier("myClinicalImpressionDaoR4") + protected IFhirResourceDao myClinicalImpressionDao; + + @Override + @AfterEach + public void after() throws Exception { + super.after(); + + myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); + myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); + myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); + myDaoConfig.setCountSearchResultsUpTo(new DaoConfig().getCountSearchResultsUpTo()); + myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); + myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); + myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); + + myClient.unregisterInterceptor(myCapturingInterceptor); + myModelConfig.setIndexOnContainedResources(false); + } + + @BeforeEach + @Override + public void before() throws Exception { + super.before(); + myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); + + myDaoConfig.setAllowMultipleDelete(true); + myClient.registerInterceptor(myCapturingInterceptor); + myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); + myModelConfig.setIndexOnContainedResources(true); + } + + @BeforeEach + public void beforeDisableResultReuse() { + myDaoConfig.setReuseCachedSearchResultsForMillis(null); + } + + + @Test + public void testContainedSearchByName() throws Exception { + + IIdType oid1; + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Doe").addGiven("Jane"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Jones").addGiven("Peter"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + + //-- Simple name match + String uri = ourServerBase + "/Observation?subject.name=Smith&_contained=true"; + List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Simple name match with or + uri = ourServerBase + "/Observation?subject.name=Smith,Jane&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(2L, oids.size()); + //assertEquals(oids.toString(), "[Observation/1, Observation/2]"); + + //-- Simple name match with qualifier + uri = ourServerBase + "/Observation?subject.name:exact=Smith&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Simple name match with and + uri = ourServerBase + "/Observation?subject.family=Smith&subject.given=John&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Simple name match with both, default to normal search, found 0 + uri = ourServerBase + "/Observation?subject.name=Smith&_contained=both"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(0L, oids.size()); + + } + + @Test + public void testContainedSearchByDate() throws Exception { + + IIdType oid1; + IIdType oid3; + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Doe").addGiven("Jane"); + p.getBirthDateElement().setValueAsString("2000-02-01"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Jones").addGiven("Peter"); + p.getBirthDateElement().setValueAsString("2000-03-01"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + oid3 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + //-- Search by date default op + String uri = ourServerBase + "/Observation?subject.birthdate=2000-01-01&_contained=true"; + List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Search by date op=eq + uri = ourServerBase + "/Observation?subject.birthdate=eq2000-01-01&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Search by date op=eq, with or + uri = ourServerBase + "/Observation?subject.birthdate=2000-01-01,2000-02-01&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(2L, oids.size()); + //assertEquals(oids.toString(), "[Observation/1, Observation/2]"); + + //-- Simple name match with op = gt + uri = ourServerBase + "/Observation?subject.birthdate=gt2000-02-10&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid3.getValue())); + + //-- Simple name match with AND + uri = ourServerBase + "/Observation?subject.family=Smith&subject.birthdate=eq2000-01-01&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Simple name match with AND - not found + uri = ourServerBase + "/Observation?subject.family=Smith&subject.birthdate=eq2000-02-01&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(0L, oids.size()); + } + + + @Test + public void testContainedSearchByNumber() throws Exception { + + IIdType cid1; + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + + RiskAssessment risk = new RiskAssessment(); + risk.setId("risk1"); + risk.setStatus(RiskAssessmentStatus.CORRECTED); + risk.getSubject().setReference("#patient1"); + risk.getPredictionFirstRep().setProbability(new DecimalType(2)); + + ClinicalImpression imp = new ClinicalImpression(); + imp.setStatus(ClinicalImpressionStatus.COMPLETED); + + imp.getContained().add(p); + imp.getSubject().setReference("#patient1"); + + imp.getContained().add(risk); + imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); + + cid1 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); + + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + + RiskAssessment risk = new RiskAssessment(); + risk.setId("risk1"); + risk.setStatus(RiskAssessmentStatus.CORRECTED); + risk.getSubject().setReference("#patient1"); + risk.getPredictionFirstRep().setProbability(new DecimalType(5)); + + ClinicalImpression imp = new ClinicalImpression(); + imp.setStatus(ClinicalImpressionStatus.COMPLETED); + + imp.getContained().add(p); + imp.getSubject().setReference("#patient1"); + + imp.getContained().add(risk); + imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); + + IIdType cid2 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); + + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid2); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + + RiskAssessment risk = new RiskAssessment(); + risk.setId("risk1"); + risk.setStatus(RiskAssessmentStatus.CORRECTED); + risk.getSubject().setReference("#patient1"); + risk.getPredictionFirstRep().setProbability(new DecimalType(10)); + + ClinicalImpression imp = new ClinicalImpression(); + imp.setStatus(ClinicalImpressionStatus.COMPLETED); + + imp.getContained().add(p); + imp.getSubject().setReference("#patient1"); + + imp.getContained().add(risk); + imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); + + IIdType cid3 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); + + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid3); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); + } + + //-- Search by number + String uri = ourServerBase + "/ClinicalImpression?investigation.probability=2&_contained=true"; + List cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, cids.size()); + assertThat(cids, contains(cid1.getValue())); + + + //-- Search by number with op = eq + uri = ourServerBase + "/ClinicalImpression?investigation.probability=eq2&_contained=true"; + cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, cids.size()); + assertThat(cids, contains(cid1.getValue())); + + + //-- Search by number with op = eq and or + uri = ourServerBase + "/ClinicalImpression?investigation.probability=eq2,10&_contained=true"; + cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(2L, cids.size()); + + //-- Search by number with op = lt + uri = ourServerBase + "/ClinicalImpression?investigation.probability=lt4&_contained=true"; + cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, cids.size()); + assertThat(cids, contains(cid1.getValue())); + } + + @Test + public void testContainedSearchByQuantity() throws Exception { + + IIdType eid1; + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(200); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(300); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid2); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(400); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid3); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + //-- Search by quantity + String uri = ourServerBase + "/Encounter?reason-reference.combo-value-quantity=200&_contained=true"; + List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, eids.size()); + assertThat(eids, contains(eid1.getValue())); + + + //-- Search by quantity + uri = ourServerBase + "/Encounter?reason-reference.combo-value-quantity=le400&_contained=true"; + eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(3L, eids.size()); + + } + + @Test + public void testContainedSearchByToken() throws Exception { + + IIdType eid1; + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(200); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-8").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(300); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid2); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-9").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(400); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid3); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + //-- Search by code + String uri = ourServerBase + "/Encounter?reason-reference.code=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7") + "&_contained=true"; + List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, eids.size()); + assertThat(eids, contains(eid1.getValue())); + + } + + @Test + public void testContainedSearchByComposite() throws Exception { + + IIdType eid2; + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(200); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-8").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(300); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid2); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + { + Encounter encounter = new Encounter(); + encounter.setStatus(EncounterStatus.ARRIVED); + + Patient patient = new Patient(); + patient.setId("patient1"); + patient.addName().setFamily("Doe").addGiven("Jane"); + encounter.getSubject().setReference("#patient1"); + encounter.getContained().add(patient); + + Observation obs = new Observation(); + obs.setId("obs1"); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReference("#patient1"); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-9").setSystem("http://loinc.org"); + Quantity quantity = obs.getValueQuantity(); + quantity.setValue(400); + encounter.addReasonReference().setReference("#obs1"); + encounter.getContained().add(obs); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); + + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + + Encounter createdEncounter = myEncounterDao.read(eid3); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); + } + + //-- Search by composite + String uri = ourServerBase + "/Encounter?reason-reference.combo-code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-8$300") + "&_contained=true"; + List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, eids.size()); + assertThat(eids, contains(eid2.getValue())); + + //-- Search by composite - not found + uri = ourServerBase + "/Encounter?reason-reference.combo-code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$300") + "&_contained=true"; + eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(0L, eids.size()); + + } + + + @Test + public void testContainedSearchByUri() throws Exception { + + IIdType oid1; + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + CarePlan carePlan = new CarePlan(); + carePlan.setId("carePlan1"); + carePlan.setStatus(CarePlanStatus.ACTIVE); + carePlan.setIntent(CarePlanIntent.ORDER); + carePlan.getSubject().setReference("#patient1"); + carePlan.addInstantiatesUri("http://www.hl7.com"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + obs.getContained().add(carePlan); + obs.getBasedOnFirstRep().setReference("#carePlan1"); + + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + Observation createdObs = myObservationDao.read(oid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs)); + } + + { + Patient p = new Patient(); + p.setId("patient2"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + CarePlan carePlan = new CarePlan(); + carePlan.setId("carePlan2"); + carePlan.setStatus(CarePlanStatus.ACTIVE); + carePlan.setIntent(CarePlanIntent.ORDER); + carePlan.getSubject().setReference("#patient2"); + carePlan.addInstantiatesUri("http://www2.hl7.com"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient2"); + obs.getContained().add(carePlan); + obs.getBasedOnFirstRep().setReference("#carePlan2"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient3"); + p.addName().setFamily("Smith").addGiven("John"); + p.getBirthDateElement().setValueAsString("2000-01-01"); + + CarePlan carePlan = new CarePlan(); + carePlan.setId("carePlan3"); + carePlan.setStatus(CarePlanStatus.ACTIVE); + carePlan.setIntent(CarePlanIntent.ORDER); + carePlan.getSubject().setReference("#patient3"); + carePlan.addInstantiatesUri("http://www2.hl7.com"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 3"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient3"); + obs.getContained().add(carePlan); + obs.getBasedOnFirstRep().setReference("#carePlan3"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + //-- Search by uri + String uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www.hl7.com&_contained=true"; + List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + //-- Search by uri more than 1 results + uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www2.hl7.com&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(2L, oids.size()); + + //-- Search by uri with 'or' + uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www.hl7.com,http://www2.hl7.com&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(3L, oids.size()); + + } + + @Test + public void testUpdateContainedResource() throws Exception { + + IIdType oid1; + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Smith").addGiven("John"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + Observation createdObs = myObservationDao.read(oid1); + + //-- changed the last name to Doe + List containedResources = createdObs.getContained(); + + for (Resource res : containedResources) { + if (res instanceof Patient) { + Patient p1 = (Patient)res; + HumanName name = p1.getNameFirstRep(); + name.setFamily("Doe"); + break; + } + } + + // -- update + oid1 = myObservationDao.update(createdObs, mySrd).getId().toUnqualifiedVersionless(); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Doe").addGiven("Jane"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Patient p = new Patient(); + p.setId("patient1"); + p.addName().setFamily("Jones").addGiven("Peter"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 2"); + obs.getContained().add(p); + obs.getSubject().setReference("#patient1"); + + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + + //-- No Obs with Patient Smith + String uri = ourServerBase + "/Observation?subject.family=Smith&_contained=true"; + List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(0L, oids.size()); + + //-- Two Obs with Patient Doe + uri = ourServerBase + "/Observation?subject.family=Doe&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(2L, oids.size()); + } + + + @Test + public void testDeleteContainedResource() throws Exception { + + IIdType oid1; + + { + Patient p1 = new Patient(); + p1.setId("patient1"); + p1.addName().setFamily("Smith").addGiven("John"); + + Patient p2 = new Patient(); + p2.setId("patient2"); + p2.addName().setFamily("Doe").addGiven("Jane"); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getContained().add(p1); + obs.getSubject().setReference("#patient1"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // -- remove contained resource + obs.getContained().remove(p1); + // -- add new contained resource + obs.getContained().add(p2); + obs.getSubject().setReference("#patient2"); + + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + // -- update + oid1 = myObservationDao.update(obs, mySrd).getId().toUnqualifiedVersionless(); + + Observation updatedObs = myObservationDao.read(oid1); + + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(updatedObs)); + } + + //-- No Obs with Patient Smith + String uri = ourServerBase + "/Observation?subject.family=Smith&_contained=true"; + List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(0L, oids.size()); + + //-- 1 Obs with Patient Doe + uri = ourServerBase + "/Observation?subject.family=Doe&_contained=true"; + oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getValue())); + + } + private List searchAndReturnUnqualifiedVersionlessIdValues(String uri) throws IOException { + List ids; + HttpGet get = new HttpGet(uri); + + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp); + ids = toUnqualifiedVersionlessIdValues(bundle); + } + return ids; + } + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 43f98bdc209..4e06fd1d4f5 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -98,6 +98,8 @@ public class ModelConfig { private Set myAutoVersionReferenceAtPaths = Collections.emptySet(); private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private boolean myRespectVersionsForSearchIncludes; + + private boolean myIndexOnContainedResources = false; /** * Constructor @@ -730,6 +732,27 @@ public class ModelConfig { myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes; } + + /** + * Should indexed on the contained resources, it could be searched by _contained=true + * This may have performance impacts + * + * @since 5.4.0 + */ + public boolean isIndexOnContainedResources() { + return myIndexOnContainedResources; + } + + /** + * Should indexed on the contained resources, it could be searched by _contained=true + * This may have performance impacts + * + * @since 5.4.0 + */ + public void setIndexOnContainedResources(boolean theIndexOnContainedResources) { + myIndexOnContainedResources = theIndexOnContainedResources; + } + private static void validateTreatBaseUrlsAsLocal(String theUrl) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java new file mode 100644 index 00000000000..1755b09b85d --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.jpa.searchparam; + +/* + * #%L + * HAPI FHIR Search Parameters + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public enum SearchContainedEnum { + + /** + * default, search on the non-contained (normal) resources + */ + FALSE, + + /** + * search on the contained resources only + */ + TRUE, + + /** + * Search on the normal resources and contained resources. + * This option is not supported yet. + */ + BOTH, +} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index 4e6a31a8063..e6434a8cf5e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -79,7 +79,8 @@ public class SearchParameterMap implements Serializable { private boolean myLastN; private Integer myLastNMax; private boolean myDeleteExpunge; - + private SearchContainedEnum mySearchContainedMode = SearchContainedEnum.FALSE; + /** * Constructor */ @@ -734,4 +735,13 @@ public class SearchParameterMap implements Serializable { return retVal; } + public SearchContainedEnum getSearchContainedMode() { + return mySearchContainedMode; + } + + public void setSearchContainedMode(SearchContainedEnum theSearchContainedMode) { + this.mySearchContainedMode = theSearchContainedMode; + } + + } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index 2ef20a396b2..20319c80c74 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -171,9 +171,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractResourceLinks(IBaseResource theResource) { + public SearchParamSet extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) { IExtractor extractor = createReferenceExtractor(); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE, theWantLocalReferences); } private IExtractor createReferenceExtractor() { @@ -231,7 +231,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private List extractReferenceParamsAsQueryTokens(RuntimeSearchParam theSearchParam, IBaseResource theResource, IExtractor theExtractor) { SearchParamSet params = new SearchParamSet<>(); - extractSearchParam(theSearchParam, theResource, theExtractor, params); + extractSearchParam(theSearchParam, theResource, theExtractor, params, false); return refsToStringList(params); } @@ -244,7 +244,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private List extractParamsAsQueryTokens(RuntimeSearchParam theSearchParam, IBaseResource theResource, IExtractor theExtractor) { SearchParamSet params = new SearchParamSet<>(); - extractSearchParam(theSearchParam, theResource, theExtractor, params); + extractSearchParam(theSearchParam, theResource, theExtractor, params, false); return toStringList(params); } @@ -257,14 +257,14 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public SearchParamSet extractSearchParamTokens(IBaseResource theResource) { IExtractor extractor = createTokenExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false); } @Override public SearchParamSet extractSearchParamTokens(IBaseResource theResource, RuntimeSearchParam theSearchParam) { IExtractor extractor = createTokenExtractor(theResource); SearchParamSet setToPopulate = new SearchParamSet<>(); - extractSearchParam(theSearchParam, theResource, extractor, setToPopulate); + extractSearchParam(theSearchParam, theResource, extractor, setToPopulate, false); return setToPopulate; } @@ -293,11 +293,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor public SearchParamSet extractSearchParamSpecial(IBaseResource theResource) { String resourceTypeName = toRootTypeName(theResource); IExtractor extractor = createSpecialExtractor(resourceTypeName); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false); } private IExtractor createSpecialExtractor(String theResourceTypeName) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { if (COORDS_INDEX_PATHS.contains(path)) { addCoords_Position(theResourceTypeName, params, searchParam, value); } @@ -311,11 +311,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public SearchParamSet extractSearchParamUri(IBaseResource theResource) { IExtractor extractor = createUriExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI, false); } private IExtractor createUriExtractor(IBaseResource theResource) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { String nextType = toRootTypeName(value); String resourceType = toRootTypeName(theResource); switch (nextType) { @@ -336,7 +336,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public SearchParamSet extractSearchParamDates(IBaseResource theResource) { IExtractor extractor = createDateExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.DATE); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.DATE, false); } private IExtractor createDateExtractor(IBaseResource theResource) { @@ -346,17 +346,17 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public Date extractDateFromResource(IBase theValue, String thePath) { DateExtractor extractor = new DateExtractor("DateType"); - return extractor.get(theValue, thePath).getValueHigh(); + return extractor.get(theValue, thePath, false).getValueHigh(); } @Override public SearchParamSet extractSearchParamNumber(IBaseResource theResource) { IExtractor extractor = createNumberExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false); } private IExtractor createNumberExtractor(IBaseResource theResource) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { String nextType = toRootTypeName(value); String resourceType = toRootTypeName(theResource); switch (nextType) { @@ -384,18 +384,18 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public SearchParamSet extractSearchParamQuantity(IBaseResource theResource) { IExtractor extractor = createQuantityExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); } @Override public SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { IExtractor extractor = createQuantityNormalizedExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); } private IExtractor createQuantityExtractor(IBaseResource theResource) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) { return; } @@ -421,7 +421,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private IExtractor createQuantityNormalizedExtractor(IBaseResource theResource) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) { return; } @@ -449,11 +449,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor public SearchParamSet extractSearchParamStrings(IBaseResource theResource) { IExtractor extractor = createStringExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING, false); } private IExtractor createStringExtractor(IBaseResource theResource) { - return (params, searchParam, value, path) -> { + return (params, searchParam, value, path, theWantLocalReferences) -> { String resourceType = toRootTypeName(theResource); if (value instanceof IPrimitiveType) { @@ -934,7 +934,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } - private SearchParamSet extractSearchParams(IBaseResource theResource, IExtractor theExtractor, RestSearchParameterTypeEnum theSearchParamType) { + private SearchParamSet extractSearchParams(IBaseResource theResource, IExtractor theExtractor, RestSearchParameterTypeEnum theSearchParamType, boolean theWantLocalReferences) { SearchParamSet retVal = new SearchParamSet<>(); Collection searchParams = getSearchParams(theResource); @@ -943,12 +943,12 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor continue; } - extractSearchParam(nextSpDef, theResource, theExtractor, retVal); + extractSearchParam(nextSpDef, theResource, theExtractor, retVal, theWantLocalReferences); } return retVal; } - private void extractSearchParam(RuntimeSearchParam theSearchParameterDef, IBaseResource theResource, IExtractor theExtractor, SearchParamSet theSetToPopulate) { + private void extractSearchParam(RuntimeSearchParam theSearchParameterDef, IBaseResource theResource, IExtractor theExtractor, SearchParamSet theSetToPopulate, boolean theWantLocalReferences) { String nextPathUnsplit = theSearchParameterDef.getPath(); if (isBlank(nextPathUnsplit)) { return; @@ -961,7 +961,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor if (nextObject != null) { String typeName = toRootTypeName(nextObject); if (!myIgnoredForSearchDatatypes.contains(typeName)) { - theExtractor.extract(theSetToPopulate, theSearchParameterDef, nextObject, nextPath); + theExtractor.extract(theSetToPopulate, theSearchParameterDef, nextObject, nextPath, theWantLocalReferences); } } } @@ -1181,7 +1181,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @FunctionalInterface private interface IExtractor { - void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath); + void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences); } @@ -1196,9 +1196,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath) { - myExtractor0.extract(theParams, theSearchParam, theValue, thePath); - myExtractor1.extract(theParams, theSearchParam, theValue, thePath); + public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) { + myExtractor0.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences); + myExtractor1.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences); } } @@ -1207,7 +1207,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private PathAndRef myPathAndRef = null; @Override - public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath) { + public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) { if (theValue instanceof IBaseResource) { return; } @@ -1257,10 +1257,13 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } if (nextId == null || - nextId.isEmpty() || - nextId.getValue().startsWith("#") || - nextId.getValue().startsWith("urn:")) { - return; + nextId.isEmpty() || + nextId.getValue().startsWith("urn:")) { + return; + } + if (!theWantLocalReferences) { + if (nextId.getValue().startsWith("#")) + return; } myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, valueRef, false); @@ -1275,7 +1278,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor public PathAndRef get(IBase theValue, String thePath) { extract(new SearchParamSet<>(), new RuntimeSearchParam(null, null, "Reference", null, null, null, null, null, null, null), - theValue, thePath); + theValue, thePath, false); return myPathAndRef; } } @@ -1294,7 +1297,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath) { + public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) { String nextType = toRootTypeName(theValue); switch (nextType) { case "date": @@ -1389,10 +1392,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } } - public ResourceIndexedSearchParamDate get(IBase theValue, String thePath) { + public ResourceIndexedSearchParamDate get(IBase theValue, String thePath, boolean theWantLocalReferences) { extract(new SearchParamSet<>(), new RuntimeSearchParam(null, null, "date", null, null, null, null, null, null, null), - theValue, thePath); + theValue, thePath, theWantLocalReferences); return myIndexedSearchParamDate; } } @@ -1407,7 +1410,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public void extract(SearchParamSet params, RuntimeSearchParam searchParam, IBase value, String path) { + public void extract(SearchParamSet params, RuntimeSearchParam searchParam, IBase value, String path, boolean theWantLocalReferences) { // DSTU3+ if (value instanceof IBaseEnumeration) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java index 891e2dd9202..2871503d11e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java @@ -61,7 +61,7 @@ public interface ISearchParamExtractor { SearchParamSet extractSearchParamUri(IBaseResource theResource); - SearchParamSet extractResourceLinks(IBaseResource theResource); + SearchParamSet extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences); String[] split(String theExpression); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java index 656a3cbb50a..bfaaa306b59 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java @@ -42,9 +42,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.ReferenceParam; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.google.common.collect.Streams; + import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; @@ -57,7 +55,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Stream; +import javax.annotation.Nonnull; import static org.apache.commons.lang3.StringUtils.compare; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -144,6 +142,17 @@ public final class ResourceIndexedSearchParams { theEntity.setResourceLinks(myLinks); } + public void updateSpnamePrefixForIndexedOnContainedResource(String theSpnamePrefix) { + updateSpnamePrefixForIndexedOnContainedResource(myNumberParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myQuantityParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myQuantityNormalizedParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myDateParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myUriParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myTokenParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myStringParams, theSpnamePrefix); + updateSpnamePrefixForIndexedOnContainedResource(myCoordsParams, theSpnamePrefix); + } + void setUpdatedTime(Date theUpdateTime) { setUpdatedTime(myStringParams, theUpdateTime); setUpdatedTime(myNumberParams, theUpdateTime); @@ -161,6 +170,14 @@ public final class ResourceIndexedSearchParams { } } + private void updateSpnamePrefixForIndexedOnContainedResource(Collection theParams, @Nonnull String theSpnamePrefix) { + + for (BaseResourceIndexedSearchParam param : theParams) { + param.setParamName(theSpnamePrefix + "." + param.getParamName()); + param.calculateHashes(); // re-calculuteHashes + } + } + public Set getPopulatedResourceLinkParameters() { return myPopulatedResourceLinkParameters; } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index a3247d7e74b..cd0da9978b9 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -52,6 +52,10 @@ import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ResourceReferenceInfo; +import ca.uhn.fhir.util.StringUtil; + import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseReference; @@ -96,14 +100,87 @@ public class SearchParamExtractorService { IBaseResource resource = normalizeResource(theResource); // All search parameter types except Reference - extractSearchIndexParameters(theRequestDetails, theParams, resource, theEntity); + ResourceIndexedSearchParams normalParams = new ResourceIndexedSearchParams(); + extractSearchIndexParameters(theRequestDetails, normalParams, resource, theEntity); + mergeParams(normalParams, theParams); + if (myModelConfig.isIndexOnContainedResources()) { + ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams(); + extractSearchIndexParametersForContainedResources(theRequestDetails, containedParams, resource, theEntity); + mergeParams(containedParams, theParams); + } + + // Do this after, because we add to strings during both string and token processing, and contained resource if any + populateResourceTables(theParams, theEntity); + // Reference search parameters extractResourceLinks(theRequestPartitionId, theParams, theEntity, resource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails); theParams.setUpdatedTime(theTransactionDetails.getTransactionDate()); } + private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) { + + FhirTerser terser = myContext.newTerser(); + + // 1. get all contained resources + Collection containedResources = terser.getAllEmbeddedResources(theResource, false); + + // 2. Find referenced search parameters + ISearchParamExtractor.SearchParamSet referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true); + + String spnamePrefix = null; + ResourceIndexedSearchParams currParams; + // 3. for each referenced search parameter, create an index + for (PathAndRef nextPathAndRef : referencedSearchParamSet) { + + // 3.1 get the search parameter name as spname prefix + spnamePrefix = nextPathAndRef.getSearchParamName(); + + if (spnamePrefix == null || nextPathAndRef.getRef() == null) + continue; + + // 3.2 find the contained resource + IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef()); + if (containedResource == null) + continue; + + currParams = new ResourceIndexedSearchParams(); + + // 3.3 create indexes for the current contained resource + extractSearchIndexParameters(theRequestDetails, currParams, containedResource, theEntity); + + // 3.4 added reference name as a prefix for the contained resource if any + // e.g. for Observation.subject contained reference + // the SP_NAME = subject.family + currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix); + + // 3.5 merge to the mainParams + // NOTE: the spname prefix is different + mergeParams(currParams, theParams); + } + } + + private IBaseResource findContainedResource(Collection resources, IBaseReference reference) { + for (IBaseResource resource : resources) { + if (resource.getIdElement().equals(reference.getReferenceElement())) + return resource; + } + return null; + } + + private void mergeParams(ResourceIndexedSearchParams theSrcParams, ResourceIndexedSearchParams theTargetParams) { + + theTargetParams.myNumberParams.addAll(theSrcParams.myNumberParams); + theTargetParams.myQuantityParams.addAll(theSrcParams.myQuantityParams); + theTargetParams.myQuantityNormalizedParams.addAll(theSrcParams.myQuantityNormalizedParams); + theTargetParams.myDateParams.addAll(theSrcParams.myDateParams); + theTargetParams.myUriParams.addAll(theSrcParams.myUriParams); + theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams); + theTargetParams.myStringParams.addAll(theSrcParams.myStringParams); + theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams); + } + private void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) { // Strings @@ -156,7 +233,10 @@ public class SearchParamExtractorService { } } - // Do this after, because we add to strings during both string and token processing + } + + private void populateResourceTables(ResourceIndexedSearchParams theParams, ResourceTable theEntity) { + populateResourceTable(theParams.myNumberParams, theEntity); populateResourceTable(theParams.myQuantityParams, theEntity); populateResourceTable(theParams.myQuantityNormalizedParams, theEntity); @@ -165,7 +245,6 @@ public class SearchParamExtractorService { populateResourceTable(theParams.myTokenParams, theEntity); populateResourceTable(theParams.myStringParams, theEntity); populateResourceTable(theParams.myCoordsParams, theEntity); - } /** @@ -186,7 +265,7 @@ public class SearchParamExtractorService { private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) { String resourceName = myContext.getResourceType(theResource); - ISearchParamExtractor.SearchParamSet refs = mySearchParamExtractor.extractResourceLinks(theResource); + ISearchParamExtractor.SearchParamSet refs = mySearchParamExtractor.extractResourceLinks(theResource, false); SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs); for (PathAndRef nextPathAndRef : refs) { From be50a46d7639b77aab4a1de27be59e550b4ea811 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 9 Mar 2021 11:51:30 -0500 Subject: [PATCH 6/6] Tweaks to contained searches (#2461) * Tweaks to contained searches * Add changelog * Build fix --- .../java/ca/uhn/fhir/rest/api/Constants.java | 1 + .../rest/api/SearchContainedModeEnum.java | 76 +++ .../ca/uhn/fhir/jpa/demo/CommonConfig.java | 4 - .../uhn/fhir/jpa/demo/FhirServerConfig.java | 4 - .../5_4_0/support-contained-searches.yaml | 6 + .../ca/uhn/fhir/jpa/api/config/DaoConfig.java | 94 ---- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 17 +- .../fhir/jpa/search/builder/QueryStack.java | 8 +- .../jpa/search/builder/SearchBuilder.java | 4 +- .../ResourceLinkPredicateBuilder.java | 4 +- .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 3 - .../fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java | 3 - .../FhirResourceDaoDstu3ContainedTest.java | 22 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 6 - .../r4/FhirResourceDaoR4ContainedTest.java | 17 +- ...urceDaoR4SearchWithLuceneDisabledTest.java | 11 +- .../ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java | 1 - .../provider/ResourceProviderDstu2Test.java | 3 - ...ResourceProviderR4SearchContainedTest.java | 502 +++++++++--------- .../RestHookWithEventDefinitionR4Test.java | 17 +- .../WebsocketWithSubscriptionIdDstu3Test.java | 12 +- .../fhir/jpa/model/entity/ModelConfig.java | 10 +- .../jpa/searchparam/SearchContainedEnum.java | 40 -- .../jpa/searchparam/SearchParameterMap.java | 13 +- .../uhn/fhirtest/config/TestDstu2Config.java | 1 + .../uhn/fhirtest/config/TestDstu3Config.java | 1 + .../ca/uhn/fhirtest/config/TestR4Config.java | 1 + .../ca/uhn/fhirtest/config/TestR5Config.java | 1 + .../server/interceptor/auth/RuleBuilder.java | 5 +- .../fhir/rest/server/method/MethodUtil.java | 2 + .../method/SearchContainedModeParameter.java | 34 ++ .../resources/vm/jpa_resource_provider.vm | 6 +- 32 files changed, 448 insertions(+), 481 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchContainedModeEnum.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml delete mode 100644 hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchContainedModeParameter.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 1961ded8c12..93d957299d2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -267,6 +267,7 @@ public class Constants { */ public static final String RESOURCE_PARTITION_ID = Constants.class.getName() + "_RESOURCE_PARTITION_ID"; public static final String CT_APPLICATION_GZIP = "application/gzip"; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; static { CHARSET_UTF8 = StandardCharsets.UTF_8; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchContainedModeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchContainedModeEnum.java new file mode 100644 index 00000000000..526726f352a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchContainedModeEnum.java @@ -0,0 +1,76 @@ +package ca.uhn.fhir.rest.api; + +/* + * #%L + * HAPI FHIR Search Parameters + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.UrlUtil; + +import java.util.HashMap; +import java.util.Map; + +public enum SearchContainedModeEnum { + + /** + * default, search on the non-contained (normal) resources + */ + FALSE("false"), + + /** + * search on the contained resources only + */ + TRUE("true"), + + /** + * Search on the normal resources and contained resources. + * This option is not supported yet. + */ + BOTH("both"); + + private static volatile Map ourCodeToEnum; + private final String myCode; + + SearchContainedModeEnum(String theCode) { + myCode = theCode; + } + + private String getCode() { + return myCode; + } + + public static SearchContainedModeEnum fromCode(String theCode) { + Map codeToEnum = ourCodeToEnum; + if (codeToEnum == null) { + codeToEnum = new HashMap<>(); + for (SearchContainedModeEnum next : values()) { + codeToEnum.put(next.getCode(), next); + } + ourCodeToEnum = codeToEnum; + } + + SearchContainedModeEnum retVal = codeToEnum.get(theCode); + if (retVal == null) { + throw new InvalidRequestException("Invalid contained mode: " + UrlUtil.sanitizeUrlPart(theCode)); + } + + return retVal; + } + +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/CommonConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/CommonConfig.java index b0668955161..9e3463aa160 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/CommonConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/CommonConfig.java @@ -25,7 +25,6 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import org.apache.commons.dbcp2.BasicDataSource; -import org.apache.commons.lang3.time.DateUtils; import org.hibernate.dialect.H2Dialect; import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings; import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; @@ -49,9 +48,6 @@ public class CommonConfig { @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); - retVal.setSubscriptionEnabled(true); - retVal.setSubscriptionPollDelay(5000); - retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); return retVal; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index ec7b0546280..ee145f02902 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -59,9 +59,6 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); - retVal.setSubscriptionEnabled(true); - retVal.setSubscriptionPollDelay(5000); - retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); retVal.setAllowMultipleDelete(true); return retVal; } @@ -79,7 +76,6 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { /** * Do some fancy logging to create a nice access log that has details about each incoming request. - * @return */ public LoggingInterceptor loggingInterceptor() { LoggingInterceptor retVal = new LoggingInterceptor(); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml new file mode 100644 index 00000000000..d5d13a6c90e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/support-contained-searches.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 2441 +title: "Support has been added to the JPA server for indexing and searching using the `_contained` parameter, which + allows searching using chained parameters that chain into contained resources. This feature is disabled by default + but can be enabled via a setting on the ModelConfig object." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index 97847d2ac44..d45156245b0 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -118,11 +118,6 @@ public class DaoConfig { * update setter javadoc if default changes */ private Integer myFetchSizeDefaultMaximum = null; - private int myHardTagListLimit = 1000; - /** - * update setter javadoc if default changes - */ - private boolean myIndexContainedResources = true; private int myMaximumExpansionSize = DEFAULT_MAX_EXPANSION_SIZE; private Integer myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION; @@ -209,9 +204,6 @@ public class DaoConfig { * Constructor */ public DaoConfig() { - setSubscriptionEnabled(true); - setSubscriptionPollDelay(0); - setSubscriptionPurgeInactiveAfterMillis(Long.MAX_VALUE); setMarkResourcesForReindexingUponSearchParameterChange(true); setReindexThreadCount(Runtime.getRuntime().availableProcessors()); setExpungeThreadCount(Runtime.getRuntime().availableProcessors()); @@ -580,20 +572,6 @@ public class DaoConfig { myFetchSizeDefaultMaximum = theFetchSizeDefaultMaximum; } - /** - * Gets the maximum number of results to return in a GetTags query (DSTU1 only) - */ - public int getHardTagListLimit() { - return myHardTagListLimit; - } - - /** - * Gets the maximum number of results to return in a GetTags query (DSTU1 only) - */ - public void setHardTagListLimit(int theHardTagListLimit) { - myHardTagListLimit = theHardTagListLimit; - } - /** * If set to {@link IndexEnabledEnum#DISABLED} (default is {@link IndexEnabledEnum#DISABLED}) * the server will not create search indexes for search parameters with no values in resources. @@ -1442,22 +1420,6 @@ public class DaoConfig { myExpungeBatchSize = theExpungeBatchSize; } - /** - * Should contained IDs be indexed the same way that non-contained IDs are (default is - * true) - */ - public boolean isIndexContainedResources() { - return myIndexContainedResources; - } - - /** - * Should contained IDs be indexed the same way that non-contained IDs are (default is - * true) - */ - public void setIndexContainedResources(boolean theIndexContainedResources) { - myIndexContainedResources = theIndexContainedResources; - } - /** * Should resources be marked as needing reindexing when a * SearchParameter resource is added or changed. This should generally @@ -1591,62 +1553,6 @@ public class DaoConfig { myValidateSearchParameterExpressionsOnSave = theValidateSearchParameterExpressionsOnSave; } - /** - * Do not call this method, it exists only for legacy reasons. It - * will be removed in a future version. Configure the page size on your - * paging provider instead. - * - * @deprecated This method does not do anything. Configure the page size on your - * paging provider instead. Deprecated in HAPI FHIR 2.3 (Jan 2017) - */ - @Deprecated - public void setHardSearchLimit(int theHardSearchLimit) { - // this method does nothing - } - - /** - * This is the maximum number of resources that will be added to a single page of returned resources. Because of - * includes with wildcards and other possibilities it is possible for a client to make requests that include very - * large amounts of data, so this hard limit can be imposed to prevent runaway requests. - * - * @deprecated Deprecated in HAPI FHIR 3.2.0 as this method doesn't actually do anything - */ - @Deprecated - public void setIncludeLimit(@SuppressWarnings("unused") int theIncludeLimit) { - // nothing - } - - /** - * @deprecated As of HAPI FHIR 3.0.0, subscriptions no longer use polling for - * detecting changes, so this setting has no effect - */ - @Deprecated - public void setSubscriptionEnabled(boolean theSubscriptionEnabled) { - // nothing - } - - /** - * @deprecated As of HAPI FHIR 3.0.0, subscriptions no longer use polling for - * detecting changes, so this setting has no effect - */ - @Deprecated - public void setSubscriptionPollDelay(long theSubscriptionPollDelay) { - // ignore - } - - /** - * @deprecated As of HAPI FHIR 3.0.0, subscriptions no longer use polling for - * detecting changes, so this setting has no effect - */ - @Deprecated - public void setSubscriptionPurgeInactiveAfterMillis(Long theMillis) { - // ignore - } - - public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) { - setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND); - } - /** * This setting sets the number of search results to prefetch. For example, if this list * is set to [100, 1000, -1] then the server will initially load 100 results and not diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index f4288f05d8c..2dcae91118e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -54,7 +54,7 @@ import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; -import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -1292,16 +1292,11 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) { - if (theRequest != null) { - String[] contained = theRequest.getParameters().get(Constants.PARAM_CONTAINED); - if (contained != null && contained.length > 0) { - if (contained[0].equals("true")) { - theParams.setSearchContainedMode(SearchContainedEnum.TRUE); - ourLog.info("Search on contained resources only"); - } else if (contained[0].equals("both")) { - ourLog.warn("Search on both normal resources and contained resources are not support. set to default search on normal resources"); - } - } + if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) { + throw new MethodNotAllowedException("Contained mode 'both' is not currently supported"); + } + if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE && !myModelConfig.isIndexOnContainedResources()) { + throw new MethodNotAllowedException("Searching with _contained mode enabled is not enabled on this server"); } if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index a63f8b38c40..fb3489bb890 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -52,7 +52,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.SourceParam; @@ -561,7 +561,7 @@ public class QueryStack { List paths = join.createResourceLinkPaths(targetResourceType, paramReference); Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType)); Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); - Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedEnum.FALSE); + Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE); andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); } @@ -1046,7 +1046,7 @@ public class QueryStack { } @Nullable - public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchContainedEnum theSearchContainedMode) { + public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchContainedModeEnum theSearchContainedMode) { if (theAndOrParams.isEmpty()) { return null; @@ -1108,7 +1108,7 @@ public class QueryStack { break; case REFERENCE: for (List nextAnd : theAndOrParams) { - if (theSearchContainedMode.equals(SearchContainedEnum.TRUE)) + if (theSearchContainedMode.equals(SearchContainedModeEnum.TRUE)) andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); else andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index 0d5a086bea8..db28da9ce88 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -60,7 +60,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper; import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper; -import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.jpa.util.BaseIterator; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; @@ -216,7 +216,7 @@ public class SearchBuilder implements ISearchBuilder { attemptCompositeUniqueSpProcessing(theQueryStack, theParams, theRequest); } - SearchContainedEnum searchContainedMode = theParams.getSearchContainedMode(); + SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode(); // Handle each parameter List paramNames = new ArrayList<>(myParams.keySet()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index c64e43ae503..f37a3e428f8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -43,7 +43,7 @@ import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; -import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -395,7 +395,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder { List andPredicates = new ArrayList<>(); List> chainParamValues = Collections.singletonList(orValues); - andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId, SearchContainedEnum.FALSE)); + andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE)); orPredicates.add(toAndPredicate(andPredicates)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index f530dbaaabe..87ad9c1383b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -244,9 +244,6 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @BeforeEach public void beforeResetConfig() { - myDaoConfig.setHardSearchLimit(1000); - myDaoConfig.setHardTagListLimit(1000); - myDaoConfig.setIncludeLimit(2000); myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index a33280e97ce..3680f0247a5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -396,9 +396,6 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @BeforeEach public void beforeResetConfig() { - myDaoConfig.setHardSearchLimit(1000); - myDaoConfig.setHardTagListLimit(1000); - myDaoConfig.setIncludeLimit(2000); myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java index c9a20d333fe..8319ec16bd6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java @@ -3,12 +3,10 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; -import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -18,23 +16,17 @@ public class FhirResourceDaoDstu3ContainedTest extends BaseJpaDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3ContainedTest.class); - - @Test - public void before() { - myDaoConfig.setIndexContainedResources(true); - } - @Test public void testIndexContained() { Patient p = new Patient(); p.setId("#some_patient"); p.addName().setFamily("MYFAMILY").addGiven("MYGIVEN"); - + Observation o1 = new Observation(); o1.getCode().setText("Some Observation"); o1.setSubject(new Reference(p)); IIdType oid1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); - + Observation o2 = new Observation(); o2.getCode().setText("Some Observation"); o2.setSubject(new Reference(p)); @@ -43,16 +35,16 @@ public class FhirResourceDaoDstu3ContainedTest extends BaseJpaDstu3Test { Patient p2 = new Patient(); p2.addName().setFamily("MYFAMILY").addGiven("MYGIVEN"); IIdType pid2 = myPatientDao.create(p2, mySrd).getId().toUnqualifiedVersionless(); - + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(o2)); - - + + SearchParameterMap map = new SearchParameterMap(); map.add(Observation.SP_CODE, new TokenParam(null, "some observation").setModifier(TokenParamModifier.TEXT)); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(oid1, oid2))); } - + // TODO: make sure match URLs don't delete - + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 6eaa3bf9027..bfbf2dfbe8f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -476,7 +476,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Autowired private IValidationSupport myJpaValidationSupportChainR4; private PerformanceTracingLoggingInterceptor myPerformanceTracingLoggingInterceptor; - private List mySystemInterceptors; @Autowired private IBulkDataExportSvc myBulkDataExportSvc; @Autowired @@ -524,8 +523,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @BeforeEach public void beforeCreateInterceptor() { - mySystemInterceptors = myInterceptorRegistry.getAllRegisteredInterceptors(); - myInterceptor = mock(IServerInterceptor.class); myPerformanceTracingLoggingInterceptor = new PerformanceTracingLoggingInterceptor(); @@ -558,9 +555,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @BeforeEach public void beforeResetConfig() { - myDaoConfig.setHardSearchLimit(1000); - myDaoConfig.setHardTagListLimit(1000); - myDaoConfig.setIncludeLimit(2000); myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); myValidationSettings.setLocalReferenceValidationDefaultPolicy(new ValidationSettings().getLocalReferenceValidationDefaultPolicy()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java index 17f0cf30afc..363bf007ab3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java @@ -5,7 +5,6 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Address; import org.hl7.fhir.r4.model.Address.AddressUse; @@ -27,7 +26,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ca.uhn.fhir.jpa.searchparam.SearchContainedEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -75,7 +74,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("subject", new ReferenceParam("name", "Smith")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id))); } @@ -111,7 +110,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("subject", new ReferenceParam("name", "Smith")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id))); } @@ -180,7 +179,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("general-practitioner", new ReferenceParam("family", "Smith")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), containsInAnyOrder(toValues(id))); } @@ -265,7 +264,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("based-on", new ReferenceParam("authored", "2021-02-23")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); assertThat(toUnqualifiedVersionlessIdValues(myEncounterDao.search(map)), containsInAnyOrder(toValues(id))); } @@ -277,7 +276,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("subject", new ReferenceParam("near", "toronto")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); try { IBundleProvider outcome = myObservationDao.search(map); @@ -296,7 +295,7 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.add("subject", new ReferenceParam("marital-status", "M")); - map.setSearchContainedMode(SearchContainedEnum.TRUE); + map.setSearchContainedMode(SearchContainedModeEnum.TRUE); try { IBundleProvider outcome = myObservationDao.search(map); @@ -307,4 +306,4 @@ public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { } } -} \ No newline at end of file +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java index dd3fc3d3a49..8ac8333c6c0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java @@ -148,13 +148,6 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest { purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportSvc); } - @BeforeEach - public void beforeResetConfig() { - myDaoConfig.setHardSearchLimit(1000); - myDaoConfig.setHardTagListLimit(1000); - myDaoConfig.setIncludeLimit(2000); - } - @Override protected PlatformTransactionManager getTxManager() { return myTxManager; @@ -171,7 +164,7 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest { Organization org = new Organization(); org.setName(methodName); - IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); SearchParameterMap map = new SearchParameterMap(); map.add(ca.uhn.fhir.rest.api.Constants.PARAM_CONTENT, new StringParam(methodName)); @@ -189,7 +182,7 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest { Organization org = new Organization(); org.setName(methodName); - IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); SearchParameterMap map = new SearchParameterMap(); map.add(Organization.SP_NAME, new StringParam(methodName)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java index 14283fa83d7..517c4915c11 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java @@ -480,7 +480,6 @@ public abstract class BaseJpaR5Test extends BaseJpaTest { @BeforeEach public void beforeResetConfig() { - myDaoConfig.setHardTagListLimit(1000); myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index ea04e937679..3e7b50b22d7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -309,9 +309,6 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { @Test public void testCountParam() { - // NB this does not get used- The paging provider has its own limits built in - myDaoConfig.setHardSearchLimit(100); - List resources = new ArrayList(); for (int i = 0; i < 100; i++) { Organization org = new Organization(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java index 217a97b4a34..af91f8da02a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4SearchContainedTest.java @@ -1,13 +1,12 @@ package ca.uhn.fhir.jpa.provider.r4; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -35,21 +34,23 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.parser.StrictErrorHandler; -import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; -import ca.uhn.fhir.util.UrlUtil; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4SearchContainedTest.class); - private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor(); - @Autowired @Qualifier("myClinicalImpressionDaoR4") protected IFhirResourceDao myClinicalImpressionDao; + private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor(); @Override @AfterEach @@ -63,9 +64,10 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); - + myClient.unregisterInterceptor(myCapturingInterceptor); myModelConfig.setIndexOnContainedResources(false); + myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); } @BeforeEach @@ -78,312 +80,325 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR myClient.registerInterceptor(myCapturingInterceptor); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myModelConfig.setIndexOnContainedResources(true); - } - - @BeforeEach - public void beforeDisableResultReuse() { myDaoConfig.setReuseCachedSearchResultsForMillis(null); } + @Test + public void testContainedDisabled() throws Exception { + myModelConfig.setIndexOnContainedResources(false); + + String uri = ourServerBase + "/Observation?subject.name=Smith&_contained=true"; + try (CloseableHttpResponse response = ourHttpClient.execute(new HttpGet(uri))) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(MethodNotAllowedException.STATUS_CODE, response.getStatusLine().getStatusCode()); + assertThat(resp, containsString(">Searching with _contained mode enabled is not enabled on this server")); + } + } + + @Test + public void testContainedBoth() throws Exception { + String uri = ourServerBase + "/Observation?subject.name=Smith&_contained=both"; + try (CloseableHttpResponse response = ourHttpClient.execute(new HttpGet(uri))) { + String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(resp); + assertEquals(MethodNotAllowedException.STATUS_CODE, response.getStatusLine().getStatusCode()); + assertThat(resp, containsString("Contained mode 'both' is not currently supported")); + } + } @Test public void testContainedSearchByName() throws Exception { - + IIdType oid1; - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 1"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Doe").addGiven("Jane"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Jones").addGiven("Peter"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - - + + //-- Simple name match String uri = ourServerBase + "/Observation?subject.name=Smith&_contained=true"; List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Simple name match with or uri = ourServerBase + "/Observation?subject.name=Smith,Jane&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(2L, oids.size()); //assertEquals(oids.toString(), "[Observation/1, Observation/2]"); - + //-- Simple name match with qualifier uri = ourServerBase + "/Observation?subject.name:exact=Smith&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Simple name match with and uri = ourServerBase + "/Observation?subject.family=Smith&subject.given=John&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - - //-- Simple name match with both, default to normal search, found 0 - uri = ourServerBase + "/Observation?subject.name=Smith&_contained=both"; - oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - - assertEquals(0L, oids.size()); } @Test public void testContainedSearchByDate() throws Exception { - + IIdType oid1; IIdType oid3; - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); p.getBirthDateElement().setValueAsString("2000-01-01"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 1"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Doe").addGiven("Jane"); p.getBirthDateElement().setValueAsString("2000-02-01"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Jones").addGiven("Peter"); p.getBirthDateElement().setValueAsString("2000-03-01"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + oid3 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + //-- Search by date default op String uri = ourServerBase + "/Observation?subject.birthdate=2000-01-01&_contained=true"; List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Search by date op=eq uri = ourServerBase + "/Observation?subject.birthdate=eq2000-01-01&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Search by date op=eq, with or uri = ourServerBase + "/Observation?subject.birthdate=2000-01-01,2000-02-01&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(2L, oids.size()); //assertEquals(oids.toString(), "[Observation/1, Observation/2]"); - + //-- Simple name match with op = gt uri = ourServerBase + "/Observation?subject.birthdate=gt2000-02-10&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid3.getValue())); - + //-- Simple name match with AND uri = ourServerBase + "/Observation?subject.family=Smith&subject.birthdate=eq2000-01-01&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Simple name match with AND - not found uri = ourServerBase + "/Observation?subject.family=Smith&subject.birthdate=eq2000-02-01&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(0L, oids.size()); } - + @Test public void testContainedSearchByNumber() throws Exception { - + IIdType cid1; - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); p.getBirthDateElement().setValueAsString("2000-01-01"); - - + + RiskAssessment risk = new RiskAssessment(); risk.setId("risk1"); risk.setStatus(RiskAssessmentStatus.CORRECTED); risk.getSubject().setReference("#patient1"); risk.getPredictionFirstRep().setProbability(new DecimalType(2)); - + ClinicalImpression imp = new ClinicalImpression(); imp.setStatus(ClinicalImpressionStatus.COMPLETED); - + imp.getContained().add(p); imp.getSubject().setReference("#patient1"); - + imp.getContained().add(risk); imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); - + cid1 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); - + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); p.getBirthDateElement().setValueAsString("2000-01-01"); - - + + RiskAssessment risk = new RiskAssessment(); risk.setId("risk1"); risk.setStatus(RiskAssessmentStatus.CORRECTED); risk.getSubject().setReference("#patient1"); risk.getPredictionFirstRep().setProbability(new DecimalType(5)); - + ClinicalImpression imp = new ClinicalImpression(); imp.setStatus(ClinicalImpressionStatus.COMPLETED); - + imp.getContained().add(p); imp.getSubject().setReference("#patient1"); - + imp.getContained().add(risk); imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); - + IIdType cid2 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); - + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid2); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); p.getBirthDateElement().setValueAsString("2000-01-01"); - - + + RiskAssessment risk = new RiskAssessment(); risk.setId("risk1"); risk.setStatus(RiskAssessmentStatus.CORRECTED); risk.getSubject().setReference("#patient1"); risk.getPredictionFirstRep().setProbability(new DecimalType(10)); - + ClinicalImpression imp = new ClinicalImpression(); imp.setStatus(ClinicalImpressionStatus.COMPLETED); - + imp.getContained().add(p); imp.getSubject().setReference("#patient1"); - + imp.getContained().add(risk); imp.getInvestigationFirstRep().getItemFirstRep().setReference("#risk1"); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(imp)); - + IIdType cid3 = myClinicalImpressionDao.create(imp, mySrd).getId().toUnqualifiedVersionless(); - + ClinicalImpression createdImp = myClinicalImpressionDao.read(cid3); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdImp)); } - + //-- Search by number String uri = ourServerBase + "/ClinicalImpression?investigation.probability=2&_contained=true"; List cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, cids.size()); assertThat(cids, contains(cid1.getValue())); - - + + //-- Search by number with op = eq uri = ourServerBase + "/ClinicalImpression?investigation.probability=eq2&_contained=true"; cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, cids.size()); assertThat(cids, contains(cid1.getValue())); - - + + //-- Search by number with op = eq and or uri = ourServerBase + "/ClinicalImpression?investigation.probability=eq2,10&_contained=true"; cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); @@ -392,25 +407,25 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR //-- Search by number with op = lt uri = ourServerBase + "/ClinicalImpression?investigation.probability=lt4&_contained=true"; cids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, cids.size()); - assertThat(cids, contains(cid1.getValue())); + assertThat(cids, contains(cid1.getValue())); } - + @Test public void testContainedSearchByQuantity() throws Exception { - + IIdType eid1; { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -421,27 +436,27 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(200); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - - + + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -452,26 +467,26 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(300); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + IIdType eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid2); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -482,46 +497,46 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(400); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid3); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + //-- Search by quantity String uri = ourServerBase + "/Encounter?reason-reference.combo-value-quantity=200&_contained=true"; List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, eids.size()); assertThat(eids, contains(eid1.getValue())); - - + + //-- Search by quantity uri = ourServerBase + "/Encounter?reason-reference.combo-value-quantity=le400&_contained=true"; eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(3L, eids.size()); } - + @Test public void testContainedSearchByToken() throws Exception { - + IIdType eid1; { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -532,27 +547,27 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(200); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - - + + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -563,26 +578,26 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(300); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + IIdType eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid2); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -593,39 +608,39 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(400); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid3); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + //-- Search by code String uri = ourServerBase + "/Encounter?reason-reference.code=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7") + "&_contained=true"; List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, eids.size()); assertThat(eids, contains(eid1.getValue())); - + } - + @Test public void testContainedSearchByComposite() throws Exception { - + IIdType eid2; { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -636,27 +651,27 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(200); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + IIdType eid1 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - - + + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -667,26 +682,26 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(300); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - + eid2 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + Encounter createdEncounter = myEncounterDao.read(eid2); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + { Encounter encounter = new Encounter(); encounter.setStatus(EncounterStatus.ARRIVED); - + Patient patient = new Patient(); patient.setId("patient1"); patient.addName().setFamily("Doe").addGiven("Jane"); encounter.getSubject().setReference("#patient1"); encounter.getContained().add(patient); - + Observation obs = new Observation(); obs.setId("obs1"); obs.addIdentifier().setSystem("urn:system").setValue("FOO"); @@ -697,37 +712,37 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR quantity.setValue(400); encounter.addReasonReference().setReference("#obs1"); encounter.getContained().add(obs); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(encounter)); - - IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); - + + IIdType eid3 = myEncounterDao.create(encounter, mySrd).getId().toUnqualifiedVersionless(); + Encounter createdEncounter = myEncounterDao.read(eid3); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEncounter)); } - + //-- Search by composite String uri = ourServerBase + "/Encounter?reason-reference.combo-code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-8$300") + "&_contained=true"; List eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, eids.size()); assertThat(eids, contains(eid2.getValue())); - + //-- Search by composite - not found uri = ourServerBase + "/Encounter?reason-reference.combo-code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$300") + "&_contained=true"; eids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - - assertEquals(0L, eids.size()); - + + assertEquals(0L, eids.size()); + } - - + + @Test public void testContainedSearchByUri() throws Exception { - + IIdType oid1; - + { Patient p = new Patient(); p.setId("patient1"); @@ -740,24 +755,24 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR carePlan.setIntent(CarePlanIntent.ORDER); carePlan.getSubject().setReference("#patient1"); carePlan.addInstantiatesUri("http://www.hl7.com"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 1"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); obs.getContained().add(carePlan); obs.getBasedOnFirstRep().setReference("#carePlan1"); - - + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); - + Observation createdObs = myObservationDao.read(oid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs)); } - + { Patient p = new Patient(); p.setId("patient2"); @@ -770,19 +785,19 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR carePlan.setIntent(CarePlanIntent.ORDER); carePlan.getSubject().setReference("#patient2"); carePlan.addInstantiatesUri("http://www2.hl7.com"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient2"); obs.getContained().add(carePlan); obs.getBasedOnFirstRep().setReference("#carePlan2"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient3"); @@ -795,32 +810,32 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR carePlan.setIntent(CarePlanIntent.ORDER); carePlan.getSubject().setReference("#patient3"); carePlan.addInstantiatesUri("http://www2.hl7.com"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 3"); obs.getContained().add(p); obs.getSubject().setReference("#patient3"); obs.getContained().add(carePlan); obs.getBasedOnFirstRep().setReference("#carePlan3"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + //-- Search by uri String uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www.hl7.com&_contained=true"; List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); - + //-- Search by uri more than 1 results uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www2.hl7.com&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); assertEquals(2L, oids.size()); - + //-- Search by uri with 'or' uri = ourServerBase + "/Observation?based-on.instantiates-uri=http://www.hl7.com,http://www2.hl7.com&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); @@ -828,94 +843,94 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR assertEquals(3L, oids.size()); } - + @Test public void testUpdateContainedResource() throws Exception { - + IIdType oid1; - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Smith").addGiven("John"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 1"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); - + Observation createdObs = myObservationDao.read(oid1); - + //-- changed the last name to Doe List containedResources = createdObs.getContained(); - + for (Resource res : containedResources) { if (res instanceof Patient) { - Patient p1 = (Patient)res; + Patient p1 = (Patient) res; HumanName name = p1.getNameFirstRep(); name.setFamily("Doe"); break; } } - + // -- update oid1 = myObservationDao.update(createdObs, mySrd).getId().toUnqualifiedVersionless(); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Doe").addGiven("Jane"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - + { Patient p = new Patient(); p.setId("patient1"); p.addName().setFamily("Jones").addGiven("Peter"); - + Observation obs = new Observation(); obs.getCode().setText("Observation 2"); obs.getContained().add(p); obs.getSubject().setReference("#patient1"); - + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); } - - + + //-- No Obs with Patient Smith String uri = ourServerBase + "/Observation?subject.family=Smith&_contained=true"; List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(0L, oids.size()); - + //-- Two Obs with Patient Doe uri = ourServerBase + "/Observation?subject.family=Doe&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(2L, oids.size()); } - + @Test public void testDeleteContainedResource() throws Exception { - + IIdType oid1; - + { Patient p1 = new Patient(); p1.setId("patient1"); @@ -929,39 +944,40 @@ public class ResourceProviderR4SearchContainedTest extends BaseResourceProviderR obs.getCode().setText("Observation 1"); obs.getContained().add(p1); obs.getSubject().setReference("#patient1"); - + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); - + // -- remove contained resource obs.getContained().remove(p1); // -- add new contained resource obs.getContained().add(p2); obs.getSubject().setReference("#patient2"); - + ourLog.info("Input: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); // -- update oid1 = myObservationDao.update(obs, mySrd).getId().toUnqualifiedVersionless(); - + Observation updatedObs = myObservationDao.read(oid1); - + ourLog.info("Output: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(updatedObs)); } - + //-- No Obs with Patient Smith String uri = ourServerBase + "/Observation?subject.family=Smith&_contained=true"; List oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(0L, oids.size()); - + //-- 1 Obs with Patient Doe uri = ourServerBase + "/Observation?subject.family=Doe&_contained=true"; oids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - + assertEquals(1L, oids.size()); assertThat(oids, contains(oid1.getValue())); } + private List searchAndReturnUnqualifiedVersionlessIdValues(String uri) throws IOException { List ids; HttpGet get = new HttpGet(uri); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java index 4b9822f18db..54804331e05 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java @@ -7,7 +7,12 @@ import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; import ca.uhn.fhir.rest.api.MethodOutcome; import com.google.common.collect.Lists; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.EventDefinition; +import org.hl7.fhir.r4.model.Expression; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.TriggerDefinition; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -36,6 +41,7 @@ import java.util.List; * 6. Execute the 'sendObservation' test * 7. Look in the 'attachWebSocket' terminal execution and wait for your ping with the subscription id */ + /** * Ignored because this feature isn't implemented yet */ @@ -77,15 +83,6 @@ public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Tes mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); } - @Override - @BeforeEach - public void before() throws Exception { - super.before(); - - myDaoConfig.setSubscriptionEnabled(true); - - } - @Test public void testSubscriptionAddedTrigger() { /* diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java index 85d46486957..6f35dfdfc9b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java @@ -9,7 +9,12 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.Subscription; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,8 +25,8 @@ import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; /** * Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the @@ -71,9 +76,6 @@ public class WebsocketWithSubscriptionIdDstu3Test extends BaseResourceProviderDs public void before() throws Exception { super.before(); - myDaoConfig.setSubscriptionEnabled(true); - myDaoConfig.setSubscriptionPollDelay(0L); - mySubscriptionTestUtil.registerWebSocketInterceptor(); /* diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 4e06fd1d4f5..b875d57cf80 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -734,8 +734,8 @@ public class ModelConfig { /** - * Should indexed on the contained resources, it could be searched by _contained=true - * This may have performance impacts + * Should indexing and searching on contained resources be enabled on this server. + * This may have performance impacts, and should be enabled only if it is needed. Default is false. * * @since 5.4.0 */ @@ -744,9 +744,9 @@ public class ModelConfig { } /** - * Should indexed on the contained resources, it could be searched by _contained=true - * This may have performance impacts - * + * Should indexing and searching on contained resources be enabled on this server. + * This may have performance impacts, and should be enabled only if it is needed. Default is false. + * * @since 5.4.0 */ public void setIndexOnContainedResources(boolean theIndexOnContainedResources) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java deleted file mode 100644 index 1755b09b85d..00000000000 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchContainedEnum.java +++ /dev/null @@ -1,40 +0,0 @@ -package ca.uhn.fhir.jpa.searchparam; - -/* - * #%L - * HAPI FHIR Search Parameters - * %% - * Copyright (C) 2014 - 2021 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -public enum SearchContainedEnum { - - /** - * default, search on the non-contained (normal) resources - */ - FALSE, - - /** - * search on the contained resources only - */ - TRUE, - - /** - * Search on the normal resources and contained resources. - * This option is not supported yet. - */ - BOTH, -} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index e6434a8cf5e..85a6ef8cf4a 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.rest.api.SearchTotalModeEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; @@ -79,7 +80,7 @@ public class SearchParameterMap implements Serializable { private boolean myLastN; private Integer myLastNMax; private boolean myDeleteExpunge; - private SearchContainedEnum mySearchContainedMode = SearchContainedEnum.FALSE; + private SearchContainedModeEnum mySearchContainedMode = SearchContainedModeEnum.FALSE; /** * Constructor @@ -735,12 +736,16 @@ public class SearchParameterMap implements Serializable { return retVal; } - public SearchContainedEnum getSearchContainedMode() { + public SearchContainedModeEnum getSearchContainedMode() { return mySearchContainedMode; } - public void setSearchContainedMode(SearchContainedEnum theSearchContainedMode) { - this.mySearchContainedMode = theSearchContainedMode; + public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) { + if (theSearchContainedMode == null) { + mySearchContainedMode = SearchContainedModeEnum.FALSE; + } else { + this.mySearchContainedMode = theSearchContainedMode; + } } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index 365957556f4..cbaf424d495 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -79,6 +79,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { retVal.setWebsocketContextPath("/"); retVal.setFilterParameterEnabled(true); retVal.setDefaultSearchParamsCanBeOverridden(false); + retVal.getModelConfig().setIndexOnContainedResources(true); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index b9d81163c10..bcbdb4a825f 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -77,6 +77,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { retVal.setExpungeEnabled(true); retVal.setFilterParameterEnabled(true); retVal.setDefaultSearchParamsCanBeOverridden(false); + retVal.getModelConfig().setIndexOnContainedResources(true); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index db7f0b7192d..f1194ba835d 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -78,6 +78,7 @@ public class TestR4Config extends BaseJavaConfigR4 { retVal.setExpungeEnabled(true); retVal.setFilterParameterEnabled(true); retVal.setDefaultSearchParamsCanBeOverridden(false); + retVal.getModelConfig().setIndexOnContainedResources(true); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java index e5f3a5b2aee..f7f4d08b241 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java @@ -77,6 +77,7 @@ public class TestR5Config extends BaseJavaConfigR5 { retVal.setExpungeEnabled(true); retVal.setFilterParameterEnabled(true); retVal.setDefaultSearchParamsCanBeOverridden(false); + retVal.getModelConfig().setIndexOnContainedResources(true); return retVal; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 58281c642f5..a9d131791f0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -44,7 +44,6 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; public class RuleBuilder implements IAuthRuleBuilder { - private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final ConcurrentHashMap, String> ourTypeToName = new ConcurrentHashMap<>(); private ArrayList myRules; private IAuthRuleBuilderRule myAllow; @@ -144,7 +143,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(String... theTenantIds) { - return forTenantIds(Arrays.asList(defaultIfNull(theTenantIds, EMPTY_STRING_ARRAY))); + return forTenantIds(Arrays.asList(defaultIfNull(theTenantIds, Constants.EMPTY_STRING_ARRAY))); } @Override @@ -162,7 +161,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(String... theTenantIds) { - return notForTenantIds(Arrays.asList(defaultIfNull(theTenantIds, EMPTY_STRING_ARRAY))); + return notForTenantIds(Arrays.asList(defaultIfNull(theTenantIds, Constants.EMPTY_STRING_ARRAY))); } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index ee39ab733c1..e01d1daa859 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -131,6 +131,8 @@ public class MethodUtil { param = new SummaryEnumParameter(); } else if (parameterType.equals(PatchTypeEnum.class)) { param = new PatchTypeParameter(); + } else if (parameterType.equals(SearchContainedModeEnum.class)) { + param = new SearchContainedModeParameter(); } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchContainedModeParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchContainedModeParameter.java new file mode 100644 index 00000000000..bb6480c1320 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchContainedModeParameter.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.rest.server.method; + +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +import java.lang.reflect.Method; +import java.util.Collection; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +class SearchContainedModeParameter implements IParameter { + + @Override + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + return getTypeForRequestOrThrowInvalidRequestException(theRequest); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore + } + + public static SearchContainedModeEnum getTypeForRequestOrThrowInvalidRequestException(RequestDetails theRequest) { + String[] paramValues = theRequest.getParameters().getOrDefault(Constants.PARAM_CONTAINED, Constants.EMPTY_STRING_ARRAY); + if (paramValues.length > 0 && isNotBlank(paramValues[0])) { + return SearchContainedModeEnum.fromCode(paramValues[0]); + } + return null; + } + +} diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm index 44323929224..9c5a9f533e8 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm @@ -20,6 +20,7 @@ import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SearchContainedModeEnum; public class ${className}ResourceProvider extends ## We have specialized base classes for RPs that handle certain resource types. These @@ -138,7 +139,9 @@ public class ${className}ResourceProvider extends SummaryEnum theSummaryMode, - SearchTotalModeEnum theSearchTotalMode + SearchTotalModeEnum theSearchTotalMode, + + SearchContainedModeEnum theSearchContainedMode ) { startRequest(theServletRequest); @@ -165,6 +168,7 @@ public class ${className}ResourceProvider extends paramMap.setOffset(theOffset); paramMap.setSummaryMode(theSummaryMode); paramMap.setSearchTotalMode(theSearchTotalMode); + paramMap.setSearchContainedMode(theSearchContainedMode); getDao().translateRawParameters(theAdditionalRawParams, paramMap);