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] 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) {