diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index eab8e1d9823..dbedfafc2fc 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 58baeddd7f0..c12f4590109 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index ba4ee190103..883720ac38c 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index adb72aa8000..1e3ef5f9624 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.context; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -8,6 +9,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -18,6 +20,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; @@ -56,6 +59,7 @@ public class RuntimeSearchParam { private final RuntimeSearchParamStatusEnum myStatus; private final String myUri; private final Map>> myExtensions = new HashMap<>(); + private final Map myUpliftRefchains = new HashMap<>(); private final ComboSearchParamType myComboSearchParamType; private final List myComponents; private IPhoneticEncoder myPhoneticEncoder; @@ -148,7 +152,7 @@ public class RuntimeSearchParam { /** * Sets user data - This can be used to store any application-specific data */ - public RuntimeSearchParam addExtension(String theKey, IBaseExtension theValue) { + public RuntimeSearchParam addExtension(String theKey, IBaseExtension theValue) { List> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>()); valuesList.add(theValue); return this; @@ -276,6 +280,40 @@ public class RuntimeSearchParam { return retVal; } + public void addUpliftRefchain(@Nonnull String theCode, @Nonnull String theElementName) { + myUpliftRefchains.put(theCode, theElementName); + } + + /** + * Does this search parameter have an uplift refchain definition for the given code? + * See the HAPI FHIR documentation for a description of how uplift refchains work. + * + * @since 6.6.0 + */ + public boolean hasUpliftRefchain(String theCode) { + return myUpliftRefchains.containsKey(theCode); + } + + /** + * Returns a set of all codes associated with uplift refchains for this search parameter. + * See the HAPI FHIR documentation for a description of how uplift refchains work. + * + * @since 6.6.0 + */ + public Set getUpliftRefchainCodes() { + return Collections.unmodifiableSet(myUpliftRefchains.keySet()); + } + + /** + * Does this search parameter have any uplift refchain definitions? + * See the HAPI FHIR documentation for a description of how uplift refchains work. + * + * @since 6.6.0 + */ + public boolean hasUpliftRefchains() { + return !myUpliftRefchains.isEmpty(); + } + public enum RuntimeSearchParamStatusEnum { ACTIVE, DRAFT, diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java index ac5d25ddc6a..e9d8d0e5df5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java @@ -23,12 +23,16 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseHasExtensions; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nonnull; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -291,4 +295,32 @@ public class ExtensionUtil { public static boolean equals(IBaseExtension theLeftExtension, IBaseExtension theRightExtension) { return TerserUtil.equals(theLeftExtension, theRightExtension); } + + /** + * Given an extension, looks for the first child extension with the given URL of {@literal theChildExtensionUrl} + * and a primitive datatype value, and returns the String version of that value. E.g. if the + * value is a FHIR boolean, it would return the string "true" or "false. If the extension + * has no value, or the value is not a primitive datatype, or the URL is not found, the method + * will return {@literal null}. + * + * @param theExtension The parent extension. Must not be null. + * @param theChildExtensionUrl The child extension URL. Must not be null or blank. + * @since 6.6.0 + */ + public static > String extractChildPrimitiveExtensionValue(@Nonnull IBaseExtension theExtension, @Nonnull String theChildExtensionUrl) { + Validate.notNull(theExtension, "theExtension must not be null"); + Validate.notBlank(theChildExtensionUrl, "theChildExtensionUrl must not be null or blank"); + + Optional codeExtension = theExtension + .getExtension() + .stream() + .filter(t -> theChildExtensionUrl.equals(t.getUrl())) + .findFirst(); + String retVal = null; + if (codeExtension.isPresent() && codeExtension.get().getValue() instanceof IPrimitiveType) { + IPrimitiveType codeValue = (IPrimitiveType) codeExtension.get().getValue(); + retVal = codeValue.getValueAsString(); + } + return retVal; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java index fc40ab4ef00..a5e0add14aa 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java @@ -140,6 +140,22 @@ public class HapiExtensions { */ public static final String EXTENSION_SUBSCRIPTION_CROSS_PARTITION = "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-cross-partition"; + /** + * This extension is used for "uplifted refchains" on search parameters. See the + * HAPI FHIR documentation for an explanation of how these work. + */ + public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN = "https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain"; + /** + * This extension is used for "uplifted refchains" on search parameters. See the + * HAPI FHIR documentation for an explanation of how these work. + */ + public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE = "code"; + /** + * This extension is used for "uplifted refchains" on search parameters. See the + * HAPI FHIR documentation for an explanation of how these work. + */ + public static final String EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME = "element-name"; + /** * Non instantiable */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ProxyUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ProxyUtil.java index 83bea3f445c..b0dd7a6e010 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ProxyUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ProxyUtil.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.util; +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.apache.commons.lang3.Validate; import java.lang.reflect.InvocationHandler; diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseExtension.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseExtension.java index 7a21f788af1..02b2e3a5e9e 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseExtension.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseExtension.java @@ -22,6 +22,11 @@ package org.hl7.fhir.instance.model.api; import java.util.List; +/** + * @param The actual concrete type of the extension + * @param Note that this type param is not used anywhere - It is kept only to avoid making a breaking change + */ +//public interface IBaseExtension, D> extends ICompositeType { public interface IBaseExtension extends ICompositeType { List getExtension(); diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 966e3fef2ad..492c946ff19 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -119,6 +119,8 @@ ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteResourceNotExisting=Not deleted, resour ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteResourceAlreadyDeleted=Not deleted, resource {0} was already deleted. ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSearchParameter=Unknown search parameter "{0}" for resource type "{1}". Valid search parameters for this search are: {2} ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSortParameter=Unknown _sort parameter value "{0}" for resource type "{1}" (Note: sort parameters values must use a valid Search Parameter). Valid values for this search are: {2} +ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSortParameterTooManyChains=Invalid _sort expression, can not chain more than once in a sort expression: {0} + ca.uhn.fhir.jpa.dao.BaseStorageDao.updateWithNoId=Can not update resource of type {0} as it has no ID ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidBundleTypeForStorage=Unable to store a Bundle resource on this server with a Bundle.type value of: {0}. Note that if you are trying to perform a FHIR 'transaction' or 'batch' operation you should POST the Bundle resource to the Base URL of the server, not to the '/Bundle' endpoint. ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 43c6ade12d7..41d6c376b9a 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,14 +4,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 0b159dc80ba..bf5f7d3be73 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 0f3ea46f857..bfaa6125453 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index ef04ffab388..881490acf4e 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index d9d23d5e81c..b6217c969d5 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index ffe0a532792..0de209d19e2 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 86dbe6338d1..a35869d97c1 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index e0ba4a3be5d..dadd943595d 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index ccc60efb5e3..404b0c668f5 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 0797fb4ea8e..a2c8e6100e4 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 44fc1bf868a..4a3d1d713b0 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-chained-sort.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-chained-sort.yaml new file mode 100644 index 00000000000..e0388b58ceb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-chained-sort.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 4633 +title: "JPS server _sort expressions can now include a single chained search expression, such + as a search for `Encounter?_sort=patient.name`." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-uplifted-refchains.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-uplifted-refchains.yaml new file mode 100644 index 00000000000..884b09b5729 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4633-uplifted-refchains.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 4633 +title: "A new JPA feature called Uplifed Refchains has been added. This feature allows chained search + expressions to be precalculated for much better performance when executing chained searches." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md index d06abec758d..65e303ec29e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md @@ -1,22 +1,230 @@ # JPA Server Search -## Limitations +This page describes features and limitations for performing [FHIR searches](https://hl7.org/fhir/search.html) on the JPA server. -The HAPI FHIR JPA Server fully implements most [FHIR search](https://www.hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR: +# Limitations + +The HAPI FHIR JPA Server fully implements most [FHIR search](https://hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR: ### Chains within _has -Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported +Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported + ```http https://localhost:8000/Practitioner?_has:PractitionerRole:practitioner:service.type=CHIRO ``` ### Location.position "near" -Searching on Location.Position using `near` currently uses a box search, not a radius search. A box search is a square centred on the provided coordinates with the shortest distance to the edge of the square equal to the distance provided; this means the box search will match more locations than a radius search in the corners. Currently, distance is assumed to be in km and any provided units are ignored. Distance must be between 0.0km and 10,000km. +Searching on Location.Position using `near` currently uses a box search, not a radius search. A box search is a square centred on the provided coordinates with the shortest distance to the edge of the square equal to the distance provided; this means the box search will match more locations than a radius search in the corners. Currently, distance is assumed to be in km and any provided units are ignored. Distance must be between 0.0km and 10,000km. ### _filter The special `_filter` is only partially implemented. + + +# Uplifted Refchains and Chaining Performance + +FHIR chained searches allow for a search against a reference search parameter to 'chain' into the reference targets and search these targets for a given search criteria. + +For example, consider the search: + +```url +http://example.org/Encounter?subject.name=Simpson +``` + +This search returns any Encounter resources where the `Encounter.subject` reference points to a Patient or Group resource satisfying the search `name=Simpson`. + +In order to satisfy this search, a two-level SQL JOIN is required in order to satisfy both the reference and the string portions of the search. This search leverages two indexes created by the indexer: + +* A reference index satisfying the `subject` search parameter, associated with the _Encounter_ resource. +* A string index satisfying the `name` search parameter, associated with the _Patient_ and _Group_ resources. + +If you are having performance issues when performing chained searches like this one, a feature called **Uplifted Refchains** can be used to create a single index against the Encounter resource. Uplifted refchains promote chained search parameters to create a single index which includes both parts of the chain. In the example above this means: + +* A string index satisfying the `subject.name` search parameter, associated with the _Encounter_ resource. + +This can be very good for search performance, especially in cases where the second part of the chain (.name) matches a very large number of resources. + +## Drawbacks + +Using Uplifted Refchains has several drawbacks however, and it is important to consider them before enabling this feature: + +* Write speed will typically be slower for the resource type containing the uplifted refchain, since the target needs to be resolved, parsed, and the additional uplifted refchain index rows need to be written. +* Changes to the target data may not be reflected in the chained search. For example, using + the `Encounter?subject.name=Simpson` example above, the value of Simpson will be written to the index using the Patient's name at the time that the Encounter resource is written. If the Patient resource's name is subsequently changed to _Flanders_ in an update, the new name will not be reflected in the chained search unless the Encounter + resource is reindexed. + +## Defining Uplifted Refchains + +To define an uplifted refchain, the reference search parameter for the first part of the chain must be created or updated in order to add a new extension. + +Continuing the example above, this means updating the `Encounter:subject` search parameter, creating it if it does not exist. Be careful not to create a second search parameter if you already have one defined for Encounter:subject. In this case, you must update the existing search parameter and add the new extension to it. + +The extension has the following URL: + +```url +https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain +``` + +This extension has the following children: + +* `code` - Contains a code with the name of the chained search parameter to uplift. + +An example follows: + +```json +{ + "resourceType": "SearchParameter", + "id": "Encounter-subject", + "extension": [ + { + "url": "https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain", + "extension": [ + { + "url": "code", + "valueCode": "name" + } + ] + } + ], + "url": "http://hl7.org/fhir/SearchParameter/Encounter-subject", + "name": "subject", + "status": "active", + "code": "subject", + "base": [ + "Encounter" + ], + "type": "reference", + "expression": "Encounter.subject", + "target": [ + "Group", + "Patient" + ] +} +``` + +# Document and Message Search Parameters + +The FHIR standard defines several Search Parameters on the Bundle resource that are intended to be used for specialized Bundle types. + + + + + + + + + + + + + + + + + + + + + +
NameTypeFHIRPath Expression
compositionReferenceBundle.entry[0].resource as Composition
messageReferenceBundle.entry[0].resource as MessageHeader
+ +Unlike any other search parameters in the FHIR specification, these parameters use a FHIRPath expression that resolves to an embedded Resource (all other Search Parameters in the FHIR specification resolve to a datatype). + +These parameters are only intended to be used as a part of a chained search expression, since it would not be meaningful to use them otherwise. For example, the following query could be used in order to use the _composition_ Search Parameter to locate FHIR Documents stored on the server that are an [International Patient Summary](./ips.html). In other words, this searches for Bundle resources where the first resource in the Bundle is a Composition, and that Composition has a `Composition.type` using LOINC code `60591-5`. + +```url +https://hapi.fhir.org/baseR4/Bundle?composition.type=http://loinc.org%7C60591-5 +``` + +In order to use these search parameters, you may create a Search Parameter that includes the fully chained parameter value as the _name_ and _code_ values. In the example above, `composition` is the search parameter name, and `type` is the chain name. The fully chained parameter value is `composition.type`. + +You should then use a FHIRPath expression that fully resolves the intended value within the Bundle resource. You may use the `resolve()` function to resolve resources that are contained within the same Bundle. Note that when used in a SearchParameter expression, the `resolve()` function is not able to resolve resources outside of the resource being stored. + +```json +{ + "resourceType": "SearchParameter", + "id": "Bundle-composition-patient-identifier", + "url": "http://example.org/SearchParameter/Bundle-composition-patient-identifier", + "name": "composition.patient.identifier", + "status": "active", + "code": "composition.patient.identifier", + "base": [ "Bundle" ], + "type": "token", + "expression": "Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier" +} +``` + + +In order to use these Search Parameters, you must enable [Uplifted Refchains](#uplifted-refchains) on your server, and modify the SearchParameter resource in order to add Uplifteed Refchain definitions for any chained searches you wish to support. + +For example, to modify the _composition_ SearchParameter in order to support the query above, you can use the following resource: + +```json +{ + "resourceType": "SearchParameter", + "id": "Bundle-composition", + "extension": [ + { + "url": "https://smilecdr.com/fhir/ns/StructureDefinition/searchparameter-uplift-refchain", + "extension": [ + { + "url": "code", + "valueCode": "type" + } + ] + } + ], + "url": "http://hl7.org/fhir/SearchParameter/Bundle-composition", + "name": "composition", + "status": "active", + "code": "composition", + "base": [ + "Bundle" + ], + "type": "reference", + "expression": "Bundle.entry[0].resource as Composition" +} +``` + + +
+ +# Chained Sorting + +The FHIR specification allows `_sort` expressions to use a comma-separated list of search parameter names in order to influence the sorting on search results. + +HAPI FHIR extends this by allowing single-chained expressions as well. So for example, you can request a list of Encounter resources and sort them by the family name of the subject/patient of the Encounter by using the search shown in the example below. In this search, we are looking for all Encounter resources (typically additional search parameters would be used to limit the included Encounter resources), and sorting them by the value of the `family` search parameter on the Patient resource, where the Patient is referenced from the Encounter via the `patient` search parameter. + +```url +http://example.org/Encounter?_sort=patient.family +``` + +Like chained search expressions, the first step in the chain must be a reference SearchParameter (SearchParameter.type = 'reference'). Unlike chained search expressions, only certain search parameter types can be used in the second part of the chain: + +* String +* Date +* Token + +If the reference search parameter defines multiple target types, it must be qualified with the specific target type you want to use when sorting. For example, the Encounter:subject search parameter can refer to targets of type _Patient_ or _Group_. The following expression **will not work** because the specific target type to use is not clear to the server. + +```url +http://example.org/Encounter?_sort=subject.family +``` + +The following qualified expression adds a type qualifier and will work: + +```url +http://example.org/Encounter?_sort=Patient:subject.family +``` + +## Chained Sort Performance + +Chained sorting is more than twice as demanding of database performance. They involve sorting on an index that is only connected to the primary resource in the search by a multi-level join, and read more data in the database. Performance of chained sort expressions is highly variable. + +In particular, this kind of sorting can be very slow if the search returns a large number of results (e.g. a search for Encounter?sort=patient.name where there is a very large number of Encounter resources and no additional search parameters are limiting the number of included resources). They are safest when used in smaller collections, and as a secondary sort; as a tie-breaker within another sort. E.g. `Encounter?practitioner=practitioner-id&date=2023-02&_sort=location,patient.name`. + +In order to improve sorting performance when chained sorts are needed, an [Uplifted Refchain](#uplifted-refchains) can be defined on the SearchParameter. This index will be used for the sorting expression and can improve performance. diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index dda750cd31e..27d77c671fd 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 524326a4189..7279a1438e8 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index 00a6617d8e5..e06684395af 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 91bcf0603c1..df744700159 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 7e36a19d9d4..7ae0d861935 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -999,7 +999,7 @@ public abstract class BaseHapiFhirDao extends BaseStora entity.setDeleted(null); // TODO: is this IF statement always true? Try removing it - if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) { + if (thePerformIndexing || theEntity.getVersion() == 1) { newParams = new ResourceIndexedSearchParams(); @@ -1013,6 +1013,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } failIfPartitionMismatch(theRequest, entity); + mySearchParamWithInlineReferencesExtractor.populateFromResource(requestPartitionId, newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing); changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 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 f38ec007d2d..b3a23de460a 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 @@ -414,6 +414,7 @@ public abstract class BaseHapiFhirResourceDao extends B jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext)); myIdHelperService.addResolvedPidToForcedId(jpaPid, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null); theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid); + theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource); // Pre-cache the match URL if (theMatchUrl != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchUrlDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchUrlDao.java index a56b1ed62a0..5915e63648f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchUrlDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchUrlDao.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.dao.data; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ResourceSearchUrlSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ResourceSearchUrlSvc.java index 1103a0dacff..40a0d7a2038 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ResourceSearchUrlSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ResourceSearchUrlSvc.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchUrlJobMaintenanceSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchUrlJobMaintenanceSvcImpl.java index 743da4652b8..799403e6345 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchUrlJobMaintenanceSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchUrlJobMaintenanceSvcImpl.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.jpa.api.svc.ISearchUrlJobMaintenanceSvc; import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs; 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 84bd37f653a..cd8f94ca53f 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 @@ -77,6 +77,7 @@ import ca.uhn.fhir.rest.param.HasParam; import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.SpecialParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; @@ -129,6 +130,7 @@ import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation; import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate; import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS; import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.split; @@ -229,15 +231,103 @@ public class QueryStack { } - public void addSortOnResourceLink(String theResourceName, String theParamName, boolean theAscending) { + public void addSortOnResourceLink(String theResourceName, String theReferenceTargetType, String theParamName, String theChain, boolean theAscending) { BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this); - Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>()); + Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName); addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate); - mySqlBuilder.addSortNumeric(resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate); + if (isBlank(theChain)) { + mySqlBuilder.addSortNumeric(resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate); + return; + } + + String targetType = null; + RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); + if (theReferenceTargetType != null) { + targetType = theReferenceTargetType; + } else if (param.getTargets().size() > 1) { + throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '" + theParamName + "' as this parameter has multiple target types. Please specify the target type."); + } else if (param.getTargets().size() == 1) { + targetType = param.getTargets().iterator().next(); + } + + if (isBlank(targetType)) { + throw new InvalidRequestException(Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName + "' as this parameter as this parameter does not define a target type. Please specify the target type."); + } + + RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam(targetType, theChain); + if (targetSearchParameter == null) { + Collection validSearchParameterNames = mySearchParamRegistry + .getActiveSearchParams(targetType) + .values() + .stream() + .filter(t -> + t.getParamType() == RestSearchParameterTypeEnum.STRING || + t.getParamType() == RestSearchParameterTypeEnum.TOKEN || + t.getParamType() == RestSearchParameterTypeEnum.DATE) + .map(RuntimeSearchParam::getName) + .sorted() + .distinct() + .collect(Collectors.toList()); + String msg = myFhirContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSortParameter", theChain, targetType, validSearchParameterNames); + throw new InvalidRequestException(Msg.code(2289) + msg); + } + + BaseSearchParamPredicateBuilder chainedPredicateBuilder; + DbColumn[] sortColumn; + switch (targetSearchParameter.getParamType()) { + case STRING: + StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder(); + sortColumn = new DbColumn[]{ + stringPredicateBuilder.getColumnValueNormalized() + }; + chainedPredicateBuilder = stringPredicateBuilder; + break; + case TOKEN: + TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder(); + sortColumn = new DbColumn[]{ + tokenPredicateBuilder.getColumnSystem(), + tokenPredicateBuilder.getColumnValue() + }; + chainedPredicateBuilder = tokenPredicateBuilder; + break; + case DATE: + DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder(); + sortColumn = new DbColumn[]{ + datePredicateBuilder.getColumnValueLow() + }; + chainedPredicateBuilder = datePredicateBuilder; + break; + + /* + * Note that many of the options below aren't implemented because they + * don't seem useful to me, but they could theoretically be implemented + * if someone ever needed them. I'm not sure why you'd want to do a chained + * sort on a target that was a reference or a quantity, but if someone needed + * that we could implement it here. + */ + case NUMBER: + case REFERENCE: + case COMPOSITE: + case QUANTITY: + case URI: + case HAS: + case SPECIAL: + default: + throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter " + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: " + targetSearchParameter.getParamType().name()); + } + + addSortCustomJoin(resourceLinkPredicateBuilder.getColumnTargetResourceId(), chainedPredicateBuilder, null); + Condition predicate = chainedPredicateBuilder.createHashIdentityPredicate(targetType, theChain); + mySqlBuilder.addPredicate(predicate); + + for (DbColumn next : sortColumn) { + mySqlBuilder.addSortNumeric(next, theAscending, myUseAggregate); + } + } public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) { @@ -275,17 +365,23 @@ public class QueryStack { mySqlBuilder.addSortString(uriPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); } - private void addSortCustomJoin(BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder, BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, Condition theCondition){ + private void addSortCustomJoin(BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder, BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, Condition theCondition) { + addSortCustomJoin(theFromJoiningPredicateBuilder.getResourceIdColumn(), theToJoiningPredicateBuilder, theCondition); + } + + private void addSortCustomJoin(DbColumn theFromDbColumn, BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, Condition theCondition) { ComboCondition onCondition = mySqlBuilder.createOnCondition( - theFromJoiningPredicateBuilder.getResourceIdColumn(), + theFromDbColumn, theToJoiningPredicateBuilder.getResourceIdColumn() ); - onCondition.addCondition(theCondition); + if (theCondition != null) { + onCondition.addCondition(theCondition); + } mySqlBuilder.addCustomJoin( SelectQuery.JoinType.LEFT_OUTER, - theFromJoiningPredicateBuilder.getTable(), + theFromDbColumn.getTable(), theToJoiningPredicateBuilder.getTable(), onCondition); } @@ -385,7 +481,7 @@ public class QueryStack { ourLog.error("Cannot create missing parameter query for a composite parameter."); return null; } else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - if (isEligibleForContainedResourceSearch(theParams.getQueryParameterTypes())) { + if (isEligibleForEmbeddedChainedResourceSearch(theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes()).supportsUplifted()) { ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search."); return null; } @@ -572,6 +668,7 @@ public class QueryStack { SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); } + public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { @@ -958,238 +1055,52 @@ public class QueryStack { mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn()); } - private class ChainElement { - private final String myResourceType; - private final String mySearchParameterName; - private final String myPath; + public Condition createPredicateReferenceForEmbeddedChainedSearchResource(@Nullable DbColumn theSourceJoinColumn, + String theResourceName, RuntimeSearchParam theSearchParam, + List theList, SearchFilterParser.CompareOperation theOperation, + RequestDetails theRequest, RequestPartitionId theRequestPartitionId, + EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { - public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { - this.myResourceType = theResourceType; - this.mySearchParameterName = theSearchParameterName; - this.myPath = thePath; - } + boolean wantChainedAndNormal = theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; - public String getResourceType() { - return myResourceType; - } - - public String getPath() { return myPath; } - - public String getSearchParameterName() { return mySearchParameterName; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ChainElement that = (ChainElement) o; - return myResourceType.equals(that.myResourceType) && mySearchParameterName.equals(that.mySearchParameterName) && myPath.equals(that.myPath); - } - - @Override - public int hashCode() { - return Objects.hash(myResourceType, mySearchParameterName, myPath); - } - } - - private class ReferenceChainExtractor { - private final Map,Set> myChains = Maps.newHashMap(); - - public Map,Set> getChains() { return myChains; } - - private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { - return split(theReferenceParam.getChain(), '.').length <= 3; - } - - private List extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { - List pathsForType = theSearchParam.getPathsSplit().stream() - .map(String::trim) - .filter(t -> t.startsWith(theResourceType)) - .collect(Collectors.toList()); - if (pathsForType.isEmpty()) { - ourLog.warn("Search parameter {} does not have a path for resource type {}.", theSearchParam.getName(), theResourceType); - } - - return pathsForType; - } - - public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List theList) { - List paths = extractPaths(theResourceType, theSearchParam); - for (String path : paths) { - List searchParams = Lists.newArrayList(); - searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); - for (IQueryParameterType nextOr : theList) { - String targetValue = nextOr.getValueAsQueryToken(myFhirContext); - if (nextOr instanceof ReferenceParam) { - ReferenceParam referenceParam = (ReferenceParam) nextOr; - if (!isReferenceParamValid(referenceParam)) { - throw new InvalidRequestException(Msg.code(2007) + - "The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() + - " is too long. Only chains up to three references are supported."); - } - - String targetChain = referenceParam.getChain(); - List qualifiers = Lists.newArrayList(referenceParam.getResourceType()); - - processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType()); - - } - } - } - } - - private void processNextLinkInChain(List theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List theQualifiers, String theResourceType) { - - String nextParamName = theChain; - String nextChain = null; - String nextQualifier = null; - int linkIndex = theChain.indexOf('.'); - if (linkIndex != -1) { - nextParamName = theChain.substring(0, linkIndex); - nextChain = theChain.substring(linkIndex+1); - } - - int qualifierIndex = nextParamName.indexOf(':'); - if (qualifierIndex != -1) { - nextParamName = nextParamName.substring(0, qualifierIndex); - nextQualifier = nextParamName.substring(qualifierIndex); - } - - List qualifiersBranch = Lists.newArrayList(); - qualifiersBranch.addAll(theQualifiers); - qualifiersBranch.add(nextQualifier); - - boolean searchParamFound = false; - for (String nextTarget : thePreviousSearchParam.getTargets()) { - RuntimeSearchParam nextSearchParam = null; - if (StringUtils.isBlank(theResourceType) || theResourceType.equals(nextTarget)) { - nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName); - } - if (nextSearchParam != null) { - searchParamFound = true; - // If we find a search param on this resource type for this parameter name, keep iterating - // Otherwise, abandon this branch and carry on to the next one - if (StringUtils.isEmpty(nextChain)) { - // We've reached the end of the chain - ArrayList orValues = Lists.newArrayList(); - - if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { - orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); - } else { - IQueryParameterType qp = toParameterType(nextSearchParam); - qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); - orValues.add(qp); - } - - Set leafNodes = myChains.get(theSearchParams); - if (leafNodes == null) { - leafNodes = Sets.newHashSet(); - myChains.put(theSearchParams, leafNodes); - } - leafNodes.add(new LeafNodeDefinition(nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); - } else { - List nextPaths = extractPaths(nextTarget, nextSearchParam); - for (String nextPath : nextPaths) { - List searchParamBranch = Lists.newArrayList(); - searchParamBranch.addAll(theSearchParams); - - searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); - processNextLinkInChain(searchParamBranch, nextSearchParam, nextChain, theTargetValue, qualifiersBranch, nextQualifier); - } - } - } - } - if (!searchParamFound) { - throw new InvalidRequestException(Msg.code(1214) + myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", thePreviousSearchParam.getName() + '.' + theChain)); - } - } - } - - private static class LeafNodeDefinition { - private final RuntimeSearchParam myParamDefinition; - private final ArrayList myOrValues; - private final String myLeafTarget; - private final String myLeafParamName; - private final String myLeafPathPrefix; - private final List myQualifiers; - - public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List theQualifiers) { - myParamDefinition = theParamDefinition; - myOrValues = theOrValues; - myLeafTarget = theLeafTarget; - myLeafParamName = theLeafParamName; - myLeafPathPrefix = theLeafPathPrefix; - myQualifiers = theQualifiers; - } - - public RuntimeSearchParam getParamDefinition() { - return myParamDefinition; - } - - public ArrayList getOrValues() { - return myOrValues; - } - - public String getLeafTarget() { - return myLeafTarget; - } - - public String getLeafParamName() { - return myLeafParamName; - } - - public String getLeafPathPrefix() { - return myLeafPathPrefix; - } - - public List getQualifiers() { - return myQualifiers; - } - - public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { - return new LeafNodeDefinition(myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LeafNodeDefinition that = (LeafNodeDefinition) o; - return Objects.equals(myParamDefinition, that.myParamDefinition) && Objects.equals(myOrValues, that.myOrValues) && Objects.equals(myLeafTarget, that.myLeafTarget) && Objects.equals(myLeafParamName, that.myLeafParamName) && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) && Objects.equals(myQualifiers, that.myQualifiers); - } - - @Override - public int hashCode() { - return Objects.hash(myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); - } - } - - public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, RuntimeSearchParam theSearchParam, - List theList, SearchFilterParser.CompareOperation theOperation, - RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse builders across different subselects EnumSet cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes); - myReusePredicateBuilderTypes.clear(); - - UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); + if (wantChainedAndNormal) { + myReusePredicateBuilderTypes.clear(); + } ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); chainExtractor.deriveChains(theResourceName, theSearchParam, theList); - Map,Set> chains = chainExtractor.getChains(); + Map, Set> chains = chainExtractor.getChains(); - Map,Set> referenceLinks = Maps.newHashMap(); + Map, Set> referenceLinks = Maps.newHashMap(); for (List nextChain : chains.keySet()) { Set leafNodes = chains.get(nextChain); - collateChainedSearchOptions(referenceLinks, nextChain, leafNodes); + collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum); } - for (List nextReferenceLink: referenceLinks.keySet()) { + UnionQuery union = null; + List predicates = null; + if (wantChainedAndNormal) { + union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); + } else { + predicates = new ArrayList<>(); + } + + predicates = new ArrayList<>(); + for (List nextReferenceLink : referenceLinks.keySet()) { for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { - SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder(); + SearchQueryBuilder builder; + if (wantChainedAndNormal) { + builder = mySqlBuilder.newChildSqlBuilder(); + } else { + builder = mySqlBuilder; + } + DbColumn previousJoinColumn = null; - // Create a reference link predicate to the subselect for every link but the last one + // Create a reference link predicates to the subselect for every link but the last one for (String nextLink : nextReferenceLink) { // We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion. // TODO: Are we missing any important business logic from that method? All tests are passing. @@ -1211,37 +1122,60 @@ public class QueryStack { theRequestPartitionId, builder); - builder.addPredicate(containedCondition); - - union.addQueries(builder.getSelect()); + if (wantChainedAndNormal) { + builder.addPredicate(containedCondition); + union.addQueries(builder.getSelect()); + } else { + predicates.add(containedCondition); + } } } - InCondition inCondition; - if (theSourceJoinColumn == null) { - inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); + Condition retVal; + if (wantChainedAndNormal) { + + if (theSourceJoinColumn == null) { + retVal = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); + } else { + //-- for the resource link, need join with target_resource_id + retVal = new InCondition(theSourceJoinColumn, union); + } + } else { - //-- for the resource link, need join with target_resource_id - inCondition = new InCondition(theSourceJoinColumn, union); + + retVal = toOrPredicate(predicates); + } // restore the state of this collection to turn caching back on before we exit myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); - return inCondition; + return retVal; } - private void collateChainedSearchOptions(Map, Set> referenceLinks, List nextChain, Set leafNodes) { + private void collateChainedSearchOptions(Map, Set> referenceLinks, List nextChain, Set leafNodes, EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { // Manually collapse the chain using all possible variants of contained resource patterns. // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this someday? // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper support for `_contained` if (nextChain.size() == 1) { // discrete -> discrete - updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); + if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) { + // If !theWantChainedAndNormal that means we're only processing refchains + // so the discrete -> contained case is the only one that applies + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); + } + // discrete -> contained + RuntimeSearchParam firstParamDefinition = leafNodes.iterator().next().getParamDefinition(); updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), leafNodes .stream() .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName())) + // When we're handling discrete->contained the differences between search + // parameters don't matter. E.g. if we're processing "subject.name=foo" + // the name could be Patient:name or Group:name but it doesn't actually + // matter that these are different since in this case both of these end + // up being an identical search in the string table for "subject.name". + .map(t -> t.withParam(firstParamDefinition)) .collect(Collectors.toSet())); } else if (nextChain.size() == 2) { // discrete -> discrete -> discrete @@ -1359,7 +1293,7 @@ public class QueryStack { theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); break; case REFERENCE: - containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers, + containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers, theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); break; case HAS: @@ -1465,11 +1399,15 @@ public class QueryStack { List andPredicates = new ArrayList<>(); for (List nextAndParams : theList) { - if ( ! checkHaveTags(nextAndParams, theParamName)) { continue; } + if (!checkHaveTags(nextAndParams, theParamName)) { + continue; + } List> tokens = Lists.newArrayList(); boolean paramInverted = populateTokens(tokens, nextAndParams); - if (tokens.isEmpty()) { continue; } + if (tokens.isEmpty()) { + continue; + } Condition tagPredicate; BaseJoiningPredicateBuilder join; @@ -1529,14 +1467,18 @@ public class QueryStack { for (IQueryParameterType nextParamUncasted : theParams) { if (nextParamUncasted instanceof TokenParam) { TokenParam nextParam = (TokenParam) nextParamUncasted; - if (isNotBlank(nextParam.getValue())) { return true; } + if (isNotBlank(nextParam.getValue())) { + return true; + } if (isNotBlank(nextParam.getSystem())) { - throw new TokenParamFormatInvalidRequestException(Msg.code(1218),theParamName, nextParam.getValueAsQueryToken(myFhirContext)); + throw new TokenParamFormatInvalidRequestException(Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext)); } } UriParam nextParam = (UriParam) nextParamUncasted; - if (isNotBlank(nextParam.getValue())) { return true; } + if (isNotBlank(nextParam.getValue())) { + return true; + } } return false; @@ -1612,7 +1554,7 @@ public class QueryStack { predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); } else { //-- for the resource link, need join with target_resource_id - predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true); + predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true); } } else { @@ -1747,10 +1689,18 @@ public class QueryStack { break; case REFERENCE: for (List nextAnd : theAndOrParams) { - if (isEligibleForContainedResourceSearch(nextAnd)) { - andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); - } else { + + // Handle Search Parameters where the name is a full chain + // (e.g. SearchParameter with name=composition.patient.identifier) + if (handleFullyChainedParameter(theSourceJoinColumn, theResourceName, theParamName, theRequest, theRequestPartitionId, andPredicates, nextAnd)) { + break; + } + + EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd); + if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) { andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); + } else { + andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId, embeddedChainedSearchModeEnum)); } } break; @@ -1829,14 +1779,100 @@ public class QueryStack { return toAndPredicate(andPredicates); } - private boolean isEligibleForContainedResourceSearch(List nextAnd) { - return myStorageSettings.isIndexOnContainedResources() && - nextAnd.stream() - .filter(t -> t instanceof ReferenceParam) - .map(t -> ((ReferenceParam) t).getChain()) - .filter(StringUtils::isNotBlank) - // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we ever want to support this, it would be really hard to do. - .anyMatch(t->!t.startsWith(PARAM_HAS + ":")); + /** + * This method handles the case of Search Parameters where the name/code + * in the SP is a full chain expression. Normally to handle an expression + * like Observation?subject.name=foo are handled by a SP + * with a type of REFERENCE where the name is "subject". That is not + * handled here. On the other hand, if the SP has a name value containing + * the full chain (e.g. "subject.name") we handle that here. + * + * @return Returns {@literal true} if the search parameter was handled + * by this method + */ + private boolean handleFullyChainedParameter(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, List andPredicates, List nextAnd) { + if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) { + ReferenceParam param = (ReferenceParam) nextAnd.get(0); + if (isNotBlank(param.getChain())) { + String fullName = theParamName + "." + param.getChain(); + RuntimeSearchParam fullChainParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, fullName); + if (fullChainParam != null) { + List swappedParamTypes = nextAnd + .stream() + .map(t -> toParameterType(fullChainParam, null, t.getValueAsQueryToken(myFhirContext))) + .collect(Collectors.toList()); + List> params = List.of(swappedParamTypes); + Condition predicate = createPredicateSearchParameter(theSourceJoinColumn, theResourceName, fullName, params, theRequest, theRequestPartitionId); + andPredicates.add(predicate); + return true; + } + } + } + return false; + } + + /** + * When searching using a chained search expression (e.g. "Patient?organization.name=foo") + * we have a few options: + *
    + *
  • + * A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for + * paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} + * with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY} + * which is the standard searching case. Let's guess that 99.9% of all searches work + * this way. + *
+ *
  • + * B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} + * with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}. + * We only do this if there is an uplifted refchain declared on the "organization" + * search parameter for the "name" search parameter, and contained indexing is disabled. + * This kind of index can come from indexing normal references where the search parameter + * has an uplifted refchain declared, and it can also come from indexing contained resources. + * For both of these cases, the actual index in the database is identical. But the important + * difference is that when you're searching for contained resources you also want to + * search for normal references. When you're searching for explicit refchains, no normal + * indexes matter because they'd be a duplicate of the uplifted refchain. + *
  • + *
  • + * C. We can also do both and return a union of the two, using + * {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained + * resource indexing is enabled since we have to assume there may be indexes + * on "organization" for both contained and non-contained Organization. + * resources. + *
  • + */ + private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch(String theResourceType, String theParameterName, List theParameter) { + boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); + boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains(); + + if (!indexOnContainedResources && !indexOnUpliftedRefchains) { + return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; + } + + boolean haveUpliftCandidates = theParameter.stream() + .filter(t -> t instanceof ReferenceParam) + .map(t -> ((ReferenceParam) t).getChain()) + .filter(StringUtils::isNotBlank) + // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we ever want to support this, it would be really hard to do. + .filter(t -> !t.startsWith(PARAM_HAS + ":")) + .anyMatch(t -> { + if (indexOnContainedResources) { + return true; + } + RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceType, theParameterName); + return param != null && param.hasUpliftRefchain(t); + }); + + if (haveUpliftCandidates) { + if (indexOnContainedResources) { + return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; + } + return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY; + } else { + return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; + } + } public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { @@ -1851,7 +1887,6 @@ public class QueryStack { mySqlBuilder.addPredicate(predicate); } - // expand out the pids public void addPredicateEverythingOperation(String theResourceName, List theTypeSourceResourceNames, Long... theTargetPids) { ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); @@ -1859,6 +1894,13 @@ public class QueryStack { mySqlBuilder.addPredicate(predicate); } + public IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { + IQueryParameterType qp = toParameterType(theParam); + + qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken); + return qp; + } + private IQueryParameterType toParameterType(RuntimeSearchParam theParam) { IQueryParameterType qp; @@ -1890,13 +1932,255 @@ public class QueryStack { case URI: qp = new UriParam(); break; - case HAS: case REFERENCE: + qp = new ReferenceParam(); + break; case SPECIAL: + qp = new SpecialParam(); + break; + case HAS: default: throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); } return qp; } + /** + * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum + */ + enum EmbeddedChainedSearchModeEnum { + + UPLIFTED_ONLY(true), + UPLIFTED_AND_REF_JOIN(true), + REF_JOIN_ONLY(false); + + private final boolean mySupportsUplifted; + + EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) { + mySupportsUplifted = theSupportsUplifted; + } + + public boolean supportsUplifted() { + return mySupportsUplifted; + } + } + + private final static class ChainElement { + private final String myResourceType; + private final String mySearchParameterName; + private final String myPath; + + public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { + this.myResourceType = theResourceType; + this.mySearchParameterName = theSearchParameterName; + this.myPath = thePath; + } + + public String getResourceType() { + return myResourceType; + } + + public String getPath() { + return myPath; + } + + public String getSearchParameterName() { + return mySearchParameterName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChainElement that = (ChainElement) o; + return myResourceType.equals(that.myResourceType) && mySearchParameterName.equals(that.mySearchParameterName) && myPath.equals(that.myPath); + } + + @Override + public int hashCode() { + return Objects.hash(myResourceType, mySearchParameterName, myPath); + } + } + + private class ReferenceChainExtractor { + private final Map, Set> myChains = Maps.newHashMap(); + + public Map, Set> getChains() { + return myChains; + } + + private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { + return split(theReferenceParam.getChain(), '.').length <= 3; + } + + private List extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { + List pathsForType = theSearchParam.getPathsSplit().stream() + .map(String::trim) + .filter(t -> t.startsWith(theResourceType)) + .collect(Collectors.toList()); + if (pathsForType.isEmpty()) { + ourLog.warn("Search parameter {} does not have a path for resource type {}.", theSearchParam.getName(), theResourceType); + } + + return pathsForType; + } + + public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List theList) { + List paths = extractPaths(theResourceType, theSearchParam); + for (String path : paths) { + List searchParams = Lists.newArrayList(); + searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); + for (IQueryParameterType nextOr : theList) { + String targetValue = nextOr.getValueAsQueryToken(myFhirContext); + if (nextOr instanceof ReferenceParam) { + ReferenceParam referenceParam = (ReferenceParam) nextOr; + if (!isReferenceParamValid(referenceParam)) { + throw new InvalidRequestException(Msg.code(2007) + + "The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() + + " is too long. Only chains up to three references are supported."); + } + + String targetChain = referenceParam.getChain(); + List qualifiers = Lists.newArrayList(referenceParam.getResourceType()); + + processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType()); + } + } + } + } + + private void processNextLinkInChain(List theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List theQualifiers, String theResourceType) { + + String nextParamName = theChain; + String nextChain = null; + String nextQualifier = null; + int linkIndex = theChain.indexOf('.'); + if (linkIndex != -1) { + nextParamName = theChain.substring(0, linkIndex); + nextChain = theChain.substring(linkIndex + 1); + } + + int qualifierIndex = nextParamName.indexOf(':'); + if (qualifierIndex != -1) { + nextParamName = nextParamName.substring(0, qualifierIndex); + nextQualifier = nextParamName.substring(qualifierIndex); + } + + List qualifiersBranch = Lists.newArrayList(); + qualifiersBranch.addAll(theQualifiers); + qualifiersBranch.add(nextQualifier); + + boolean searchParamFound = false; + for (String nextTarget : thePreviousSearchParam.getTargets()) { + RuntimeSearchParam nextSearchParam = null; + if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) { + nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName); + } + if (nextSearchParam != null) { + searchParamFound = true; + // If we find a search param on this resource type for this parameter name, keep iterating + // Otherwise, abandon this branch and carry on to the next one + if (StringUtils.isEmpty(nextChain)) { + // We've reached the end of the chain + ArrayList orValues = Lists.newArrayList(); + + if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { + orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); + } else { + IQueryParameterType qp = toParameterType(nextSearchParam); + qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); + orValues.add(qp); + } + + Set leafNodes = myChains.get(theSearchParams); + if (leafNodes == null) { + leafNodes = Sets.newHashSet(); + myChains.put(theSearchParams, leafNodes); + } + leafNodes.add(new LeafNodeDefinition(nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); + } else { + List nextPaths = extractPaths(nextTarget, nextSearchParam); + for (String nextPath : nextPaths) { + List searchParamBranch = Lists.newArrayList(); + searchParamBranch.addAll(theSearchParams); + + searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); + processNextLinkInChain(searchParamBranch, nextSearchParam, nextChain, theTargetValue, qualifiersBranch, nextQualifier); + } + } + } + } + if (!searchParamFound) { + throw new InvalidRequestException(Msg.code(1214) + myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", thePreviousSearchParam.getName() + '.' + theChain)); + } + } + } + + private static class LeafNodeDefinition { + private final RuntimeSearchParam myParamDefinition; + private final ArrayList myOrValues; + private final String myLeafTarget; + private final String myLeafParamName; + private final String myLeafPathPrefix; + private final List myQualifiers; + + public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List theQualifiers) { + myParamDefinition = theParamDefinition; + myOrValues = theOrValues; + myLeafTarget = theLeafTarget; + myLeafParamName = theLeafParamName; + myLeafPathPrefix = theLeafPathPrefix; + myQualifiers = theQualifiers; + } + + public RuntimeSearchParam getParamDefinition() { + return myParamDefinition; + } + + public ArrayList getOrValues() { + return myOrValues; + } + + public String getLeafTarget() { + return myLeafTarget; + } + + public String getLeafParamName() { + return myLeafParamName; + } + + public String getLeafPathPrefix() { + return myLeafPathPrefix; + } + + public List getQualifiers() { + return myQualifiers; + } + + public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { + return new LeafNodeDefinition(myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LeafNodeDefinition that = (LeafNodeDefinition) o; + return Objects.equals(myParamDefinition, that.myParamDefinition) && Objects.equals(myOrValues, that.myOrValues) && Objects.equals(myLeafTarget, that.myLeafTarget) && Objects.equals(myLeafParamName, that.myLeafParamName) && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) && Objects.equals(myQualifiers, that.myQualifiers); + } + + @Override + public int hashCode() { + return Objects.hash(myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); + } + + /** + * Return a copy of this object with the given {@link RuntimeSearchParam} + * but all other values unchanged. + */ + public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) { + return new LeafNodeDefinition(theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); + } + } + } 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 93779296013..9d128127c39 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 @@ -93,6 +93,7 @@ import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.collect.Streams; import com.healthmarketscience.sqlbuilder.Condition; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.math.NumberUtils; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -125,6 +126,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -748,41 +750,111 @@ public class SearchBuilder implements ISearchBuilder { } else { - RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName()); + RuntimeSearchParam param = null; + + /* + * If we have a sort like _sort=subject.name and we have an + * uplifted refchain for that combination we can do it more efficiently + * by using the index associated with the uplifted refchain. In this case, + * we need to find the actual target search parameter (corresponding + * to "name" in this example) so that we know what datatype it is. + */ + String paramName = theSort.getParamName(); + if (myStorageSettings.isIndexOnUpliftedRefchains()) { + String[] chains = StringUtils.split(paramName, '.'); + if (chains.length == 2) { + + // Given: Encounter?_sort=Patient:subject.name + String referenceParam = chains[0]; // subject + String referenceParamTargetType = null; // Patient + String targetParam = chains[1]; // name + + int colonIdx = referenceParam.indexOf(':'); + if (colonIdx > -1) { + referenceParamTargetType = referenceParam.substring(0, colonIdx); + referenceParam = referenceParam.substring(colonIdx + 1); + } + RuntimeSearchParam outerParam = mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam); + if (outerParam == null) { + throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam); + } + + if (outerParam.hasUpliftRefchain(targetParam)) { + for (String nextTargetType : outerParam.getTargets()) { + if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) { + continue; + } + RuntimeSearchParam innerParam = mySearchParamRegistry.getActiveSearchParam(nextTargetType, targetParam); + if (innerParam != null) { + param = innerParam; + break; + } + } + } + } + } + + int colonIdx = paramName.indexOf(':'); + String referenceTargetType = null; + if (colonIdx > -1) { + referenceTargetType = paramName.substring(0, colonIdx); + paramName = paramName.substring(colonIdx + 1); + } + + int dotIdx = paramName.indexOf('.'); + String chainName = null; + if (param == null && dotIdx > -1) { + chainName = paramName.substring(dotIdx + 1); + paramName = paramName.substring(0, dotIdx); + if (chainName.contains(".")) { + String msg = myContext + .getLocalizer() + .getMessageSanitized(BaseStorageDao.class, "invalidSortParameterTooManyChains", paramName + "." + chainName); + throw new InvalidRequestException(Msg.code(2286) + msg); + } + } + if (param == null) { - String msg = myContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSortParameter", theSort.getParamName(), getResourceName(), mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(getResourceName())); - throw new InvalidRequestException(Msg.code(1194) + msg); + param = mySearchParamRegistry.getActiveSearchParam(myResourceName, paramName); + } + + if (param == null) { + throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName); + } + + if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { + throw new InvalidRequestException(Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter"); } switch (param.getParamType()) { case STRING: - theQueryStack.addSortOnString(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnString(myResourceName, paramName, ascending); break; case DATE: - theQueryStack.addSortOnDate(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnDate(myResourceName, paramName, ascending); break; case REFERENCE: - theQueryStack.addSortOnResourceLink(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnResourceLink(myResourceName, referenceTargetType, paramName, chainName, ascending); break; case TOKEN: - theQueryStack.addSortOnToken(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnToken(myResourceName, paramName, ascending); break; case NUMBER: - theQueryStack.addSortOnNumber(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnNumber(myResourceName, paramName, ascending); break; case URI: - theQueryStack.addSortOnUri(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnUri(myResourceName, paramName, ascending); break; case QUANTITY: - theQueryStack.addSortOnQuantity(myResourceName, theSort.getParamName(), ascending); + theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending); break; case COMPOSITE: List compositeList = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param); if (compositeList == null) { - throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + theSort.getParamName() + " is not defined by the resource " + myResourceName); + throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName + " is not defined by the resource " + myResourceName); } if (compositeList.size() != 2) { - throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + theSort.getParamName() + throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName + " must have 2 composite types declared in parameter annotation, found " + compositeList.size()); } @@ -796,7 +868,7 @@ public class SearchBuilder implements ISearchBuilder { case SPECIAL: case HAS: default: - throw new InvalidRequestException(Msg.code(1197) + "This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName()); + throw new InvalidRequestException(Msg.code(1197) + "This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + paramName); } } @@ -806,6 +878,12 @@ public class SearchBuilder implements ISearchBuilder { } + private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) { + Collection validSearchParameterNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName); + String msg = myContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSortParameter", theParamName, theResourceName, validSearchParameterNames); + throw new InvalidRequestException(Msg.code(1194) + msg); + } + private void createCompositeSort(QueryStack theQueryStack, RestSearchParameterTypeEnum theParamType, String theParamName, boolean theAscending) { switch (theParamType) { 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 fe497c7dfbe..c1e3444ecdd 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 @@ -282,8 +282,8 @@ public class ResourceLinkPredicateBuilder return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); } - public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List theQualifiers) { - List pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers); + public Condition createPredicateSourcePaths(String theResourceName, String theParamName) { + List pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, Collections.emptyList()); return createPredicateSourcePaths(pathsToMatch); } @@ -627,56 +627,12 @@ public class ResourceLinkPredicateBuilder type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId); chainValue = type; } else { - chainValue = toParameterType(param, qualifier, resourceId); + chainValue = myQueryStack.toParameterType(param, qualifier, resourceId); } return chainValue; } - 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 = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam); - if (compositeOf.size() != 2) { - throw new InternalErrorException(Msg.code(1247) + "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 REFERENCE: - qp = new ReferenceParam(); - break; - case SPECIAL: - qp = new SpecialParam(); - break; - case URI: - qp = new UriParam(); - break; - case HAS: - default: - throw new InternalErrorException(Msg.code(1249) + "Don't know how to convert param type: " + theParam.getParamType()); - } - return qp; - } - - @Nonnull private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) { String searchParamName = theResourceName + ":" + theParamName; @@ -684,13 +640,6 @@ public class ResourceLinkPredicateBuilder return new InvalidRequestException(msg); } - private IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { - IQueryParameterType qp = toParameterType(theParam); - - qp.setValueAsQueryToken(getFhirContext(), theParam.getName(), theQualifier, theValueAsQueryToken); - return qp; - } - @Nonnull private InvalidRequestException newInvalidResourceTypeException(String theResourceType) { String msg = getFhirContext().getLocalizer().getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PersistenceContextProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PersistenceContextProvider.java index 1fe1db71a79..d0ad98ebf12 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PersistenceContextProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PersistenceContextProvider.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.util; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryParameterUtils.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryParameterUtils.java index 126b91a0533..4db0e59565e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryParameterUtils.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryParameterUtils.java @@ -62,6 +62,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; @@ -71,6 +72,7 @@ public class QueryParameterUtils { public static final int DEFAULT_SYNC_SIZE = 250; private static final BidiMap ourCompareOperationToParamPrefix; + public static final Condition[] EMPTY_CONDITION_ARRAY = new Condition[0]; static { DualHashBidiMap compareOperationToParamPrefix = new DualHashBidiMap<>(); @@ -89,13 +91,16 @@ public class QueryParameterUtils { @Nullable public static Condition toAndPredicate(List theAndPredicates) { - List andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList()); + List andPredicates = theAndPredicates + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); if (andPredicates.size() == 0) { return null; } else if (andPredicates.size() == 1) { return andPredicates.get(0); } else { - return ComboCondition.and(andPredicates.toArray(new Condition[0])); + return ComboCondition.and(andPredicates.toArray(EMPTY_CONDITION_ARRAY)); } } @@ -107,7 +112,7 @@ public class QueryParameterUtils { } else if (orPredicates.size() == 1) { return orPredicates.get(0); } else { - return ComboCondition.or(orPredicates.toArray(new Condition[0])); + return ComboCondition.or(orPredicates.toArray(EMPTY_CONDITION_ARRAY)); } } diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 11dd61e8426..71ec68659d0 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index af999f9ae03..4f8ce2d090d 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 04ce14747dd..1bed4465bf8 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 49c9c187cb7..1542b0e382b 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index f3348d114ed..915fc9d2038 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -255,8 +255,10 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP @Override public String toString() { ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + b.append("resourceType", getResourceType()); b.append("paramName", getParamName()); b.append("resourceId", getResourcePid()); + b.append("hashIdentity", getHashIdentity()); b.append("hashNormalizedPrefix", getHashNormalizedPrefix()); b.append("valueNormalized", getValueNormalized()); b.append("partitionId", getPartitionId()); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceSearchUrlEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceSearchUrlEntity.java index 5d4b95a7ef0..5e81e2fa1f1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceSearchUrlEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceSearchUrlEntity.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.entity; +/*- + * #%L + * HAPI FHIR JPA Model + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java index 84ac8db7257..58792a62992 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java @@ -111,6 +111,7 @@ public class StorageSettings { private Set myAutoVersionReferenceAtPaths = Collections.emptySet(); private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private boolean myRespectVersionsForSearchIncludes; + private boolean myIndexOnUpliftedRefchains = false; private boolean myIndexOnContainedResources = false; private boolean myIndexOnContainedResourcesRecursively = false; private boolean myAllowMdmExpansion = false; @@ -1060,6 +1061,28 @@ public class StorageSettings { myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes; } + /** + * If enabled, "Uplifted Refchains" will be enabled. This feature causes + * HAPI FHIR to generate indexes for stored resources that include the current + * value of the target of a chained reference, such as "Encounter?subject.name". + * + * @since 6.6.0 + */ + public boolean isIndexOnUpliftedRefchains() { + return myIndexOnUpliftedRefchains; + } + + /** + * If enabled, "Uplifted Refchains" will be enabled. This feature causes + * HAPI FHIR to generate indexes for stored resources that include the current + * value of the target of a chained reference, such as "Encounter?subject.name". + * + * @since 6.6.0 + */ + public void setIndexOnUpliftedRefchains(boolean theIndexOnUpliftedRefchains) { + myIndexOnUpliftedRefchains = theIndexOnUpliftedRefchains; + } + /** * Should indexing and searching on contained resources be enabled on this server. * This may have performance impacts, and should be enabled only if it is needed. Default is false. diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index fa411623ce5..390f6a55a4a 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml 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 34688051e1b..ca77f58a1d8 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 @@ -41,10 +41,12 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.bundle.BundleEntryParts; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import org.apache.commons.lang3.ObjectUtils; @@ -54,12 +56,14 @@ import org.apache.commons.text.StringTokenizer; import org.fhir.ucum.Pair; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseEnumeration; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r5.model.Base; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -179,7 +183,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public SearchParamSet extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) { IExtractor extractor = createReferenceExtractor(); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE, theWantLocalReferences); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE, theWantLocalReferences, ISearchParamExtractor.ALL_PARAMS); } private IExtractor createReferenceExtractor() { @@ -233,6 +237,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor extractor = createSpecialExtractor(theResource.getIdElement().getResourceType()); break; case COMPOSITE: + case HAS: default: throw new UnsupportedOperationException(Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction"); } @@ -265,10 +270,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamComposites(IBaseResource theResource) { + public SearchParamSet extractSearchParamComposites(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createCompositeExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false); - + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false, theParamsToIndex); } private IExtractor createCompositeExtractor(IBaseResource theResource) { @@ -554,9 +558,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamTokens(IBaseResource theResource) { + public SearchParamSet extractSearchParamTokens(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createTokenExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false, theParamsToIndex); } @Override @@ -589,10 +593,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamSpecial(IBaseResource theResource) { + public SearchParamSet extractSearchParamSpecial(IBaseResource theResource, Set theParamsToIndex) { String resourceTypeName = toRootTypeName(theResource); IExtractor extractor = createSpecialExtractor(resourceTypeName); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false, theParamsToIndex); } private IExtractor createSpecialExtractor(String theResourceTypeName) { @@ -609,9 +613,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamUri(IBaseResource theResource) { + public SearchParamSet extractSearchParamUri(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createUriExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI, false, theParamsToIndex); } private IExtractor createUriExtractor(IBaseResource theResource) { @@ -634,9 +638,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamDates(IBaseResource theResource) { + public SearchParamSet extractSearchParamDates(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createDateExtractor(theResource); - return extractSearchParams(theResource, extractor, DATE, false); + return extractSearchParams(theResource, extractor, DATE, false, theParamsToIndex); } private IExtractor createDateExtractor(IBaseResource theResource) { @@ -650,9 +654,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamNumber(IBaseResource theResource) { + public SearchParamSet extractSearchParamNumber(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createNumberExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false, theParamsToIndex); } private IExtractor createNumberExtractor(IBaseResource theResource) { @@ -685,16 +689,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamQuantity(IBaseResource theResource) { + public SearchParamSet extractSearchParamQuantity(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createQuantityUnnormalizedExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex); } @Override - public SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { + public SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createQuantityNormalizedExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex); } @Nonnull @@ -764,10 +768,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } @Override - public SearchParamSet extractSearchParamStrings(IBaseResource theResource) { + public SearchParamSet extractSearchParamStrings(IBaseResource theResource, Set theParamsToIndex) { IExtractor extractor = createStringExtractor(theResource); - return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING, false); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING, false, theParamsToIndex); } private IExtractor createStringExtractor(IBaseResource theResource) { @@ -830,7 +834,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor try { allValues = allValuesFunc.get(); } catch (Exception e) { - e.printStackTrace(); String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString()); throw new InternalErrorException(Msg.code(504) + msg, e); } @@ -1319,7 +1322,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor * This is the only way you could actually specify a FhirPath expression for those * prior to 6.2.0 so this isn't a breaking change. */ - SearchParamSet extractSearchParams(IBaseResource theResource, IExtractor theExtractor, RestSearchParameterTypeEnum theSearchParamType, boolean theWantLocalReferences) { + SearchParamSet extractSearchParams(IBaseResource theResource, IExtractor theExtractor, RestSearchParameterTypeEnum theSearchParamType, boolean theWantLocalReferences, Set theParamsToIndex) { SearchParamSet retVal = new SearchParamSet<>(); Collection searchParams = getSearchParams(theResource); @@ -1331,9 +1334,13 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor continue; } + if (!theParamsToIndex.equals(ISearchParamExtractor.ALL_PARAMS) && !theParamsToIndex.contains(nextSpDef.getName())) { + continue; + } + // See the method javadoc for an explanation of this if (startsWith(nextSpDef.getPath(), "Resource.")) { - continue; + continue; } extractSearchParam(nextSpDef, theResource, theExtractor, retVal, theWantLocalReferences); @@ -1673,6 +1680,31 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } + @SuppressWarnings("unchecked") + protected final T resolveResourceInBundleWithPlaceholderId(Object theAppContext, String theUrl) { + /* + * If this is a reference that is a UUID, we must be looking for local + * references within a Bundle + */ + if (theAppContext instanceof IBaseBundle && isNotBlank(theUrl) && !theUrl.startsWith("#")) { + List entries = BundleUtil.toListOfEntries(getContext(), (IBaseBundle) theAppContext); + for (BundleEntryParts next : entries) { + if (next.getResource() != null) { + if (theUrl.startsWith("urn:uuid:")) { + if (theUrl.equals(next.getUrl()) || theUrl.equals(next.getResource().getIdElement().getValue())) { + return (T) next.getResource(); + } + } else { + if (theUrl.equals(next.getResource().getIdElement().getValue())) { + return (T) next.getResource(); + } + } + } + } + } + return null; + } + @FunctionalInterface public interface IValueExtractor { @@ -1781,6 +1813,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Override public void extract(SearchParamSet theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) { if (theValue instanceof IBaseResource) { + myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, (IBaseResource) theValue); + theParams.add(myPathAndRef); return; } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java index 31e4f746b7d..7628770eabc 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public interface IResourceLinkResolver { @@ -35,16 +36,34 @@ public interface IResourceLinkResolver { * so that we can create indexed links between resources, and so that we can validate that the target actually * exists in cases where we need to check that. *

    - * This method returns an {@link IResourceLookup} so as to avoid needing to resolve the entire resource. + * This method returns an {@link IResourceLookup} to avoid needing to resolve the entire resource. * * @param theRequestPartitionId The partition ID of the target resource - * @param theSourceResourceName + * @param theSourceResourceName The resource type for the resource containing the reference * @param thePathAndRef The path and reference * @param theRequest The incoming request, if any - * @param theTransactionDetails + * @param theTransactionDetails The current TransactionDetails object */ IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, String theSourceResourceName, PathAndRef thePathAndRef, RequestDetails theRequest, TransactionDetails theTransactionDetails); + /** + * This method resolves the target of a reference found within a resource that is being created/updated. We do this + * so that we can create indexed links between resources, and so that we can validate that the target actually + * exists in cases where we need to check that. + *

    + * This method returns an {@link IResourceLookup} to avoid needing to resolve the entire resource. + * + * @param theRequestPartitionId The partition ID of the target resource + * @param theSourceResourceName The resource type for the resource containing the reference + * @param thePathAndRef The path and reference + * @param theRequest The incoming request, if any + * @param theTransactionDetails The current TransactionDetails object + */ + @Nullable + IBaseResource loadTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, String theSourceResourceName, PathAndRef thePathAndRef, RequestDetails theRequest, TransactionDetails theTransactionDetails); + + + void validateTypeOrThrowException(Class theType); } 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 cab5a806ea3..cfcbd2d0bc5 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 @@ -44,31 +44,66 @@ import java.util.Set; public interface ISearchParamExtractor { -// SearchParamSet extractSearchParamCoords(IBaseResource theResource); + /** + * Constant for the {@literal theParamsToIndex} parameters on this interface + * indicating that all search parameters should be indexed. + */ + Set ALL_PARAMS = Set.of("*"); - SearchParamSet extractSearchParamDates(IBaseResource theResource); + default SearchParamSet extractSearchParamDates(IBaseResource theResource) { + return extractSearchParamDates(theResource, ALL_PARAMS); + } - SearchParamSet extractSearchParamNumber(IBaseResource theResource); + SearchParamSet extractSearchParamDates(IBaseResource theResource, Set theParamsToIndex); - SearchParamSet extractSearchParamQuantity(IBaseResource theResource); + default SearchParamSet extractSearchParamNumber(IBaseResource theResource) { + return extractSearchParamNumber(theResource, ALL_PARAMS); + } - SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource); + SearchParamSet extractSearchParamNumber(IBaseResource theResource, Set theParamsToIndex); - SearchParamSet extractSearchParamStrings(IBaseResource theResource); + default SearchParamSet extractSearchParamQuantity(IBaseResource theResource) { + return extractSearchParamQuantity(theResource, ALL_PARAMS); + } - SearchParamSet extractSearchParamComposites(IBaseResource theResource); + SearchParamSet extractSearchParamQuantity(IBaseResource theResource, Set theParamsToIndex); - SearchParamSet extractSearchParamTokens(IBaseResource theResource); + default SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { + return extractSearchParamQuantityNormalized(theResource, ALL_PARAMS); + } + + SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource, Set theParamsToIndex); + + default SearchParamSet extractSearchParamStrings(IBaseResource theResource) { + return extractSearchParamStrings(theResource, ALL_PARAMS); + } + + SearchParamSet extractSearchParamStrings(IBaseResource theResource, Set theParamsToIndex); + + default SearchParamSet extractSearchParamComposites(IBaseResource theResource) { + return extractSearchParamComposites(theResource, ALL_PARAMS); + } + + SearchParamSet extractSearchParamComposites(IBaseResource theResource, Set theParamsToIndex); + default SearchParamSet extractSearchParamTokens(IBaseResource theResource) { + return extractSearchParamTokens(theResource, ALL_PARAMS); + } + + SearchParamSet extractSearchParamTokens(IBaseResource theResource, Set theParamsToIndex); SearchParamSet extractSearchParamTokens(IBaseResource theResource, RuntimeSearchParam theSearchParam); - SearchParamSet extractSearchParamSpecial(IBaseResource theResource); - + SearchParamSet extractSearchParamSpecial(IBaseResource theResource, Set theParamsToIndex); SearchParamSet extractSearchParamComboUnique(String theResourceType, ResourceIndexedSearchParams theParams); SearchParamSet extractSearchParamComboNonUnique(String theResourceType, ResourceIndexedSearchParams theParams); - SearchParamSet extractSearchParamUri(IBaseResource theResource); + default SearchParamSet extractSearchParamUri(IBaseResource theResource) { + return extractSearchParamUri(theResource, ALL_PARAMS); + } + + + SearchParamSet extractSearchParamUri(IBaseResource theResource, Set theParamsToIndex); SearchParamSet extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java index 92b84570db9..c735ee93a88 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java @@ -20,17 +20,21 @@ package ca.uhn.fhir.jpa.searchparam.extractor; * #L% */ +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; public class PathAndRef { private final String myPath; private final IBaseReference myRef; + private final IBaseResource myResource; private final String mySearchParamName; private final boolean myCanonical; /** - * Constructor + * Constructor for a reference */ public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef, boolean theCanonical) { super(); @@ -38,6 +42,31 @@ public class PathAndRef { myPath = thePath; myRef = theRef; myCanonical = theCanonical; + myResource = null; + } + + /** + * Constructor for a resource (this is expected to be rare, only really covering + * cases like the path Bundle.entry.resource) + */ + public PathAndRef(String theSearchParamName, String thePath, IBaseResource theResource) { + super(); + mySearchParamName = theSearchParamName; + myPath = thePath; + myRef = null; + myCanonical = false; + myResource = theResource; + } + + /** + * Note that this will generally be null, it is only used for cases like + * indexing {@literal Bundle.entry.resource}. If this is populated, {@link #getRef()} + * will be null and vice versa. + * + * @since 6.6.0 + */ + public IBaseResource getResource() { + return myResource; } public boolean isCanonical() { @@ -52,8 +81,23 @@ public class PathAndRef { return myPath; } + /** + * If this is populated, {@link #getResource()} will be null, and vice versa. + */ public IBaseReference getRef() { return myRef; } + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + b.append("paramName", mySearchParamName); + if (myRef != null && myRef.getReferenceElement() != null) { + b.append("ref", myRef.getReferenceElement().getValue()); + } + b.append("path", myPath); + b.append("resource", myResource); + b.append("canonical", myCanonical); + return b.toString(); + } } 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 1c5c905a59e..83df7c9e25e 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 @@ -133,15 +133,15 @@ public final class ResourceIndexedSearchParams { theEntity.setResourceLinks(myLinks); } - public void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, String theSpnamePrefix) { - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myNumberParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityNormalizedParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myDateParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myUriParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myTokenParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myStringParams, theSpnamePrefix); - updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myCoordsParams, theSpnamePrefix); + public void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, String theSpnamePrefix) { + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myNumberParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityNormalizedParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myDateParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myUriParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myTokenParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myStringParams, theSpnamePrefix); + updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myCoordsParams, theSpnamePrefix); } public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) { @@ -176,7 +176,7 @@ public final class ResourceIndexedSearchParams { } } - private void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, Collection theParams, @Nonnull String theSpnamePrefix) { + private void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, Collection theParams, @Nonnull String theSpnamePrefix) { for (BaseResourceIndexedSearchParam param : theParams) { param.setResourceType(theContainingType); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java index f812b2b4200..2b15406115d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java @@ -75,7 +75,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { return () -> { ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); - return myFhirPathEngine.evaluate((Base) theResource, parsed); + return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed); }; } @@ -98,13 +98,13 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements } - private static class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext { + private class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext { private final Map myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); @Override public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { - return null; + return Collections.emptyList(); } @Override @@ -135,6 +135,10 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements @Override public Base resolveReference(Object theAppContext, String theUrl, Base theRefContext) throws FHIRException { + Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl); + if (retVal != null) { + return retVal; + } /* * When we're doing resolution within the SearchParamExtractor, if we want @@ -144,7 +148,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements * Encounter.patient.where(resolve() is Patient) */ IdType url = new IdType(theUrl); - Base retVal = null; if (isNotBlank(url.getResourceType())) { retVal = myResourceTypeToStub.get(url.getResourceType()); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java index cac4ac93656..ae333e71e8c 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java @@ -74,7 +74,7 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { return () -> { ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); - return myFhirPathEngine.evaluate((Base) theResource, parsed); + return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed); }; } @@ -97,13 +97,13 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements } - private static class SearchParamExtractorR4BHostServices implements FHIRPathEngine.IEvaluationContext { + private class SearchParamExtractorR4BHostServices implements FHIRPathEngine.IEvaluationContext { private final Map myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); @Override public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { - return null; + return Collections.emptyList(); } @Override @@ -134,6 +134,10 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements @Override public Base resolveReference(Object theAppContext, String theUrl, Base refContext) throws FHIRException { + Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl); + if (retVal != null) { + return retVal; + } /* * When we're doing resolution within the SearchParamExtractor, if we want @@ -143,7 +147,6 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements * Encounter.patient.where(resolve() is Patient) */ IdType url = new IdType(theUrl); - Base retVal = null; if (isNotBlank(url.getResourceType())) { retVal = myResourceTypeToStub.get(url.getResourceType()); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java index fd680f004f1..5df87cc9d5f 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java @@ -26,9 +26,12 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.bundle.BundleEntryParts; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.model.Base; @@ -88,18 +91,18 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { return () -> { ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); - return myFhirPathEngine.evaluate((Base) theResource, parsed); + return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed); }; } - private static class SearchParamExtractorR5HostServices implements FHIRPathEngine.IEvaluationContext { + private class SearchParamExtractorR5HostServices implements FHIRPathEngine.IEvaluationContext { private final Map myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); @Override public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { - return null; + return Collections.emptyList(); } @Override @@ -129,6 +132,10 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements @Override public Base resolveReference(Object appContext, String theUrl, Base refContext) throws FHIRException { + Base retVal = resolveResourceInBundleWithPlaceholderId(appContext, theUrl); + if (retVal != null) { + return retVal; + } /* * When we're doing resolution within the SearchParamExtractor, if we want @@ -138,7 +145,6 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements * Encounter.patient.where(resolve() is Patient) */ IdType url = new IdType(theUrl); - Base retVal = null; if (isNotBlank(url.getResourceType())) { retVal = myResourceTypeToStub.get(url.getResourceType()); @@ -185,5 +191,4 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements } - } 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 8bdafc19604..bd2bf8e593e 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 @@ -35,7 +35,6 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.BasePartitionable; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter; -import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; @@ -49,6 +48,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.parser.DataFormatException; @@ -67,6 +67,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Date; @@ -117,12 +118,22 @@ public class SearchParamExtractorService { // All search parameter types except Reference ResourceIndexedSearchParams normalParams = new ResourceIndexedSearchParams(); - extractSearchIndexParameters(theRequestDetails, normalParams, theResource); + extractSearchIndexParameters(theRequestDetails, normalParams, theResource, ISearchParamExtractor.ALL_PARAMS); mergeParams(normalParams, theNewParams); - if (myStorageSettings.isIndexOnContainedResources()) { + boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); + ISearchParamExtractor.SearchParamSet indexedReferences = mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources); + SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences); + + if (indexOnContainedResources) { ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams(); - extractSearchIndexParametersForContainedResources(theRequestDetails, containedParams, theResource, theEntity); + extractSearchIndexParametersForContainedResources(theRequestDetails, containedParams, theResource, theEntity, indexedReferences); + mergeParams(containedParams, theNewParams); + } + + if (myStorageSettings.isIndexOnUpliftedRefchains()) { + ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams(); + extractSearchIndexParametersForUpliftedRefchains(theRequestDetails, containedParams, theEntity, theRequestPartitionId, theTransactionDetails, indexedReferences); mergeParams(containedParams, theNewParams); } @@ -130,9 +141,9 @@ public class SearchParamExtractorService { populateResourceTables(theNewParams, theEntity); // Reference search parameters - extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails); + extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails, indexedReferences); - if (myStorageSettings.isIndexOnContainedResources()) { + if (indexOnContainedResources) { extractResourceLinksForContainedResources(theRequestPartitionId, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails); } @@ -144,57 +155,156 @@ public class SearchParamExtractorService { myStorageSettings = theStorageSettings; } - private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) { + /** + * Extract search parameter indexes for contained resources. E.g. if we + * are storing a Patient with a contained Organization, we might extract + * a String index on the Patient with paramName="organization.name" and + * value="Org Name" + */ + private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, ISearchParamExtractor.SearchParamSet theIndexedReferences) { FhirTerser terser = myContext.newTerser(); // 1. get all contained resources Collection containedResources = terser.getAllEmbeddedResources(theResource, false); - extractSearchIndexParametersForContainedResources(theRequestDetails, theParams, theResource, theEntity, containedResources, new HashSet<>()); + // Extract search parameters + IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() { + @Override + public Set getChainedSearchParametersToIndexForPath(PathAndRef thePathAndRef) { + // Currently for contained resources we always index all search parameters + // on all contained resources. A potential nice future optimization would + // be to make this configurable, perhaps with an optional extension you could + // add to a SearchParameter? + return ISearchParamExtractor.ALL_PARAMS; + } + + @Override + public IBaseResource fetchResourceAtPath(PathAndRef thePathAndRef) { + return findContainedResource(containedResources, thePathAndRef.getRef()); + } + }; + boolean recurse = myStorageSettings.isIndexOnContainedResourcesRecursively(); + extractSearchIndexParametersForTargetResources(theRequestDetails, theParams, theEntity, new HashSet<>(), strategy, theIndexedReferences, recurse, true); } - private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, Collection theContainedResources, Collection theAlreadySeenResources) { + /** + * Extract search parameter indexes for uplifted refchains. E.g. if we + * are storing a Patient with reference to an Organization and the + * "Patient:organization" SearchParameter declares an uplifted refchain + * on the "name" SearchParameter, we might extract a String index + * on the Patient with paramName="organization.name" and value="Org Name" + */ + private void extractSearchIndexParametersForUpliftedRefchains(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, ResourceTable theEntity, RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails, ISearchParamExtractor.SearchParamSet theIndexedReferences) { + IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() { + + @Override + public Set getChainedSearchParametersToIndexForPath(PathAndRef thePathAndRef) { + String searchParamName = thePathAndRef.getSearchParamName(); + RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theEntity.getResourceType(), searchParamName); + return searchParam.getUpliftRefchainCodes(); + } + + @Override + public IBaseResource fetchResourceAtPath(PathAndRef thePathAndRef) { + // The PathAndRef will contain a resource if the SP path was inside a Bundle + // and pointed to a resource (e.g. Bundle.entry.resource) as opposed to + // pointing to a reference (e.g. Observation.subject) + if (thePathAndRef.getResource() != null) { + return thePathAndRef.getResource(); + } + + // Ok, it's a normal reference + IIdType reference = thePathAndRef.getRef().getReferenceElement(); + + // If we're processing a FHIR transaction, we store the resources + // mapped by their resolved resource IDs in theTransactionDetails + IBaseResource resolvedResource = theTransactionDetails.getResolvedResource(reference); + + // And the usual case is that the reference points to a resource + // elsewhere in the repository, so we load it + if (resolvedResource == null && myResourceLinkResolver != null && !reference.getValue().startsWith("urn:uuid:")) { + RequestPartitionId targetRequestPartitionId = determineResolverPartitionId(theRequestPartitionId); + resolvedResource = myResourceLinkResolver.loadTargetResource(targetRequestPartitionId, theEntity.getResourceType(), thePathAndRef, theRequestDetails, theTransactionDetails); + if (resolvedResource != null) { + ourLog.trace("Found target: {}", resolvedResource.getIdElement()); + theTransactionDetails.addResolvedResource(thePathAndRef.getRef().getReferenceElement(), resolvedResource); + } + } + + return resolvedResource; + } + }; + extractSearchIndexParametersForTargetResources(theRequestDetails, theParams, theEntity, new HashSet<>(), strategy, theIndexedReferences, false, false); + } + + /** + * Extract indexes for contained references as well as for uplifted refchains. + * These two types of indexes are both similar special cases. Normally we handle + * chained searches ("Patient?organization.name=Foo") using a join from the + * {@link ResourceLink} table (for the "organization" part) to the + * {@link ResourceIndexedSearchParamString} table (for the "name" part). But + * for both contained resource indexes and uplifted refchains we use only the + * {@link ResourceIndexedSearchParamString} table to handle the entire + * "organization.name" part, or the other similar tables for token, number, etc. + * + * @see #extractSearchIndexParametersForContainedResources(RequestDetails, ResourceIndexedSearchParams, IBaseResource, ResourceTable, ISearchParamExtractor.SearchParamSet) + * @see #extractSearchIndexParametersForUpliftedRefchains(RequestDetails, ResourceIndexedSearchParams, ResourceTable, RequestPartitionId, TransactionDetails, ISearchParamExtractor.SearchParamSet) + */ + private void extractSearchIndexParametersForTargetResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, ResourceTable theEntity, Collection theAlreadySeenResources, IChainedSearchParameterExtractionStrategy theTargetIndexingStrategy, ISearchParamExtractor.SearchParamSet theIndexedReferences, boolean theRecurse, boolean theIndexOnContainedResources) { // 2. Find referenced search parameters - ISearchParamExtractor.SearchParamSet referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true); String spnamePrefix; - ResourceIndexedSearchParams currParams; // 3. for each referenced search parameter, create an index - for (PathAndRef nextPathAndRef : referencedSearchParamSet) { + for (PathAndRef nextPathAndRef : theIndexedReferences) { // 3.1 get the search parameter name as spname prefix spnamePrefix = nextPathAndRef.getSearchParamName(); - if (spnamePrefix == null || nextPathAndRef.getRef() == null) + if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null)) continue; - // 3.2 find the contained resource - IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef()); - if (containedResource == null) - continue; - - // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops - if (theAlreadySeenResources.contains(containedResource)) { + // 3.1.2 check if this ref actually applies here + Set searchParamsToIndex = theTargetIndexingStrategy.getChainedSearchParametersToIndexForPath(nextPathAndRef); + if (searchParamsToIndex.isEmpty()) { continue; } - currParams = new ResourceIndexedSearchParams(); + // 3.2 find the target resource + IBaseResource targetResource = theTargetIndexingStrategy.fetchResourceAtPath(nextPathAndRef); + if (targetResource == null) + continue; + + // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops + if (theAlreadySeenResources.contains(targetResource)) { + continue; + } + + ResourceIndexedSearchParams currParams = new ResourceIndexedSearchParams(); // 3.3 create indexes for the current contained resource - extractSearchIndexParameters(theRequestDetails, currParams, containedResource); + extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex); // 3.4 recurse to process any other contained resources referenced by this one - if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { + // Recursing is currently only allowed for contained resources and not + // uplifted refchains because the latter could potentially kill performance + // with the number of resource resolutions needed in order to handle + // a single write. Maybe in the future we could add caching to improve + // this + if (theRecurse) { HashSet nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); - nextAlreadySeenResources.add(containedResource); - extractSearchIndexParametersForContainedResources(theRequestDetails, currParams, containedResource, theEntity, theContainedResources, nextAlreadySeenResources); + nextAlreadySeenResources.add(targetResource); + + ISearchParamExtractor.SearchParamSet indexedReferences = mySearchParamExtractor.extractResourceLinks(targetResource, theIndexOnContainedResources); + SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences); + + extractSearchIndexParametersForTargetResources(theRequestDetails, currParams, theEntity, nextAlreadySeenResources, theTargetIndexingStrategy, indexedReferences, true, theIndexOnContainedResources); } // 3.5 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(theEntity.getResourceType(), spnamePrefix); + currParams.updateSpnamePrefixForIndexOnUpliftedChain(theEntity.getResourceType(), nextPathAndRef.getSearchParamName()); // 3.6 merge to the mainParams // NOTE: the spname prefix is different @@ -223,42 +333,42 @@ public class SearchParamExtractorService { theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams); } - void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource) { + void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, Set theParamsToIndex) { // Strings - ISearchParamExtractor.SearchParamSet strings = extractSearchParamStrings(theResource); + ISearchParamExtractor.SearchParamSet strings = extractSearchParamStrings(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings); theParams.myStringParams.addAll(strings); // Numbers - ISearchParamExtractor.SearchParamSet numbers = extractSearchParamNumber(theResource); + ISearchParamExtractor.SearchParamSet numbers = extractSearchParamNumber(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers); theParams.myNumberParams.addAll(numbers); // Quantities - ISearchParamExtractor.SearchParamSet quantities = extractSearchParamQuantity(theResource); + ISearchParamExtractor.SearchParamSet quantities = extractSearchParamQuantity(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities); theParams.myQuantityParams.addAll(quantities); if (myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED) || myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) { - ISearchParamExtractor.SearchParamSet quantitiesNormalized = extractSearchParamQuantityNormalized(theResource); + ISearchParamExtractor.SearchParamSet quantitiesNormalized = extractSearchParamQuantityNormalized(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized); theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized); } // Dates - ISearchParamExtractor.SearchParamSet dates = extractSearchParamDates(theResource); + ISearchParamExtractor.SearchParamSet dates = extractSearchParamDates(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates); theParams.myDateParams.addAll(dates); // URIs - ISearchParamExtractor.SearchParamSet uris = extractSearchParamUri(theResource); + ISearchParamExtractor.SearchParamSet uris = extractSearchParamUri(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris); theParams.myUriParams.addAll(uris); // Tokens (can result in both Token and String, as we index the display name for // the types: Coding, CodeableConcept) - ISearchParamExtractor.SearchParamSet tokens = extractSearchParamTokens(theResource); + ISearchParamExtractor.SearchParamSet tokens = extractSearchParamTokens(theResource, theParamsToIndex); for (BaseResourceIndexedSearchParam next : tokens) { if (next instanceof ResourceIndexedSearchParamToken) { theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next); @@ -272,13 +382,13 @@ public class SearchParamExtractorService { // Composites // dst2 composites use stuff like value[x] , and we don't support them. if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { - ISearchParamExtractor.SearchParamSet composites = extractSearchParamComposites(theResource); + ISearchParamExtractor.SearchParamSet composites = extractSearchParamComposites(theResource, theParamsToIndex); handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites); theParams.myCompositeParams.addAll(composites); } // Specials - ISearchParamExtractor.SearchParamSet specials = extractSearchParamSpecial(theResource); + ISearchParamExtractor.SearchParamSet specials = extractSearchParamSpecial(theResource, theParamsToIndex); for (BaseResourceIndexedSearchParam next : specials) { if (next instanceof ResourceIndexedSearchParamCoords) { theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next); @@ -304,19 +414,22 @@ public class SearchParamExtractorService { myContext = theContext; } - private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) { - extractResourceLinks(theRequestPartitionId, new ResourceIndexedSearchParams(), theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequest); + private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest, ISearchParamExtractor.SearchParamSet theIndexedReferences) { + extractResourceLinks(theRequestPartitionId, new ResourceIndexedSearchParams(), theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequest, theIndexedReferences); } - private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theExistingParams, ResourceIndexedSearchParams theNewParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) { + private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theExistingParams, ResourceIndexedSearchParams theNewParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest, ISearchParamExtractor.SearchParamSet theIndexedReferences) { String sourceResourceName = myContext.getResourceType(theResource); - ISearchParamExtractor.SearchParamSet refs = mySearchParamExtractor.extractResourceLinks(theResource, false); - SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs); + for (PathAndRef nextPathAndRef : theIndexedReferences) { + if (nextPathAndRef.getRef() != null) { + if (nextPathAndRef.getRef().getReferenceElement().isLocal()) { + continue; + } - for (PathAndRef nextPathAndRef : refs) { - RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(sourceResourceName, nextPathAndRef.getSearchParamName()); - extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theTransactionDetails, sourceResourceName, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest); + RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(sourceResourceName, nextPathAndRef.getSearchParamName()); + extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theTransactionDetails, sourceResourceName, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest); + } } theEntity.setHasLinks(theNewParams.myLinks.size() > 0); @@ -531,7 +644,8 @@ public class SearchParamExtractorService { currParams = new ResourceIndexedSearchParams(); // 3.3 create indexes for the current contained resource - extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest); + ISearchParamExtractor.SearchParamSet indexedReferences = mySearchParamExtractor.extractResourceLinks(containedResource, true); + extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest, indexedReferences); // 3.4 recurse to process any other contained resources referenced by this one if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { @@ -602,6 +716,14 @@ public class SearchParamExtractorService { return ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion); } + private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) { + RequestPartitionId targetRequestPartitionId = theRequestPartitionId; + if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.getAllowReferencesAcrossPartitions() == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) { + targetRequestPartitionId = RequestPartitionId.allPartitions(); + } + return targetRequestPartitionId; + } + private void populateResourceTable(Collection theParams, ResourceTable theResourceTable) { for (BaseResourceIndexedSearchParam next : theParams) { if (next.getResourcePid() == null) { @@ -621,43 +743,42 @@ public class SearchParamExtractorService { } } - private ISearchParamExtractor.SearchParamSet extractSearchParamDates(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamDates(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamDates(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamDates(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamNumber(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamNumber(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamNumber(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamNumber(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamQuantity(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamQuantity(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamQuantity(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamQuantity(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamStrings(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamStrings(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamStrings(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamStrings(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamTokens(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamTokens(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamTokens(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamTokens(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamSpecial(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamSpecial(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamSpecial(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamSpecial(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamUri(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamUri(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamUri(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamUri(theResource, theParamsToIndex); } - private ISearchParamExtractor.SearchParamSet extractSearchParamComposites(IBaseResource theResource) { - return mySearchParamExtractor.extractSearchParamComposites(theResource); + private ISearchParamExtractor.SearchParamSet extractSearchParamComposites(IBaseResource theResource, Set theParamsToIndex) { + return mySearchParamExtractor.extractSearchParamComposites(theResource, theParamsToIndex); } - @VisibleForTesting void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) { myInterceptorBroadcaster = theInterceptorBroadcaster; @@ -682,6 +803,32 @@ public class SearchParamExtractorService { populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity); } + /** + * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)} + * in order to use that method for extracting chained search parameter indexes both + * from contained resources and from uplifted refchains. + */ + private interface IChainedSearchParameterExtractionStrategy { + + /** + * Which search parameters should be indexed for the resource target + * at the given path. In other words if thePathAndRef contains + * "Patient/123", then we could return a Set containing "name" and "gender" + * if we only want those two parameters to be indexed for the + * resolved Patient resource with that ID. + */ + @Nonnull + Set getChainedSearchParametersToIndexForPath(@Nonnull PathAndRef thePathAndRef); + + /** + * Actually fetch the resource at the given path, or return + * {@literal null} if none can be found. + */ + @Nullable + IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef); + + } + static void handleWarnings(RequestDetails theRequestDetails, IInterceptorBroadcaster theInterceptorBroadcaster, ISearchParamExtractor.SearchParamSet theSearchParamSet) { if (theSearchParamSet.getWarnings().isEmpty()) { return; diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java index 30e6cda1d84..7e46b06ffc8 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.DatatypeUtil; +import ca.uhn.fhir.util.ExtensionUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.PhoneticEncoderUtil; @@ -362,19 +363,30 @@ public class SearchParameterCanonicalizer { */ protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) { if (theSearchParamResource instanceof IBaseHasExtensions) { - List> extensions = ((IBaseHasExtensions) theSearchParamResource).getExtension(); - for (IBaseExtension next : extensions) { + List> extensions = (List>) ((IBaseHasExtensions) theSearchParamResource).getExtension(); + for (IBaseExtension next : extensions) { String nextUrl = next.getUrl(); if (isNotBlank(nextUrl)) { theRuntimeSearchParam.addExtension(nextUrl, next); if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) { setEncoder(theRuntimeSearchParam, next.getValue()); + } else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) { + addUpliftRefchain(theRuntimeSearchParam, next); } } } } } + @SuppressWarnings("unchecked") + private void addUpliftRefchain(RuntimeSearchParam theRuntimeSearchParam, IBaseExtension theExtension) { + String code = ExtensionUtil.extractChildPrimitiveExtensionValue(theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE); + String elementName = ExtensionUtil.extractChildPrimitiveExtensionValue(theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME); + if (isNotBlank(code)) { + theRuntimeSearchParam.addUpliftRefchain(code, elementName); + } + } + private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) { if (theValue instanceof IPrimitiveType) { String stringValue = ((IPrimitiveType) theValue).getValueAsString(); diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRefTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRefTest.java new file mode 100644 index 00000000000..702d81a0e3f --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRefTest.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PathAndRefTest { + + @Test + public void testToString() { + PathAndRef ref = new PathAndRef("foo", "Foo.bar", new Reference("Patient/123"), false); + assertEquals("PathAndRef[paramName=foo,ref=Patient/123,path=Foo.bar,resource=,canonical=false]", ref.toString()); + } + +} diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index 1381d5971ea..e92fd7f5b7f 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -24,11 +24,14 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ResourceSearchParams; +import ca.uhn.fhir.util.HapiExtensions; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Extension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,9 +42,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.shaded.com.google.common.collect.Sets; import javax.annotation.Nonnull; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -338,6 +343,38 @@ public class SearchParamRegistryImplTest { assertEquals("FOO", value.getValueAsString()); } + @Test + public void testUpliftRefchains() { + SearchParameter sp = new SearchParameter(); + Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("name1")); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME, new StringType("element1")); + Extension upliftRefChain2 = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain2.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("name2")); + sp.setCode("subject"); + sp.setName("subject"); + sp.setDescription("Modified Subject"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setExpression("Encounter.subject"); + sp.addBase("Encounter"); + sp.addTarget("Patient"); + + ArrayList newEntities = new ArrayList<>(ourEntities); + newEntities.add(createEntity(99, 1)); + ResourceVersionMap newResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(newEntities); + when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(newResourceVersionMap); + when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider(sp)); + + mySearchParamRegistry.forceRefresh(); + + RuntimeSearchParam canonicalSp = mySearchParamRegistry.getRuntimeSearchParam("Encounter", "subject"); + assertEquals("Modified Subject", canonicalSp.getDescription()); + assertTrue(canonicalSp.hasUpliftRefchain("name1")); + assertFalse(canonicalSp.hasUpliftRefchain("name99")); + assertEquals(Sets.newHashSet("name1", "name2"), canonicalSp.getUpliftRefchainCodes()); + } + private List resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) { // Add a new search parameter entity List newEntities = new ArrayList(ourEntities); diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 52f8f49978e..9499987dc63 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index ede03eddd02..2c45137f444 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index 97ac732d34d..25eb94536ee 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 4e39779e04d..dd53b28ac7b 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java index 77733dc6a19..042622b574c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java @@ -69,6 +69,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { myStorageSettings.setAllowMultipleDelete(true); myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds()); myStorageSettings.setReuseCachedSearchResultsForMillis(null); + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); } @Test @@ -264,6 +265,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { } String url = "/AuditEvent?patient.name=Smith"; + logAllStringIndexes(); // execute myCaptureQueriesListener.clear(); @@ -868,6 +870,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { } String url = "/Observation?subject.organization.name=HealthCo"; + logAllStringIndexes(); // execute myCaptureQueriesListener.clear(); @@ -1527,7 +1530,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 7); // If a reference in the chain has multiple potential target resource types, the number of subselects increases - countUnionStatementsInGeneratedQuery("/Observation?subject.name=Smith", 3); + // Note: This previously had 3 unions but 2 of the selects within were duplicates of each other + countUnionStatementsInGeneratedQuery("/Observation?subject.name=Smith", 2); // If such a reference if qualified to restrict the type, the number goes back down countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 6cc69a71556..f28fec1fb98 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -83,6 +83,7 @@ import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Communication; import org.hl7.fhir.r4.model.CommunicationRequest; +import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; import org.hl7.fhir.r4.model.DateTimeType; @@ -137,6 +138,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.springframework.beans.factory.annotation.Autowired; @@ -5733,6 +5736,79 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } } + /** + * Index for + * [base]/Bundle?composition.patient.identifier=foo + */ + @ParameterizedTest + @CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) + public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { + // Setup 1 + + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); + + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/Bundle-composition-patient-identifier"); + sp.setCode("composition.patient.identifier"); + sp.setName("composition.patient.identifier"); + sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); + sp.addBase("Bundle"); + ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp, mySrd); + + mySearchParamRegistry.forceRefresh(); + + // Test 1 + + Composition composition = new Composition(); + composition.setSubject(new Reference(thePatientId)); + + Patient patient = new Patient(); + patient.setId(new IdType(thePatientId)); + patient.addIdentifier().setSystem("http://foo").setValue("bar"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + bundle.addEntry().setResource(patient); + + myBundleDao.create(bundle, mySrd); + + Bundle bundle2 = new Bundle(); + bundle2.setType(Bundle.BundleType.DOCUMENT); + myBundleDao.create(bundle2, mySrd); + + // Verify 1 + runInTransaction(() -> { + logAllTokenIndexes(); + + List params = myResourceIndexedSearchParamTokenDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "composition.patient.identifier http://foo|bar" + )); + }); + + // Test 2 + IBundleProvider outcome; + + SearchParameterMap map = SearchParameterMap + .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + + map = SearchParameterMap + .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + } @Nested public class TagBelowTests { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantBatchOperationR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantBatchOperationR4Test.java index 9a3e22b1e2f..17cd6cdc530 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantBatchOperationR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantBatchOperationR4Test.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.system.HapiSystemProperties; import org.hamcrest.MatcherAssert; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.hapi.rest.server.helper.BatchHelperR4; @@ -53,6 +54,8 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv myReindexParameterCache = myStorageSettings.isMarkResourcesForReindexingUponSearchParameterChange(); myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false); + + HapiSystemProperties.enableUnitTestMode(); } @BeforeEach @@ -177,7 +180,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv List alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient); // Only the one in the first tenant should be indexed myTenantClientInterceptor.setTenantId(TENANT_A); - await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); + assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0)); myTenantClientInterceptor.setTenantId(TENANT_B); MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0)); @@ -210,7 +213,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv }); myTenantClientInterceptor.setTenantId(DEFAULT_PARTITION_NAME); - await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); + assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); } @Test @@ -258,7 +261,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv List alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient); // Only the one in the first tenant should be indexed myTenantClientInterceptor.setTenantId(TENANT_A); - MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); + assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0)); myTenantClientInterceptor.setTenantId(TENANT_B); MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0)); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index f4d0677b4b9..ae178b3ac25 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -59,6 +59,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.time.DateUtils; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; @@ -8166,6 +8168,15 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { myIsMissing = theIsMissing; myIsValuePresentOnResource = theHasField; } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE) + .append("valuePresent", myIsValuePresentOnResource) + .append("isMissing", myIsMissing) + .append("enableMissingFields", myEnableMissingFieldsValue) + .toString(); + } } /** diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index 783f7a794f6..3c0c389eafb 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/dao/r4b/FhirResourceDaoR4BSearchNoFtTest.java b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/dao/r4b/FhirResourceDaoR4BSearchNoFtTest.java new file mode 100644 index 00000000000..0c158cdea83 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/dao/r4b/FhirResourceDaoR4BSearchNoFtTest.java @@ -0,0 +1,100 @@ +package ca.uhn.fhir.jpa.dao.r4b; + +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.TokenParam; +import org.hl7.fhir.r4b.model.Bundle; +import org.hl7.fhir.r4b.model.Composition; +import org.hl7.fhir.r4b.model.Enumerations; +import org.hl7.fhir.r4b.model.IdType; +import org.hl7.fhir.r4b.model.Patient; +import org.hl7.fhir.r4b.model.Reference; +import org.hl7.fhir.r4b.model.SearchParameter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FhirResourceDaoR4BSearchNoFtTest extends BaseJpaR4BTest { + + /** + * Index for + * [base]/Bundle?composition.patient.identifier=foo + */ + @ParameterizedTest + @ValueSource(strings = {"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) + public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { + // Setup 1 + + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); + + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/Bundle-composition-patient-identifier"); + sp.setCode("composition.patient.identifier"); + sp.setName("composition.patient.identifier"); + sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); + sp.addBase("Bundle"); + ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp, mySrd); + + mySearchParamRegistry.forceRefresh(); + + // Test 1 + + Composition composition = new Composition(); + composition.setSubject(new Reference(thePatientId)); + + Patient patient = new Patient(); + patient.setId(new IdType(thePatientId)); + patient.addIdentifier().setSystem("http://foo").setValue("bar"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + bundle.addEntry().setResource(patient); + + myBundleDao.create(bundle, mySrd); + + Bundle bundle2 = new Bundle(); + bundle2.setType(Bundle.BundleType.DOCUMENT); + myBundleDao.create(bundle2, mySrd); + + // Verify 1 + runInTransaction(() -> { + logAllTokenIndexes(); + + List params = myResourceIndexedSearchParamTokenDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "composition.patient.identifier http://foo|bar" + )); + }); + + // Test 2 + IBundleProvider outcome; + + SearchParameterMap map = SearchParameterMap + .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + + map = SearchParameterMap + .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + } + +} diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index da3f63f5012..7993d6a802b 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java index 2be9413f65a..7f26b55b81f 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.r5; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig; @@ -11,16 +12,24 @@ import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import org.hamcrest.Matchers; +import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.ClinicalUseDefinition; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.Composition; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.ObservationDefinition; import org.hl7.fhir.r5.model.Organization; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Practitioner; import org.hl7.fhir.r5.model.PractitionerRole; import org.hl7.fhir.r5.model.Reference; +import org.hl7.fhir.r5.model.SearchParameter; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.test.context.ContextConfiguration; import java.util.Date; @@ -28,6 +37,7 @@ import java.util.List; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; @ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class) @@ -35,6 +45,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class); + @AfterEach + public void after() { + myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields()); + } + @Test public void testHasWithTargetReference() { Organization org = new Organization(); @@ -221,4 +236,79 @@ public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test { } + /** + * Index for + * [base]/Bundle?composition.patient.identifier=foo + */ + @ParameterizedTest + @CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) + public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { + // Setup 1 + + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); + + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/Bundle-composition-patient-identifier"); + sp.setCode("composition.patient.identifier"); + sp.setName("composition.patient.identifier"); + sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); + sp.addBase("Bundle"); + ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp, mySrd); + + mySearchParamRegistry.forceRefresh(); + + // Test 1 + + Composition composition = new Composition(); + composition.addSubject().setReference(thePatientId); + + Patient patient = new Patient(); + patient.setId(new IdType(thePatientId)); + patient.addIdentifier().setSystem("http://foo").setValue("bar"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + bundle.addEntry().setResource(patient); + + myBundleDao.create(bundle, mySrd); + + Bundle bundle2 = new Bundle(); + bundle2.setType(Bundle.BundleType.DOCUMENT); + myBundleDao.create(bundle2, mySrd); + + // Verify 1 + runInTransaction(() -> { + logAllTokenIndexes(); + + List params = myResourceIndexedSearchParamTokenDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "composition.patient.identifier http://foo|bar" + )); + }); + + // Test 2 + IBundleProvider outcome; + + SearchParameterMap map = SearchParameterMap + .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + + map = SearchParameterMap + .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + } + + } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java new file mode 100644 index 00000000000..a264c0b14b3 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/UpliftedRefchainsAndChainedSortingR5Test.java @@ -0,0 +1,1111 @@ +package ca.uhn.fhir.jpa.dao.r5; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.HapiExtensions; +import org.hl7.fhir.r5.model.Composition; +import org.hl7.fhir.r5.model.IdType; +import org.hl7.fhir.r5.model.Bundle; +import org.hl7.fhir.r5.model.CodeType; +import org.hl7.fhir.r5.model.DateType; +import org.hl7.fhir.r5.model.Encounter; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.Extension; +import org.hl7.fhir.r5.model.Identifier; +import org.hl7.fhir.r5.model.Organization; +import org.hl7.fhir.r5.model.Patient; +import org.hl7.fhir.r5.model.Practitioner; +import org.hl7.fhir.r5.model.Reference; +import org.hl7.fhir.r5.model.SearchParameter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ContextConfiguration; + +import javax.annotation.Nonnull; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.countMatches; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class) +@SuppressWarnings({"Duplicates"}) +public class UpliftedRefchainsAndChainedSortingR5Test extends BaseJpaR5Test { + public static final String PRACTITIONER_PR1 = "Practitioner/PR1"; + public static final String ENCOUNTER_E1 = "Encounter/E1"; + public static final String PATIENT_P1 = "Patient/P1"; + public static final String PATIENT_P2 = "Patient/P2"; + public static final String PATIENT_P3 = "Patient/P3"; + public static final String ENCOUNTER_E2 = "Encounter/E2"; + public static final String ENCOUNTER_E3 = "Encounter/E3"; + public static final String ORGANIZATION_O1 = "Organization/O1"; + + @Override + @BeforeEach + public void beforeResetConfig() { + super.beforeResetConfig(); + + myStorageSettings.setIndexOnUpliftedRefchains(true); + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); + } + + @Override + @AfterEach + public void afterCleanupDao() { + super.afterCleanupDao(); + myStorageSettings.setIndexOnUpliftedRefchains(new StorageSettings().isIndexOnUpliftedRefchains()); + myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields()); + } + + @Test + public void testCreate() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + + // Test + + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + + // Verify + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + @Test + public void testCreate_NoUpliftRefchainsDefined() { + // Setup + + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + + // Test + + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + + // Verify + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, empty()); + }); + } + + /** + * Index for + * [base]/Bundle?composition.type=foo + * Using an uplifted refchain on the Bundle:composition SearchParameter + */ + @Test + public void testCreate_BundleWithComposition_UsingSimpleUplift() { + // Setup + + RuntimeSearchParam subjectSp = mySearchParamRegistry.getRuntimeSearchParam("Bundle", "composition"); + SearchParameter sp = new SearchParameter(); + Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("type")); + + sp.setId(subjectSp.getId()); + sp.setCode(subjectSp.getName()); + sp.setName(subjectSp.getName()); + sp.setUrl(subjectSp.getUri()); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setExpression("Bundle.entry[0].resource.as(Composition)"); + subjectSp.getBase().forEach(sp::addBase); + subjectSp.getTargets().forEach(sp::addTarget); + ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp, mySrd); + + mySearchParamRegistry.forceRefresh(); + + // Test + + Composition composition = new Composition(); + composition.getType().addCoding().setSystem("http://foo").setCode("bar"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + myBundleDao.create(bundle, mySrd); + + // Verify + runInTransaction(() -> { + logAllTokenIndexes(); + + List params = myResourceIndexedSearchParamTokenDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "composition.type http://foo|bar" + )); + }); + } + + @Test + public void testCreate_InvalidTarget() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + // Test + try { + createEncounter(ENCOUNTER_E1, PATIENT_P1); + fail(); + } catch (ResourceNotFoundException e) { + + // Verify + assertThat(e.getMessage(), containsString("Resource Patient/P1 is not known")); + } + + } + + @Test + public void testCreate_InTransaction_InvalidPlaceholderReferenceTarget() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + + // Test + + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, IdType.newRandomUuid().getValue())); + Bundle requestBundle = bb.getBundleTyped(); + + try { + mySystemDao.transaction(mySrd, requestBundle); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Unable to satisfy placeholder ID")); + } + + } + + + @Test + public void testCreate_InTransaction_SourceAndTarget() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + + // Test + + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newPatientP1_HomerSimpson()); + bb.addTransactionUpdateEntry(newPatientP2_MargeSimpson()); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, PATIENT_P1)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, PATIENT_P2)); + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + // 1- Resolve resource forced IDs, and 2- Resolve Practitioner/PR1 reference + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + @Test + public void testCreate_InTransaction_TargetCreated_RefsUsePlaceholderIds() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + + // Test + + String p1Id = IdType.newRandomUuid().getValue(); + String p2Id = IdType.newRandomUuid().getValue(); + + // Put the creates second to ensure that order doesn't matter + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, p1Id)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, p2Id)); + bb.addTransactionCreateEntry(newPatientP1_HomerSimpson().setId(p1Id)); + bb.addTransactionCreateEntry(newPatientP2_MargeSimpson().setId(p2Id)); + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + // 1- Resolve resource forced IDs, and 2- Resolve Practitioner/PR1 reference + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + + @Test + public void testCreate_InTransaction_TargetConditionalUpdated_NotAlreadyExisting() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + + // Test + + String p1Id = IdType.newRandomUuid().getValue(); + String p2Id = IdType.newRandomUuid().getValue(); + + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, p1Id)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, p2Id)); + bb.addTransactionUpdateEntry(newPatientP1_HomerSimpson().setId(p1Id)).conditional("Patient?identifier=http://system|200"); + bb.addTransactionUpdateEntry(newPatientP2_MargeSimpson().setId(p2Id)).conditional("Patient?identifier=http://system|300"); + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + // 1- Resolve resource forced IDs, and 2- Resolve Practitioner/PR1 reference + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + @Test + public void testCreate_InTransaction_TargetConditionalUpdated_AlreadyExisting() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + myPatientDao.create(new Patient().addIdentifier(new Identifier().setSystem("http://system").setValue("200")), mySrd); + myPatientDao.create(new Patient().addIdentifier(new Identifier().setSystem("http://system").setValue("300")), mySrd); + + // Test + + String p1Id = IdType.newRandomUuid().getValue(); + String p2Id = IdType.newRandomUuid().getValue(); + + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, p1Id)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, p2Id)); + bb.addTransactionUpdateEntry(newPatientP1_HomerSimpson().setId(p1Id)).conditional("Patient?identifier=http://system|200"); + bb.addTransactionUpdateEntry(newPatientP2_MargeSimpson().setId(p2Id)).conditional("Patient?identifier=http://system|300"); + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + // 1- Resolve resource forced IDs, and 2- Resolve Practitioner/PR1 reference + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(10, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + @Test + public void testCreate_InTransaction_TargetConditionalCreatedNotAlreadyExisting_RefsUsePlaceholderIds() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + + // Test + + String p1Id = IdType.newRandomUuid().getValue(); + String p2Id = IdType.newRandomUuid().getValue(); + + // Put the creates second to ensure that order doesn't matter + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, p1Id)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, p2Id)); + bb.addTransactionCreateEntry(newPatientP1_HomerSimpson().setId(p1Id)).conditional("identifier=http://system|200"); + bb.addTransactionCreateEntry(newPatientP2_MargeSimpson().setId(p2Id)).conditional("identifier=http://system|300"); + ; + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + // 1- Resolve resource forced IDs, and 2- Resolve Practitioner/PR1 reference + assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + + @Test + public void testCreate_InTransaction_TargetConditionalCreatedAlreadyExisting_RefsUsePlaceholderIds() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + + // Test + + String p1Id = IdType.newRandomUuid().getValue(); + String p2Id = IdType.newRandomUuid().getValue(); + + // Put the creates second to ensure that order doesn't matter + // Also, patients being conditionally created here don't have names, which + // ensures we pull the names in the uplift params from the pre-existing + // resources that do have names. + BundleBuilder bb = new BundleBuilder(myFhirContext); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E1, p1Id)); + bb.addTransactionUpdateEntry(newEncounter(ENCOUNTER_E2, p2Id)); + bb.addTransactionCreateEntry(new Patient().addIdentifier(new Identifier().setSystem("http://system").setValue("200")).setId(p1Id)).conditional("identifier=http://system|200"); + bb.addTransactionCreateEntry(new Patient().addIdentifier(new Identifier().setSystem("http://system").setValue("300")).setId(p2Id)).conditional("identifier=http://system|300"); + ; + Bundle requestBundle = bb.getBundleTyped(); + + myCaptureQueriesListener.clear(); + mySystemDao.transaction(mySrd, requestBundle); + + // Verify SQL + + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(10, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + + // Verify correct indexes are written + + runInTransaction(() -> { + logAllStringIndexes(); + + List params = myResourceIndexedSearchParamStringDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getValueExact()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "subject.name Homer", + "subject.name Simpson", + "subject.name Marge", + "subject.name Simpson" + )); + }); + } + + @Test + public void testSearch_WithoutUpliftRefchain_SingleChain() { + // Setup + + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous("subject", new ReferenceParam("name", "homer")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual, contains(ENCOUNTER_E1)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(2, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HFJ_RES_LINK"), querySql); + assertEquals(0, countMatches(querySql, "HASH_IDENTITY"), querySql); + } + + @Test + public void testSearch_UsingUpliftRefchain_SingleChain() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous("subject", new ReferenceParam("name", "homer")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual, contains(ENCOUNTER_E1)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(1, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(0, countMatches(querySql, "HFJ_RES_LINK"), querySql); + assertEquals(0, countMatches(querySql, "HASH_IDENTITY"), querySql); + } + + /** + * Only single chains are supported. Should fall back to a normal + * search otherwise. + */ + @Test + public void testSearch_UsingUpliftRefchain_DoubleChain() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + createOrganizationO1_SpringfieldHospital(); + createPractitionerPr1_BarneyGumble(); + createPatientP1_HomerSimpson(ORGANIZATION_O1); + createPatientP2_MargeSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous("subject", new ReferenceParam("organization.name", "springfield")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual, contains(ENCOUNTER_E1)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(1, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(2, countMatches(querySql, "HFJ_RES_LINK"), querySql); + assertEquals(0, countMatches(querySql, "HASH_IDENTITY"), querySql); + } + + @Test + public void testSearch_SortOnUpliftRefchain_SingleChain() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("subject.name")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(0, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(0, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortOnUpliftRefchain_SingleChain_Qualified() { + // Setup + + createSearchParam_EncounterSubject_WithUpliftOnName(); + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("Patient:subject.name")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(0, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(0, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortWithoutUpliftRefchain_SingleChain_String() { + // Setup + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + logAllResourceLinks(); + logAllStringIndexes("name"); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("patient.name")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(0, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(1, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortWithoutUpliftRefchain_SingleChain_String_AlsoIncludesSearch() { + // Setup + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + logAllResourceLinks(); + logAllStringIndexes("name"); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .add("patient", new ReferenceParam("name", "Simpson")) + .setSort(new SortSpec("patient.name")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(2, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(1, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(2, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortWithoutUpliftRefchain_SingleChain_Token() { + // Setup + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + logAllResourceLinks(); + logAllStringIndexes("name"); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("patient.identifier")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_TOKEN"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(1, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortWithoutUpliftRefchain_SingleChain_Date() { + // Setup + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + logAllResourceLinks(); + logAllStringIndexes("name"); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("patient.birthdate")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_DATE"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(1, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + @Test + public void testSearch_SortWithoutUpliftRefchain_SingleChain_MultiTargetReference_Qualified() { + // Setup + + createPractitionerPr1_BarneyGumble(); + // Add these with given names out of order for sorting + createPatientP1_HomerSimpson(); + createPatientP2_MargeSimpson(); + createPatientP3_AbacusSimpson(); + createEncounter(ENCOUNTER_E1, PATIENT_P1); + createEncounter(ENCOUNTER_E2, PATIENT_P2); + createEncounter(ENCOUNTER_E3, PATIENT_P3); + logAllResourceLinks(); + logAllStringIndexes("name"); + + // Test + + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("Patient:subject.name")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myEncounterDao.search(map, mySrd); + + // Verify + List actual = toUnqualifiedVersionlessIdValues(outcome); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(actual.toString(), actual, contains(ENCOUNTER_E3, ENCOUNTER_E1, ENCOUNTER_E2)); + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + String querySql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, countMatches(querySql, "HFJ_SPIDX_STRING"), querySql); + assertEquals(0, countMatches(querySql, "HASH_NORM_PREFIX"), querySql); + assertEquals(1, countMatches(querySql, "HASH_IDENTITY"), querySql); + assertEquals(1, countMatches(querySql, "HFJ_RES_LINK"), querySql); + } + + /** + * Observation:focus is a Reference(Any) so it can't be used in a sort chain because + * this would be horribly, horribly inefficient. + */ + @Test + public void testSearch_SortWithUnsupportedExpression_AnyTargetReference_Unqualified() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("focus.name")); + myObservationDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Unable to sort on a chained parameter from 'focus' as this parameter has multiple target types. Please specify the target type.")); + } + + } + + /** + * Observation:date isn't a Reference so it can't be used in a chained expression + */ + @Test + public void testSearch_SortWithUnsupportedExpression_SingleChain_NonReferenceType() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("date.name")); + myObservationDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), containsString("Invalid chain, date is not a reference SearchParameter")); + } + + } + + @Test + public void testSearch_SortWithUnsupportedExpression_SingleChain_MultiTargetReference_Unqualified() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("subject.name")); + myCaptureQueriesListener.clear(); + myEncounterDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Unable to sort on a chained parameter from 'subject' as this parameter has multiple target types. Please specify the target type")); + + } + } + + @Test + public void testSearch_SortWithUnsupportedExpression_ThreeLevelChains() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("patient.organization.name")); + myCaptureQueriesListener.clear(); + myEncounterDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Invalid _sort expression, can not chain more than once in a sort expression: patient.organization.name")); + + } + } + + @Test + public void testSearch_SortWithUnsupportedExpression_UnknownFirstPart() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("foo.name")); + myCaptureQueriesListener.clear(); + myEncounterDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Unknown _sort parameter value \"foo\" for resource type \"Encounter\"")); + + } + } + + @Test + public void testSearch_SortWithUnsupportedExpression_UnknownSecondPart() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("patient.foo")); + myCaptureQueriesListener.clear(); + myEncounterDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Unknown _sort parameter value \"foo\" for resource type \"Patient\"")); + + } + } + + @Test + public void testSearch_SortWithUnsupportedExpression_UnsupportedTargetType() { + + // Test + + try { + SearchParameterMap map = SearchParameterMap + .newSynchronous() + .setSort(new SortSpec("result.value-quantity")); + myCaptureQueriesListener.clear(); + myDiagnosticReportDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + + // Verify + assertThat(e.getMessage(), containsString("Unable to sort on a chained parameter result.value-quantity as this parameter. Can not sort on chains of target type: QUANTITY")); + + } + } + + private void createOrganizationO1_SpringfieldHospital() { + Organization org = new Organization(); + org.setId(ORGANIZATION_O1); + org.setName("Springfield Hospital"); + myOrganizationDao.update(org, mySrd); + } + + private void createEncounter(String theEncounterId, String theSubjectReference) { + Encounter e1 = newEncounter(theEncounterId, theSubjectReference); + myEncounterDao.update(e1, mySrd); + } + + private void createPatientP1_HomerSimpson() { + createPatientP1_HomerSimpson(null); + } + + private void createPatientP1_HomerSimpson(String theOrganizationId) { + Patient p1 = newPatientP1_HomerSimpson(theOrganizationId); + myPatientDao.update(p1, mySrd); + } + + private void createPatientP2_MargeSimpson() { + Patient p2 = newPatientP2_MargeSimpson(); + myPatientDao.update(p2, mySrd); + } + + private void createPatientP3_AbacusSimpson() { + Patient p3 = new Patient(); + p3.setId(PATIENT_P3); + p3.addIdentifier().setSystem("http://system").setValue("100"); + p3.addName().setFamily("Simpson").addGiven("Abacus"); + p3.setBirthDateElement(new DateType("2020-01-01")); + myPatientDao.update(p3, mySrd); + } + + private void createPractitionerPr1_BarneyGumble() { + Practitioner practitioner = new Practitioner(); + practitioner.setId(PRACTITIONER_PR1); + practitioner.addName().setFamily("Gumble").addGiven("Barney"); + myPractitionerDao.update(practitioner, mySrd); + } + + private void createSearchParam_EncounterSubject_WithUpliftOnName() { + RuntimeSearchParam subjectSp = mySearchParamRegistry.getRuntimeSearchParam("Encounter", "subject"); + SearchParameter sp = new SearchParameter(); + Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new CodeType("name")); + + sp.setId(subjectSp.getId()); + sp.setCode(subjectSp.getName()); + sp.setName(subjectSp.getName()); + sp.setUrl(subjectSp.getUri()); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setExpression(subjectSp.getPath()); + subjectSp.getBase().forEach(sp::addBase); + subjectSp.getTargets().forEach(sp::addTarget); + mySearchParameterDao.update(sp, mySrd); + + mySearchParamRegistry.forceRefresh(); + } + + @Nonnull + private static Encounter newEncounter(String theEncounterId, String theSubjectReference) { + Encounter e1 = new Encounter(); + e1.setId(theEncounterId); + e1.setSubject(new Reference(theSubjectReference)); + e1.addParticipant().setActor(new Reference(PRACTITIONER_PR1)); + return e1; + } + + @Nonnull + private static Patient newPatientP1_HomerSimpson() { + return newPatientP1_HomerSimpson(null); + } + + @Nonnull + private static Patient newPatientP1_HomerSimpson(String theOrganizationId) { + Patient p1 = new Patient(); + p1.setId(PATIENT_P1); + p1.addIdentifier().setSystem("http://system").setValue("200"); + p1.addName().setFamily("Simpson").addGiven("Homer"); + p1.setBirthDateElement(new DateType("2020-01-02")); + if (theOrganizationId != null) { + p1.setManagingOrganization(new Reference(theOrganizationId)); + } + return p1; + } + + @Nonnull + private static Patient newPatientP2_MargeSimpson() { + Patient p2 = new Patient(); + p2.setId(PATIENT_P2); + p2.addIdentifier().setSystem("http://system").setValue("300"); + p2.setBirthDateElement(new DateType("2020-01-03")); + p2.addName().setFamily("Simpson").addGiven("Marge"); + return p2; + } + + +} diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 3d9526e4833..87f59488b1a 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 33b3c572b69..c3d5f21a2a7 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java index 760bc27bfec..38269b1e2ff 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java @@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config; import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig; import ca.uhn.fhir.interceptor.api.IInterceptorService; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.config.ThreadPoolFactoryConfig; import ca.uhn.fhir.jpa.batch2.JpaBatch2Config; import ca.uhn.fhir.jpa.model.config.PartitionSettings; @@ -105,4 +106,9 @@ public class CommonConfig { return new ScheduledSubscriptionDeleter(); } + @Bean + public CommonJpaStorageSettingsConfigurer commonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) { + return new CommonJpaStorageSettingsConfigurer(theStorageSettings); + } + } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java new file mode 100644 index 00000000000..7e8bc19273f --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java @@ -0,0 +1,9 @@ +package ca.uhn.fhirtest.config; + +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; + +public class CommonJpaStorageSettingsConfigurer { + public CommonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) { + theStorageSettings.setIndexOnUpliftedRefchains(true); + } +} diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 44232da0854..cefcb5182ec 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 445ba9406e7..489f8883358 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index ac4361bc81e..d642ac9af9a 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java index 4ae136ea3ea..64a8b4e2c65 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import javax.annotation.Nonnull; @@ -60,6 +61,7 @@ public class TransactionDetails { private List myRollbackUndoActions = Collections.emptyList(); private Map myResolvedResourceIds = Collections.emptyMap(); private Map myResolvedMatchUrls = Collections.emptyMap(); + private Map> myResolvedResources = Collections.emptyMap(); private Map myUserData; private ListMultimap myDeferredInterceptorBroadcasts; private EnumSet myDeferredInterceptorBroadcastPointcuts; @@ -123,6 +125,21 @@ public class TransactionDetails { return myResolvedResourceIds.get(idValue); } + /** + * A Resolved Resource ID is a mapping between a resource ID (e.g. "Patient/ABC" or + * "Observation/123") and the actual persisted/resolved resource with this ID. + */ + @Nullable + public IBaseResource getResolvedResource(IIdType theId) { + String idValue = theId.toUnqualifiedVersionless().getValue(); + IBaseResource retVal = null; + Supplier supplier = myResolvedResources.get(idValue); + if (supplier != null) { + retVal = supplier.get(); + } + return retVal; + } + /** * Was the given resource ID resolved previously in this transaction as not existing */ @@ -150,6 +167,30 @@ public class TransactionDetails { myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId); } + /** + * A Resolved Resource ID is a mapping between a resource ID (e.g. "Patient/ABC" or + * "Observation/123") and the actual persisted/resolved resource. + * This version takes a {@link Supplier} which will only be fetched if the + * resource is actually needed. This is good in cases where the resource is + * lazy loaded. + */ + public void addResolvedResource(IIdType theResourceId, @Nonnull Supplier theResource) { + assert theResourceId != null; + + if (myResolvedResources.isEmpty()) { + myResolvedResources = new HashMap<>(); + } + myResolvedResources.put(theResourceId.toVersionless().getValue(), theResource); + } + + /** + * A Resolved Resource ID is a mapping between a resource ID (e.g. "Patient/ABC" or + * "Observation/123") and the actual persisted/resolved resource. + */ + public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) { + addResolvedResource(theResourceId, () -> theResource); + } + public Map getResolvedMatchUrls() { return myResolvedMatchUrls; } @@ -315,5 +356,6 @@ public class TransactionDetails { public boolean isFhirTransaction() { return myFhirTransaction; } + } diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index d385269cdbd..2ae77976e80 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index 024b8532e07..80237bad884 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml @@ -20,7 +20,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT com.github.ben-manes.caffeine diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index 930c20ca0c1..7e16a136021 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index a8e20685d4e..c8943686e1e 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 27031c19cd8..dca1c429b21 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 9111532265d..912ee0d7aa4 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 3f83ee5d79a..695afca4418 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index e829933c995..a70090d9cf6 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 7bfc8304eb6..713a4b26866 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index baa6caf779e..e46269bfc71 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT hapi-fhir-spring-boot-samples diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index c655b9a00f9..feae5a0513c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index e7b4548eb5e..9d029521b08 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index 4d80d4e442e..b40305e994b 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index 63a0b5eec9f..2e77f2682d1 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index 055025da895..c15ef77d66d 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-test-utilities/src/main/java/ca/uhn/hapi/fhir/batch2/test/AbstractIJobPersistenceSpecificationTest.java b/hapi-fhir-storage-batch2-test-utilities/src/main/java/ca/uhn/hapi/fhir/batch2/test/AbstractIJobPersistenceSpecificationTest.java index 6101c43608b..05e165a4c93 100644 --- a/hapi-fhir-storage-batch2-test-utilities/src/main/java/ca/uhn/hapi/fhir/batch2/test/AbstractIJobPersistenceSpecificationTest.java +++ b/hapi-fhir-storage-batch2-test-utilities/src/main/java/ca/uhn/hapi/fhir/batch2/test/AbstractIJobPersistenceSpecificationTest.java @@ -1,5 +1,25 @@ package ca.uhn.hapi.fhir.batch2.test; +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 specification tests + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.batch2.api.IJobPersistence; import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk; import ca.uhn.fhir.batch2.model.JobInstance; diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index e835ace2b84..42b256577f9 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IWorkChunkPersistence.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IWorkChunkPersistence.java index ed051b66b22..fdb3a988d63 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IWorkChunkPersistence.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IWorkChunkPersistence.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.batch2.api; +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 Task Processor + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java index 3d7f2e08861..a735d9f47f3 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java @@ -32,6 +32,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.PartitionedJobParameters; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.pid.IResourcePidList; import ca.uhn.fhir.jpa.api.pid.TypedResourcePid; +import ca.uhn.fhir.system.HapiSystemProperties; import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; @@ -85,7 +86,7 @@ public class ResourceIdListStep ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 87b43215c05..283166a8ef8 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index 6125855045f..b744cf08696 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index 2e22bae5b3b..d1ae3d20ef5 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/ISearchUrlJobMaintenanceSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/ISearchUrlJobMaintenanceSvc.java index 71f30b53ab2..bfeb71d0bd0 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/ISearchUrlJobMaintenanceSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/ISearchUrlJobMaintenanceSvc.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.api.svc; +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2023 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 interface ISearchUrlJobMaintenanceSvc { void removeStaleEntries(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index de6b4a62ed1..9717266fba3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -944,6 +944,7 @@ public abstract class BaseTransactionProcessor { handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest); } entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); + theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); if (outcome.getCreated() == false) { nonUpdatedEntities.add(outcome.getId()); } else { @@ -1031,6 +1032,7 @@ public abstract class BaseTransactionProcessor { } } + theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest); entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java index dcc9cc3b461..06dd04c9935 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java @@ -32,6 +32,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; import ca.uhn.fhir.jpa.model.cross.IResourceLookup; import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; @@ -76,6 +77,8 @@ public class DaoResourceLinkResolver implements private DaoRegistry myDaoRegistry; @Autowired private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private IHapiTransactionService myTransactionService; @Override public IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, String theSourceResourceName, PathAndRef thePathAndRef, RequestDetails theRequest, TransactionDetails theTransactionDetails) { @@ -154,6 +157,20 @@ public class DaoResourceLinkResolver implements return resolvedResource; } + @Nullable + @Override + public IBaseResource loadTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, String theSourceResourceName, PathAndRef thePathAndRef, RequestDetails theRequest, TransactionDetails theTransactionDetails) { + return myTransactionService + .withRequest(theRequest) + .withTransactionDetails(theTransactionDetails) + .withRequestPartitionId(theRequestPartitionId) + .execute(()->{ + IIdType targetId = thePathAndRef.getRef().getReferenceElement(); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(targetId.getResourceType()); + return dao.read(targetId, theRequest); + }); + } + /** * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID */ diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java index 85d0aec569b..022998369a1 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java @@ -384,7 +384,7 @@ public class HapiTransactionService implements IHapiTransactionService { @Override public void execute(Runnable theTask) { - Callable task = () -> { + TransactionCallback task = tx -> { theTask.run(); return null; }; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java index 9398e647f12..a077fe5cec8 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java @@ -37,14 +37,17 @@ import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.HapiExtensions; +import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -54,7 +57,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class SearchParamValidatingInterceptor { public static final String SEARCH_PARAM = "SearchParameter"; - public List myUpliftExtensions; private FhirContext myFhirContext; @@ -74,8 +76,8 @@ public class SearchParamValidatingInterceptor { validateSearchParamOnUpdate(theNewResource, theRequestDetails); } - public void validateSearchParamOnCreate(IBaseResource theResource, RequestDetails theRequestDetails){ - if(isNotSearchParameterResource(theResource)){ + public void validateSearchParamOnCreate(IBaseResource theResource, RequestDetails theRequestDetails) { + if (isNotSearchParameterResource(theResource)) { return; } RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource); @@ -83,21 +85,43 @@ public class SearchParamValidatingInterceptor { return; } + validateSearchParamOnCreateAndUpdate(runtimeSearchParam); + SearchParameterMap searchParameterMap = extractSearchParameterMap(runtimeSearchParam); if (searchParameterMap != null) { validateStandardSpOnCreate(theRequestDetails, searchParameterMap); } } + private void validateSearchParamOnCreateAndUpdate(RuntimeSearchParam theRuntimeSearchParam) { + + // Validate uplifted refchains + List> refChainExtensions = theRuntimeSearchParam.getExtensions(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + for (IBaseExtension nextExtension : refChainExtensions) { + List codeExtensions = nextExtension + .getExtension() + .stream() + .map(t->(IBaseExtension)t) + .filter(t -> HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE.equals(t.getUrl())) + .collect(Collectors.toList()); + if (codeExtensions.size() != 1) { + throw new UnprocessableEntityException(Msg.code(2283) + "Extension with URL " + HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN + " must have exactly one child extension with URL " + HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE); + } + if (codeExtensions.get(0).getValue() == null || !"code".equals(myFhirContext.getElementDefinition(codeExtensions.get(0).getValue().getClass()).getName())) { + throw new UnprocessableEntityException(Msg.code(2284) + "Extension with URL " + HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE + " must have a value of type 'code'"); + } + } + } + private void validateStandardSpOnCreate(RequestDetails theRequestDetails, SearchParameterMap searchParameterMap) { List persistedIdList = getDao().searchForIds(searchParameterMap, theRequestDetails); - if( isNotEmpty(persistedIdList) ) { + if (isNotEmpty(persistedIdList)) { throw new UnprocessableEntityException(Msg.code(2196) + "Can't process submitted SearchParameter as it is overlapping an existing one."); } } - public void validateSearchParamOnUpdate(IBaseResource theResource, RequestDetails theRequestDetails){ - if(isNotSearchParameterResource(theResource)){ + public void validateSearchParamOnUpdate(IBaseResource theResource, RequestDetails theRequestDetails) { + if (isNotSearchParameterResource(theResource)) { return; } RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource); @@ -105,6 +129,8 @@ public class SearchParamValidatingInterceptor { return; } + validateSearchParamOnCreateAndUpdate(runtimeSearchParam); + SearchParameterMap searchParameterMap = extractSearchParameterMap(runtimeSearchParam); if (searchParameterMap != null) { validateStandardSpOnUpdate(theRequestDetails, runtimeSearchParam, searchParameterMap); @@ -114,14 +140,14 @@ public class SearchParamValidatingInterceptor { private boolean isNewSearchParam(RuntimeSearchParam theSearchParam, Set theExistingIds) { return theExistingIds .stream() - .noneMatch(resId -> resId.substring(resId.indexOf("/")+1).equals(theSearchParam.getId().getIdPart())); + .noneMatch(resId -> resId.substring(resId.indexOf("/") + 1).equals(theSearchParam.getId().getIdPart())); } private void validateStandardSpOnUpdate(RequestDetails theRequestDetails, RuntimeSearchParam runtimeSearchParam, SearchParameterMap searchParameterMap) { List pidList = getDao().searchForIds(searchParameterMap, theRequestDetails); - if(isNotEmpty(pidList)){ + if (isNotEmpty(pidList)) { Set resolvedResourceIds = myIdHelperService.translatePidsToFhirResourceIds(new HashSet<>(pidList)); - if(isNewSearchParam(runtimeSearchParam, resolvedResourceIds)) { + if (isNewSearchParam(runtimeSearchParam, resolvedResourceIds)) { throwDuplicateError(); } } @@ -131,8 +157,8 @@ public class SearchParamValidatingInterceptor { throw new UnprocessableEntityException(Msg.code(2125) + "Can't process submitted SearchParameter as it is overlapping an existing one."); } - private boolean isNotSearchParameterResource(IBaseResource theResource){ - return ! SEARCH_PARAM.equalsIgnoreCase(myFhirContext.getResourceType(theResource)); + private boolean isNotSearchParameterResource(IBaseResource theResource) { + return !SEARCH_PARAM.equalsIgnoreCase(myFhirContext.getResourceType(theResource)); } @Nullable @@ -200,13 +226,4 @@ public class SearchParamValidatingInterceptor { return retVal; } - public List getUpliftExtensions() { - if (myUpliftExtensions == null) { - myUpliftExtensions = new ArrayList<>(); - } - return myUpliftExtensions; - } - public void addUpliftExtension(String theUrl) { - getUpliftExtensions().add(theUrl); - } } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/SearchParameterValidatingInterceptorTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/SearchParameterValidatingInterceptorTest.java index 36890926a1a..073a28d3e7a 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/SearchParameterValidatingInterceptorTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/SearchParameterValidatingInterceptorTest.java @@ -10,15 +10,21 @@ import ca.uhn.fhir.jpa.searchparam.submit.interceptor.SearchParamValidatingInter import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.HapiExtensions; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.SimpleQuantity; +import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import javax.annotation.Nonnull; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @@ -37,8 +43,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class SearchParameterValidatingInterceptorTest { - public static final String UPLIFT_URL = "https://some-url"; - static final FhirContext ourFhirContext = FhirContext.forR4(); + static final FhirContext ourFhirContext = FhirContext.forR4Cached(); static String ID1 = "ID1"; static String ID2 = "ID2"; @Mock @@ -60,7 +65,6 @@ public class SearchParameterValidatingInterceptorTest { mySearchParamValidatingInterceptor.setSearchParameterCanonicalizer(new SearchParameterCanonicalizer(ourFhirContext)); mySearchParamValidatingInterceptor.setIIDHelperService(myIdHelperService); mySearchParamValidatingInterceptor.setDaoRegistry(myDaoRegistry); - mySearchParamValidatingInterceptor.addUpliftExtension(UPLIFT_URL); myExistingSearchParameter = buildSearchParameterWithId(ID1); @@ -102,6 +106,48 @@ public class SearchParameterValidatingInterceptorTest { } } + @Test + public void whenCreateSpWithUpliftRefchains_Bad_WrongCodeDatatype() { + SearchParameter sp = buildReferenceSearchParameter(); + Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE, new SimpleQuantity().setValue(123L)); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME, new StringType("element1")); + try { + mySearchParamValidatingInterceptor.resourcePreCreate(sp, myRequestDetails); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("2284")); + } + + } + + @Test + public void whenCreateSpWithUpliftRefchains_Bad_NoCode() { + SearchParameter sp = buildReferenceSearchParameter(); + Extension upliftRefChain = sp.addExtension().setUrl(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN); + upliftRefChain.addExtension(HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME, new StringType("element1")); + try { + mySearchParamValidatingInterceptor.resourcePreCreate(sp, myRequestDetails); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("2283")); + } + + } + + @Nonnull + private static SearchParameter buildReferenceSearchParameter() { + SearchParameter sp = new SearchParameter(); + sp.setCode("subject"); + sp.setName("subject"); + sp.setDescription("Modified Subject"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.REFERENCE); + sp.setExpression("Encounter.subject"); + sp.addBase("Encounter"); + sp.addTarget("Patient"); + return sp; + } @Test public void whenUsingPutOperationToCreateNonOverlappingSearchParam_thenIsAllowed() { diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 62cf165bd2d..ad776e66c0c 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 6f118fc36eb..1dadf69eee6 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index bab194c5ff2..655b535a2cd 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/ExtensionUtilTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/ExtensionUtilTest.java new file mode 100644 index 00000000000..0be429bd221 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/ExtensionUtilTest.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.util; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ExtensionUtilTest { + + @Test + public void testExtractChildPrimitiveExtensionValue() { + + Patient p = new Patient(); + Extension parent = p.addExtension().setUrl("parent"); + parent.addExtension("child1", new BooleanType(true)); + parent.addExtension("child2", new BooleanType(false)); + parent.addExtension("child3", new Quantity(123)); + + assertEquals("false", ExtensionUtil.extractChildPrimitiveExtensionValue(parent, "child2")); + assertEquals(null, ExtensionUtil.extractChildPrimitiveExtensionValue(parent, "unknown")); + assertEquals(null, ExtensionUtil.extractChildPrimitiveExtensionValue(parent, "child3")); + + } + + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 581240b703c..62718524c9e 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 5956a549db8..086b3268a84 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 8d5c11c7076..8f468a1a8f5 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index a495736e563..85f73add583 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index b667fdbabde..21ed8dddb7b 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 592dcf40712..7d09c9a607e 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index 28b755d426d..c6d14a902b9 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index dfc3c1d4259..4d89a741970 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 55fad7c4e4c..a97fb51b9b2 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index a00682039c6..1257a6ec8da 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index 1c02eda581e..80dd9c6fa35 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 689bead966c..65988c56a1a 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index e2978b387fd..8cbf0df796f 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index fa67106f9de..95b56e91ef0 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 20999cc7829..760a337bece 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 5cad1bd4ced..2f6b27ba00f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 3c2188e6fe2..2613268fa5c 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 8d8bc15356a..106f0cbb89d 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index 5c05fa6eeb4..3e85d2eda4e 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.5.6-SNAPSHOT + 6.5.7-SNAPSHOT ../../pom.xml