Search and Sort on Uplifted Refchains (#4633)

* Start work on refchains

* Semi working

* Add tests for transactions

* Add docs and lots of tests

* Add changelog

* Add tests

* Work on cleanup

* Add document operations

* Test fix

* Test fix

* Fixes

* Fix typo

* Test fix

* Test update

* Test updates

* Test fix

* Test fixes

* Test additions

* Test fix

* Add some javadocs

* Test fixes

* Intermittent test fix

* Doc tweak

* Test fixes

* Merge master in

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md

Co-authored-by: michaelabuckley <michaelabuckley@gmail.com>

* Review comments

* Version bump

* Add license

---------

Co-authored-by: michaelabuckley <michaelabuckley@gmail.com>
This commit is contained in:
James Agnew 2023-03-15 07:04:32 -04:00 committed by GitHub
parent 8956b273f0
commit cf5470ae58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 3399 additions and 566 deletions

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.context; package ca.uhn.fhir.context;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; 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.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -18,6 +20,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
@ -56,6 +59,7 @@ public class RuntimeSearchParam {
private final RuntimeSearchParamStatusEnum myStatus; private final RuntimeSearchParamStatusEnum myStatus;
private final String myUri; private final String myUri;
private final Map<String, List<IBaseExtension<?, ?>>> myExtensions = new HashMap<>(); private final Map<String, List<IBaseExtension<?, ?>>> myExtensions = new HashMap<>();
private final Map<String, String> myUpliftRefchains = new HashMap<>();
private final ComboSearchParamType myComboSearchParamType; private final ComboSearchParamType myComboSearchParamType;
private final List<Component> myComponents; private final List<Component> myComponents;
private IPhoneticEncoder myPhoneticEncoder; private IPhoneticEncoder myPhoneticEncoder;
@ -148,7 +152,7 @@ public class RuntimeSearchParam {
/** /**
* Sets user data - This can be used to store any application-specific data * 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<IBaseExtension<?, ?>> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>()); List<IBaseExtension<?, ?>> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>());
valuesList.add(theValue); valuesList.add(theValue);
return this; return this;
@ -276,6 +280,40 @@ public class RuntimeSearchParam {
return retVal; 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<String> 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 { public enum RuntimeSearchParamStatusEnum {
ACTIVE, ACTIVE,
DRAFT, DRAFT,

View File

@ -23,12 +23,16 @@ package ca.uhn.fhir.util;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg; 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.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 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.List;
import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -291,4 +295,32 @@ public class ExtensionUtil {
public static boolean equals(IBaseExtension<?,?> theLeftExtension, IBaseExtension<?,?> theRightExtension) { public static boolean equals(IBaseExtension<?,?> theLeftExtension, IBaseExtension<?,?> theRightExtension) {
return TerserUtil.equals(theLeftExtension, 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 <D, T extends IBaseExtension<T, D>> String extractChildPrimitiveExtensionValue(@Nonnull IBaseExtension<T, D> theExtension, @Nonnull String theChildExtensionUrl) {
Validate.notNull(theExtension, "theExtension must not be null");
Validate.notBlank(theChildExtensionUrl, "theChildExtensionUrl must not be null or blank");
Optional<T> 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;
}
} }

View File

@ -140,6 +140,22 @@ public class HapiExtensions {
*/ */
public static final String EXTENSION_SUBSCRIPTION_CROSS_PARTITION = "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-cross-partition"; 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 * Non instantiable
*/ */

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.util; 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 org.apache.commons.lang3.Validate;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationHandler;

View File

@ -22,6 +22,11 @@ package org.hl7.fhir.instance.model.api;
import java.util.List; import java.util.List;
/**
* @param <T> The actual concrete type of the extension
* @param <D> Note that this type param is not used anywhere - It is kept only to avoid making a breaking change
*/
//public interface IBaseExtension<T extends IBaseExtension<T, D>, D> extends ICompositeType {
public interface IBaseExtension<T, D> extends ICompositeType { public interface IBaseExtension<T, D> extends ICompositeType {
List<T> getExtension(); List<T> getExtension();

View File

@ -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.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.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.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.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.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 ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation

View File

@ -4,14 +4,14 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId> <artifactId>hapi-fhir-bom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>HAPI FHIR BOM</name> <name>HAPI FHIR BOM</name>
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId> <artifactId>hapi-fhir-cli</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath> <relativePath>../../hapi-deployable-pom</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -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`."

View File

@ -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."

View File

@ -1,12 +1,15 @@
# JPA Server Search # 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
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 ```http
https://localhost:8000/Practitioner?_has:PractitionerRole:practitioner:service.type=CHIRO https://localhost:8000/Practitioner?_has:PractitionerRole:practitioner:service.type=CHIRO
``` ```
@ -20,3 +23,208 @@ Searching on Location.Position using `near` currently uses a box search, not a r
The special `_filter` is only partially implemented. The special `_filter` is only partially implemented.
<a name="uplifted-refchains"/>
# 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.
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>FHIRPath Expression</th>
</tr>
</thead>
<tbody>
<tr>
<td>composition</td>
<td>Reference</td>
<td>Bundle.entry[0].resource as Composition</td>
</tr>
<tr>
<td>message</td>
<td>Reference</td>
<td>Bundle.entry[0].resource as MessageHeader</td>
</tr>
</tbody>
</table>
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"
}
```
<a name="chained-sorting"/>
# 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.

View File

@ -11,7 +11,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -999,7 +999,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
entity.setDeleted(null); entity.setDeleted(null);
// TODO: is this IF statement always true? Try removing it // TODO: is this IF statement always true? Try removing it
if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) { if (thePerformIndexing || theEntity.getVersion() == 1) {
newParams = new ResourceIndexedSearchParams(); newParams = new ResourceIndexedSearchParams();
@ -1013,6 +1013,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
} }
failIfPartitionMismatch(theRequest, entity); failIfPartitionMismatch(theRequest, entity);
mySearchParamWithInlineReferencesExtractor.populateFromResource(requestPartitionId, newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing); mySearchParamWithInlineReferencesExtractor.populateFromResource(requestPartitionId, newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing);
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);

View File

@ -414,6 +414,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext)); jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext));
myIdHelperService.addResolvedPidToForcedId(jpaPid, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null); myIdHelperService.addResolvedPidToForcedId(jpaPid, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null);
theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid); theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid);
theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource);
// Pre-cache the match URL // Pre-cache the match URL
if (theMatchUrl != null) { if (theMatchUrl != null) {

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.dao.data; 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 ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.search; 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.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao; import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.search; 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.api.svc.ISearchUrlJobMaintenanceSvc;
import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs; import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;

View File

@ -77,6 +77,7 @@ import ca.uhn.fhir.rest.param.HasParam;
import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam; 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.StringParam;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier; 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.jpa.util.QueryParameterUtils.toOrPredicate;
import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS; import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; 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.isNotBlank;
import static org.apache.commons.lang3.StringUtils.split; 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(); BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this); ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this);
Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>()); Condition pathPredicate = resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName);
addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate); addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate);
if (isBlank(theChain)) {
mySqlBuilder.addSortNumeric(resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate); 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<String> 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) { public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) {
@ -276,16 +366,22 @@ public class QueryStack {
} }
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( ComboCondition onCondition = mySqlBuilder.createOnCondition(
theFromJoiningPredicateBuilder.getResourceIdColumn(), theFromDbColumn,
theToJoiningPredicateBuilder.getResourceIdColumn() theToJoiningPredicateBuilder.getResourceIdColumn()
); );
if (theCondition != null) {
onCondition.addCondition(theCondition); onCondition.addCondition(theCondition);
}
mySqlBuilder.addCustomJoin( mySqlBuilder.addCustomJoin(
SelectQuery.JoinType.LEFT_OUTER, SelectQuery.JoinType.LEFT_OUTER,
theFromJoiningPredicateBuilder.getTable(), theFromDbColumn.getTable(),
theToJoiningPredicateBuilder.getTable(), theToJoiningPredicateBuilder.getTable(),
onCondition); onCondition);
} }
@ -385,7 +481,7 @@ public class QueryStack {
ourLog.error("Cannot create missing parameter query for a composite parameter."); ourLog.error("Cannot create missing parameter query for a composite parameter.");
return null; return null;
} else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { } 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."); ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search.");
return null; return null;
} }
@ -572,6 +668,7 @@ public class QueryStack {
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
} }
public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
@ -958,220 +1055,19 @@ public class QueryStack {
mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn()); mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn());
} }
private class ChainElement { public Condition createPredicateReferenceForEmbeddedChainedSearchResource(@Nullable DbColumn theSourceJoinColumn,
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<List<ChainElement>,Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
public Map<List<ChainElement>,Set<LeafNodeDefinition>> getChains() { return myChains; }
private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
return split(theReferenceParam.getChain(), '.').length <= 3;
}
private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) {
List<String> 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<? extends IQueryParameterType> theList) {
List<String> paths = extractPaths(theResourceType, theSearchParam);
for (String path : paths) {
List<ChainElement> 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<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType());
}
}
}
}
private void processNextLinkInChain(List<ChainElement> theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List<String> 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<String> 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<IQueryParameterType> 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<LeafNodeDefinition> 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<String> nextPaths = extractPaths(nextTarget, nextSearchParam);
for (String nextPath : nextPaths) {
List<ChainElement> 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<IQueryParameterType> myOrValues;
private final String myLeafTarget;
private final String myLeafParamName;
private final String myLeafPathPrefix;
private final List<String> myQualifiers;
public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List<String> theQualifiers) {
myParamDefinition = theParamDefinition;
myOrValues = theOrValues;
myLeafTarget = theLeafTarget;
myLeafParamName = theLeafParamName;
myLeafPathPrefix = theLeafPathPrefix;
myQualifiers = theQualifiers;
}
public RuntimeSearchParam getParamDefinition() {
return myParamDefinition;
}
public ArrayList<IQueryParameterType> getOrValues() {
return myOrValues;
}
public String getLeafTarget() {
return myLeafTarget;
}
public String getLeafParamName() {
return myLeafParamName;
}
public String getLeafPathPrefix() {
return myLeafPathPrefix;
}
public List<String> 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, String theResourceName, RuntimeSearchParam theSearchParam,
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation, List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { RequestDetails theRequest, RequestPartitionId theRequestPartitionId,
EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
boolean wantChainedAndNormal = theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
// 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 // 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<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes); EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes);
if (wantChainedAndNormal) {
myReusePredicateBuilderTypes.clear(); myReusePredicateBuilderTypes.clear();
}
UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
chainExtractor.deriveChains(theResourceName, theSearchParam, theList); chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
@ -1181,15 +1077,30 @@ public class QueryStack {
for (List<ChainElement> nextChain : chains.keySet()) { for (List<ChainElement> nextChain : chains.keySet()) {
Set<LeafNodeDefinition> leafNodes = chains.get(nextChain); Set<LeafNodeDefinition> leafNodes = chains.get(nextChain);
collateChainedSearchOptions(referenceLinks, nextChain, leafNodes); collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum);
} }
UnionQuery union = null;
List<Condition> predicates = null;
if (wantChainedAndNormal) {
union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
} else {
predicates = new ArrayList<>();
}
predicates = new ArrayList<>();
for (List<String> nextReferenceLink : referenceLinks.keySet()) { for (List<String> nextReferenceLink : referenceLinks.keySet()) {
for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder(); SearchQueryBuilder builder;
if (wantChainedAndNormal) {
builder = mySqlBuilder.newChildSqlBuilder();
} else {
builder = mySqlBuilder;
}
DbColumn previousJoinColumn = null; 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) { for (String nextLink : nextReferenceLink) {
// We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion. // 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. // TODO: Are we missing any important business logic from that method? All tests are passing.
@ -1211,37 +1122,60 @@ public class QueryStack {
theRequestPartitionId, theRequestPartitionId,
builder); builder);
if (wantChainedAndNormal) {
builder.addPredicate(containedCondition); builder.addPredicate(containedCondition);
union.addQueries(builder.getSelect()); union.addQueries(builder.getSelect());
} else {
predicates.add(containedCondition);
}
} }
} }
InCondition inCondition; Condition retVal;
if (wantChainedAndNormal) {
if (theSourceJoinColumn == null) { if (theSourceJoinColumn == null) {
inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); retVal = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union);
} else { } else {
//-- for the resource link, need join with target_resource_id //-- for the resource link, need join with target_resource_id
inCondition = new InCondition(theSourceJoinColumn, union); retVal = new InCondition(theSourceJoinColumn, union);
}
} else {
retVal = toOrPredicate(predicates);
} }
// restore the state of this collection to turn caching back on before we exit // restore the state of this collection to turn caching back on before we exit
myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes);
return inCondition; return retVal;
} }
private void collateChainedSearchOptions(Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, List<ChainElement> nextChain, Set<LeafNodeDefinition> leafNodes) { private void collateChainedSearchOptions(Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, List<ChainElement> nextChain, Set<LeafNodeDefinition> leafNodes, EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
// Manually collapse the chain using all possible variants of contained resource patterns. // 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? // 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` // 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) { if (nextChain.size() == 1) {
// discrete -> discrete // discrete -> discrete
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); updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes);
}
// discrete -> contained // discrete -> contained
RuntimeSearchParam firstParamDefinition = leafNodes.iterator().next().getParamDefinition();
updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(),
leafNodes leafNodes
.stream() .stream()
.map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName())) .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())); .collect(Collectors.toSet()));
} else if (nextChain.size() == 2) { } else if (nextChain.size() == 2) {
// discrete -> discrete -> discrete // discrete -> discrete -> discrete
@ -1359,7 +1293,7 @@ public class QueryStack {
theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
break; break;
case REFERENCE: 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); theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
break; break;
case HAS: case HAS:
@ -1465,11 +1399,15 @@ public class QueryStack {
List<Condition> andPredicates = new ArrayList<>(); List<Condition> andPredicates = new ArrayList<>();
for (List<? extends IQueryParameterType> nextAndParams : theList) { for (List<? extends IQueryParameterType> nextAndParams : theList) {
if ( ! checkHaveTags(nextAndParams, theParamName)) { continue; } if (!checkHaveTags(nextAndParams, theParamName)) {
continue;
}
List<Triple<String, String, String>> tokens = Lists.newArrayList(); List<Triple<String, String, String>> tokens = Lists.newArrayList();
boolean paramInverted = populateTokens(tokens, nextAndParams); boolean paramInverted = populateTokens(tokens, nextAndParams);
if (tokens.isEmpty()) { continue; } if (tokens.isEmpty()) {
continue;
}
Condition tagPredicate; Condition tagPredicate;
BaseJoiningPredicateBuilder join; BaseJoiningPredicateBuilder join;
@ -1529,14 +1467,18 @@ public class QueryStack {
for (IQueryParameterType nextParamUncasted : theParams) { for (IQueryParameterType nextParamUncasted : theParams) {
if (nextParamUncasted instanceof TokenParam) { if (nextParamUncasted instanceof TokenParam) {
TokenParam nextParam = (TokenParam) nextParamUncasted; TokenParam nextParam = (TokenParam) nextParamUncasted;
if (isNotBlank(nextParam.getValue())) { return true; } if (isNotBlank(nextParam.getValue())) {
return true;
}
if (isNotBlank(nextParam.getSystem())) { 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; UriParam nextParam = (UriParam) nextParamUncasted;
if (isNotBlank(nextParam.getValue())) { return true; } if (isNotBlank(nextParam.getValue())) {
return true;
}
} }
return false; return false;
@ -1747,10 +1689,18 @@ public class QueryStack {
break; break;
case REFERENCE: case REFERENCE:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
if (isEligibleForContainedResourceSearch(nextAnd)) {
andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); // Handle Search Parameters where the name is a full chain
} else { // (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)); 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; break;
@ -1829,14 +1779,100 @@ public class QueryStack {
return toAndPredicate(andPredicates); return toAndPredicate(andPredicates);
} }
private boolean isEligibleForContainedResourceSearch(List<? extends IQueryParameterType> nextAnd) { /**
return myStorageSettings.isIndexOnContainedResources() && * This method handles the case of Search Parameters where the name/code
nextAnd.stream() * in the SP is a full chain expression. Normally to handle an expression
* like <code>Observation?subject.name=foo</code> 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<Condition> andPredicates, List<? extends IQueryParameterType> 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<IQueryParameterType> swappedParamTypes = nextAnd
.stream()
.map(t -> toParameterType(fullChainParam, null, t.getValueAsQueryToken(myFhirContext)))
.collect(Collectors.toList());
List<List<IQueryParameterType>> 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:
* <ul>
* <li>
* 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.
* </ul>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
*/
private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch(String theResourceType, String theParameterName, List<? extends IQueryParameterType> 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) .filter(t -> t instanceof ReferenceParam)
.map(t -> ((ReferenceParam) t).getChain()) .map(t -> ((ReferenceParam) t).getChain())
.filter(StringUtils::isNotBlank) .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. // 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 + ":")); .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) { public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
@ -1851,7 +1887,6 @@ public class QueryStack {
mySqlBuilder.addPredicate(predicate); mySqlBuilder.addPredicate(predicate);
} }
// expand out the pids // expand out the pids
public void addPredicateEverythingOperation(String theResourceName, List<String> theTypeSourceResourceNames, Long... theTargetPids) { public void addPredicateEverythingOperation(String theResourceName, List<String> theTypeSourceResourceNames, Long... theTargetPids) {
ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null);
@ -1859,6 +1894,13 @@ public class QueryStack {
mySqlBuilder.addPredicate(predicate); 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) { private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
IQueryParameterType qp; IQueryParameterType qp;
@ -1890,13 +1932,255 @@ public class QueryStack {
case URI: case URI:
qp = new UriParam(); qp = new UriParam();
break; break;
case HAS:
case REFERENCE: case REFERENCE:
qp = new ReferenceParam();
break;
case SPECIAL: case SPECIAL:
qp = new SpecialParam();
break;
case HAS:
default: default:
throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported.");
} }
return qp; 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<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() {
return myChains;
}
private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
return split(theReferenceParam.getChain(), '.').length <= 3;
}
private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) {
List<String> 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<? extends IQueryParameterType> theList) {
List<String> paths = extractPaths(theResourceType, theSearchParam);
for (String path : paths) {
List<ChainElement> 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<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType());
}
}
}
}
private void processNextLinkInChain(List<ChainElement> theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List<String> 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<String> 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<IQueryParameterType> 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<LeafNodeDefinition> 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<String> nextPaths = extractPaths(nextTarget, nextSearchParam);
for (String nextPath : nextPaths) {
List<ChainElement> 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<IQueryParameterType> myOrValues;
private final String myLeafTarget;
private final String myLeafParamName;
private final String myLeafPathPrefix;
private final List<String> myQualifiers;
public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List<String> theQualifiers) {
myParamDefinition = theParamDefinition;
myOrValues = theOrValues;
myLeafTarget = theLeafTarget;
myLeafParamName = theLeafParamName;
myLeafPathPrefix = theLeafPathPrefix;
myQualifiers = theQualifiers;
}
public RuntimeSearchParam getParamDefinition() {
return myParamDefinition;
}
public ArrayList<IQueryParameterType> getOrValues() {
return myOrValues;
}
public String getLeafTarget() {
return myLeafTarget;
}
public String getLeafParamName() {
return myLeafParamName;
}
public String getLeafPathPrefix() {
return myLeafPathPrefix;
}
public List<String> 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);
}
}
} }

View File

@ -93,6 +93,7 @@ import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import com.healthmarketscience.sqlbuilder.Condition; import com.healthmarketscience.sqlbuilder.Condition;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
@ -125,6 +126,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -748,41 +750,111 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
} else { } 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) { if (param == null) {
String msg = myContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSortParameter", theSort.getParamName(), getResourceName(), mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(getResourceName())); param = mySearchParamRegistry.getActiveSearchParam(myResourceName, paramName);
throw new InvalidRequestException(Msg.code(1194) + msg); }
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()) { switch (param.getParamType()) {
case STRING: case STRING:
theQueryStack.addSortOnString(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnString(myResourceName, paramName, ascending);
break; break;
case DATE: case DATE:
theQueryStack.addSortOnDate(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnDate(myResourceName, paramName, ascending);
break; break;
case REFERENCE: case REFERENCE:
theQueryStack.addSortOnResourceLink(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnResourceLink(myResourceName, referenceTargetType, paramName, chainName, ascending);
break; break;
case TOKEN: case TOKEN:
theQueryStack.addSortOnToken(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnToken(myResourceName, paramName, ascending);
break; break;
case NUMBER: case NUMBER:
theQueryStack.addSortOnNumber(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnNumber(myResourceName, paramName, ascending);
break; break;
case URI: case URI:
theQueryStack.addSortOnUri(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnUri(myResourceName, paramName, ascending);
break; break;
case QUANTITY: case QUANTITY:
theQueryStack.addSortOnQuantity(myResourceName, theSort.getParamName(), ascending); theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending);
break; break;
case COMPOSITE: case COMPOSITE:
List<RuntimeSearchParam> compositeList = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param); List<RuntimeSearchParam> compositeList = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param);
if (compositeList == null) { 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) { 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 " + " must have 2 composite types declared in parameter annotation, found "
+ compositeList.size()); + compositeList.size());
} }
@ -796,7 +868,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
case SPECIAL: case SPECIAL:
case HAS: case HAS:
default: 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<JpaPid> {
} }
private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) {
Collection<String> 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) { private void createCompositeSort(QueryStack theQueryStack, RestSearchParameterTypeEnum theParamType, String theParamName, boolean theAscending) {
switch (theParamType) { switch (theParamType) {

View File

@ -282,8 +282,8 @@ public class ResourceLinkPredicateBuilder
return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
} }
public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) { public Condition createPredicateSourcePaths(String theResourceName, String theParamName) {
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers); List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, Collections.emptyList());
return createPredicateSourcePaths(pathsToMatch); return createPredicateSourcePaths(pathsToMatch);
} }
@ -627,56 +627,12 @@ public class ResourceLinkPredicateBuilder
type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId); type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId);
chainValue = type; chainValue = type;
} else { } else {
chainValue = toParameterType(param, qualifier, resourceId); chainValue = myQueryStack.toParameterType(param, qualifier, resourceId);
} }
return chainValue; 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<RuntimeSearchParam> 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 @Nonnull
private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) { private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) {
String searchParamName = theResourceName + ":" + theParamName; String searchParamName = theResourceName + ":" + theParamName;
@ -684,13 +640,6 @@ public class ResourceLinkPredicateBuilder
return new InvalidRequestException(msg); 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 @Nonnull
private InvalidRequestException newInvalidResourceTypeException(String theResourceType) { private InvalidRequestException newInvalidResourceTypeException(String theResourceType) {
String msg = getFhirContext().getLocalizer().getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType); String msg = getFhirContext().getLocalizer().getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType);

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.util; 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.EntityManager;
import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContext;

View File

@ -62,6 +62,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ -71,6 +72,7 @@ public class QueryParameterUtils {
public static final int DEFAULT_SYNC_SIZE = 250; public static final int DEFAULT_SYNC_SIZE = 250;
private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix; private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
public static final Condition[] EMPTY_CONDITION_ARRAY = new Condition[0];
static { static {
DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>(); DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
@ -89,13 +91,16 @@ public class QueryParameterUtils {
@Nullable @Nullable
public static Condition toAndPredicate(List<Condition> theAndPredicates) { public static Condition toAndPredicate(List<Condition> theAndPredicates) {
List<Condition> andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList()); List<Condition> andPredicates = theAndPredicates
.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (andPredicates.size() == 0) { if (andPredicates.size() == 0) {
return null; return null;
} else if (andPredicates.size() == 1) { } else if (andPredicates.size() == 1) {
return andPredicates.get(0); return andPredicates.get(0);
} else { } 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) { } else if (orPredicates.size() == 1) {
return orPredicates.get(0); return orPredicates.get(0);
} else { } else {
return ComboCondition.or(orPredicates.toArray(new Condition[0])); return ComboCondition.or(orPredicates.toArray(EMPTY_CONDITION_ARRAY));
} }
} }

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -3,7 +3,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -255,8 +255,10 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
@Override @Override
public String toString() { public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
b.append("resourceType", getResourceType());
b.append("paramName", getParamName()); b.append("paramName", getParamName());
b.append("resourceId", getResourcePid()); b.append("resourceId", getResourcePid());
b.append("hashIdentity", getHashIdentity());
b.append("hashNormalizedPrefix", getHashNormalizedPrefix()); b.append("hashNormalizedPrefix", getHashNormalizedPrefix());
b.append("valueNormalized", getValueNormalized()); b.append("valueNormalized", getValueNormalized());
b.append("partitionId", getPartitionId()); b.append("partitionId", getPartitionId());

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.model.entity; 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.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;

View File

@ -111,6 +111,7 @@ public class StorageSettings {
private Set<String> myAutoVersionReferenceAtPaths = Collections.emptySet(); private Set<String> myAutoVersionReferenceAtPaths = Collections.emptySet();
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
private boolean myRespectVersionsForSearchIncludes; private boolean myRespectVersionsForSearchIncludes;
private boolean myIndexOnUpliftedRefchains = false;
private boolean myIndexOnContainedResources = false; private boolean myIndexOnContainedResources = false;
private boolean myIndexOnContainedResourcesRecursively = false; private boolean myIndexOnContainedResourcesRecursively = false;
private boolean myAllowMdmExpansion = false; private boolean myAllowMdmExpansion = false;
@ -1060,6 +1061,28 @@ public class StorageSettings {
myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes; 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. * 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 <code>false</code>. * This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -41,10 +41,12 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 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.FhirTerser;
import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.bundle.BundleEntryParts;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
@ -54,12 +56,14 @@ import org.apache.commons.text.StringTokenizer;
import org.fhir.ucum.Pair; import org.fhir.ucum.Pair;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase; 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.IBaseEnumeration;
import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; 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.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -179,7 +183,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
@Override @Override
public SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) { public SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) {
IExtractor<PathAndRef> extractor = createReferenceExtractor(); IExtractor<PathAndRef> extractor = createReferenceExtractor();
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE, theWantLocalReferences); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.REFERENCE, theWantLocalReferences, ISearchParamExtractor.ALL_PARAMS);
} }
private IExtractor<PathAndRef> createReferenceExtractor() { private IExtractor<PathAndRef> createReferenceExtractor() {
@ -233,6 +237,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
extractor = createSpecialExtractor(theResource.getIdElement().getResourceType()); extractor = createSpecialExtractor(theResource.getIdElement().getResourceType());
break; break;
case COMPOSITE: case COMPOSITE:
case HAS:
default: default:
throw new UnsupportedOperationException(Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction"); throw new UnsupportedOperationException(Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction");
} }
@ -265,10 +270,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamComposite> extractor = createCompositeExtractor(theResource); IExtractor<ResourceIndexedSearchParamComposite> extractor = createCompositeExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false, theParamsToIndex);
} }
private IExtractor<ResourceIndexedSearchParamComposite> createCompositeExtractor(IBaseResource theResource) { private IExtractor<ResourceIndexedSearchParamComposite> createCompositeExtractor(IBaseResource theResource) {
@ -554,9 +558,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) { public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource); IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false, theParamsToIndex);
} }
@Override @Override
@ -589,10 +593,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource) { public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex) {
String resourceTypeName = toRootTypeName(theResource); String resourceTypeName = toRootTypeName(theResource);
IExtractor<BaseResourceIndexedSearchParam> extractor = createSpecialExtractor(resourceTypeName); IExtractor<BaseResourceIndexedSearchParam> extractor = createSpecialExtractor(resourceTypeName);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false, theParamsToIndex);
} }
private IExtractor<BaseResourceIndexedSearchParam> createSpecialExtractor(String theResourceTypeName) { private IExtractor<BaseResourceIndexedSearchParam> createSpecialExtractor(String theResourceTypeName) {
@ -609,9 +613,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamUri> extractor = createUriExtractor(theResource); IExtractor<ResourceIndexedSearchParamUri> extractor = createUriExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.URI, false, theParamsToIndex);
} }
private IExtractor<ResourceIndexedSearchParamUri> createUriExtractor(IBaseResource theResource) { private IExtractor<ResourceIndexedSearchParamUri> createUriExtractor(IBaseResource theResource) {
@ -634,9 +638,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamDate> extractor = createDateExtractor(theResource); IExtractor<ResourceIndexedSearchParamDate> extractor = createDateExtractor(theResource);
return extractSearchParams(theResource, extractor, DATE, false); return extractSearchParams(theResource, extractor, DATE, false, theParamsToIndex);
} }
private IExtractor<ResourceIndexedSearchParamDate> createDateExtractor(IBaseResource theResource) { private IExtractor<ResourceIndexedSearchParamDate> createDateExtractor(IBaseResource theResource) {
@ -650,9 +654,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamNumber> extractor = createNumberExtractor(theResource); IExtractor<ResourceIndexedSearchParamNumber> extractor = createNumberExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false, theParamsToIndex);
} }
private IExtractor<ResourceIndexedSearchParamNumber> createNumberExtractor(IBaseResource theResource) { private IExtractor<ResourceIndexedSearchParamNumber> createNumberExtractor(IBaseResource theResource) {
@ -685,16 +689,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityUnnormalizedExtractor(theResource); IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityUnnormalizedExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex);
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamQuantityNormalized> extractor = createQuantityNormalizedExtractor(theResource); IExtractor<ResourceIndexedSearchParamQuantityNormalized> extractor = createQuantityNormalizedExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theParamsToIndex);
} }
@Nonnull @Nonnull
@ -764,10 +768,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@Override @Override
public SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource) { public SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource, Set<String> theParamsToIndex) {
IExtractor<ResourceIndexedSearchParamString> extractor = createStringExtractor(theResource); IExtractor<ResourceIndexedSearchParamString> extractor = createStringExtractor(theResource);
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING, false); return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.STRING, false, theParamsToIndex);
} }
private IExtractor<ResourceIndexedSearchParamString> createStringExtractor(IBaseResource theResource) { private IExtractor<ResourceIndexedSearchParamString> createStringExtractor(IBaseResource theResource) {
@ -830,7 +834,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
try { try {
allValues = allValuesFunc.get(); allValues = allValuesFunc.get();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString()); String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(Msg.code(504) + msg, e); 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 * 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. * prior to 6.2.0 so this isn't a breaking change.
*/ */
<T> SearchParamSet<T> extractSearchParams(IBaseResource theResource, IExtractor<T> theExtractor, RestSearchParameterTypeEnum theSearchParamType, boolean theWantLocalReferences) { <T> SearchParamSet<T> extractSearchParams(IBaseResource theResource, IExtractor<T> theExtractor, RestSearchParameterTypeEnum theSearchParamType, boolean theWantLocalReferences, Set<String> theParamsToIndex) {
SearchParamSet<T> retVal = new SearchParamSet<>(); SearchParamSet<T> retVal = new SearchParamSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource); Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
@ -1331,6 +1334,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
continue; continue;
} }
if (!theParamsToIndex.equals(ISearchParamExtractor.ALL_PARAMS) && !theParamsToIndex.contains(nextSpDef.getName())) {
continue;
}
// See the method javadoc for an explanation of this // See the method javadoc for an explanation of this
if (startsWith(nextSpDef.getPath(), "Resource.")) { if (startsWith(nextSpDef.getPath(), "Resource.")) {
continue; continue;
@ -1673,6 +1680,31 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
} }
@SuppressWarnings("unchecked")
protected final <T extends IBase> 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<BundleEntryParts> 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 @FunctionalInterface
public interface IValueExtractor { public interface IValueExtractor {
@ -1781,6 +1813,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
@Override @Override
public void extract(SearchParamSet<PathAndRef> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) { public void extract(SearchParamSet<PathAndRef> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) {
if (theValue instanceof IBaseResource) { if (theValue instanceof IBaseResource) {
myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, (IBaseResource) theValue);
theParams.add(myPathAndRef);
return; return;
} }

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public interface IResourceLinkResolver { 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 * 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. * exists in cases where we need to check that.
* <p> * <p>
* 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 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 thePathAndRef The path and reference
* @param theRequest The incoming request, if any * @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); 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.
* <p>
* 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<? extends IBaseResource> theType); void validateTypeOrThrowException(Class<? extends IBaseResource> theType);
} }

View File

@ -44,31 +44,66 @@ import java.util.Set;
public interface ISearchParamExtractor { public interface ISearchParamExtractor {
// SearchParamSet<ResourceIndexedSearchParamCoords> extractSearchParamCoords(IBaseResource theResource); /**
* Constant for the {@literal theParamsToIndex} parameters on this interface
* indicating that all search parameters should be indexed.
*/
Set<String> ALL_PARAMS = Set.of("*");
SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource); default SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) {
return extractSearchParamDates(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource); SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource); default SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource) {
return extractSearchParamNumber(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource); SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource); default SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) {
return extractSearchParamQuantity(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource); SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource); default SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource) {
return extractSearchParamQuantityNormalized(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource, Set<String> theParamsToIndex);
default SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource) {
return extractSearchParamStrings(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource, Set<String> theParamsToIndex);
default SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) {
return extractSearchParamComposites(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource, Set<String> theParamsToIndex);
default SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) {
return extractSearchParamTokens(theResource, ALL_PARAMS);
}
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, RuntimeSearchParam theSearchParam); SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, RuntimeSearchParam theSearchParam);
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource); SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<ResourceIndexedComboStringUnique> extractSearchParamComboUnique(String theResourceType, ResourceIndexedSearchParams theParams); SearchParamSet<ResourceIndexedComboStringUnique> extractSearchParamComboUnique(String theResourceType, ResourceIndexedSearchParams theParams);
SearchParamSet<ResourceIndexedComboTokenNonUnique> extractSearchParamComboNonUnique(String theResourceType, ResourceIndexedSearchParams theParams); SearchParamSet<ResourceIndexedComboTokenNonUnique> extractSearchParamComboNonUnique(String theResourceType, ResourceIndexedSearchParams theParams);
SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource); default SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource) {
return extractSearchParamUri(theResource, ALL_PARAMS);
}
SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource, Set<String> theParamsToIndex);
SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences); SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences);

View File

@ -20,17 +20,21 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
* #L% * #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.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
public class PathAndRef { public class PathAndRef {
private final String myPath; private final String myPath;
private final IBaseReference myRef; private final IBaseReference myRef;
private final IBaseResource myResource;
private final String mySearchParamName; private final String mySearchParamName;
private final boolean myCanonical; private final boolean myCanonical;
/** /**
* Constructor * Constructor for a reference
*/ */
public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef, boolean theCanonical) { public PathAndRef(String theSearchParamName, String thePath, IBaseReference theRef, boolean theCanonical) {
super(); super();
@ -38,6 +42,31 @@ public class PathAndRef {
myPath = thePath; myPath = thePath;
myRef = theRef; myRef = theRef;
myCanonical = theCanonical; 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() { public boolean isCanonical() {
@ -52,8 +81,23 @@ public class PathAndRef {
return myPath; return myPath;
} }
/**
* If this is populated, {@link #getResource()} will be null, and vice versa.
*/
public IBaseReference getRef() { public IBaseReference getRef() {
return myRef; 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();
}
} }

View File

@ -133,15 +133,15 @@ public final class ResourceIndexedSearchParams {
theEntity.setResourceLinks(myLinks); theEntity.setResourceLinks(myLinks);
} }
public void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, String theSpnamePrefix) { public void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, String theSpnamePrefix) {
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myNumberParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myNumberParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myQuantityNormalizedParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityNormalizedParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myDateParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myDateParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myUriParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myUriParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myTokenParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myTokenParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myStringParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myStringParams, theSpnamePrefix);
updateSpnamePrefixForIndexedOnContainedResource(theContainingType, myCoordsParams, theSpnamePrefix); updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myCoordsParams, theSpnamePrefix);
} }
public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) { public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) {
@ -176,7 +176,7 @@ public final class ResourceIndexedSearchParams {
} }
} }
private void updateSpnamePrefixForIndexedOnContainedResource(String theContainingType, Collection<? extends BaseResourceIndexedSearchParam> theParams, @Nonnull String theSpnamePrefix) { private void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, Collection<? extends BaseResourceIndexedSearchParam> theParams, @Nonnull String theSpnamePrefix) {
for (BaseResourceIndexedSearchParam param : theParams) { for (BaseResourceIndexedSearchParam param : theParams) {
param.setResourceType(theContainingType); param.setResourceType(theContainingType);

View File

@ -75,7 +75,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
return () -> { return () -> {
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); 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<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
@Override @Override
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
return null; return Collections.emptyList();
} }
@Override @Override
@ -135,6 +135,10 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
@Override @Override
public Base resolveReference(Object theAppContext, String theUrl, Base theRefContext) throws FHIRException { 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 * 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) * Encounter.patient.where(resolve() is Patient)
*/ */
IdType url = new IdType(theUrl); IdType url = new IdType(theUrl);
Base retVal = null;
if (isNotBlank(url.getResourceType())) { if (isNotBlank(url.getResourceType())) {
retVal = myResourceTypeToStub.get(url.getResourceType()); retVal = myResourceTypeToStub.get(url.getResourceType());

View File

@ -74,7 +74,7 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
return () -> { return () -> {
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); 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<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
@Override @Override
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
return null; return Collections.emptyList();
} }
@Override @Override
@ -134,6 +134,10 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements
@Override @Override
public Base resolveReference(Object theAppContext, String theUrl, Base refContext) throws FHIRException { 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 * 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) * Encounter.patient.where(resolve() is Patient)
*/ */
IdType url = new IdType(theUrl); IdType url = new IdType(theUrl);
Base retVal = null;
if (isNotBlank(url.getResourceType())) { if (isNotBlank(url.getResourceType())) {
retVal = myResourceTypeToStub.get(url.getResourceType()); retVal = myResourceTypeToStub.get(url.getResourceType());

View File

@ -26,9 +26,12 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.Cache;
import ca.uhn.fhir.sl.cache.CacheFactory; 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.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.instance.model.api.IBase; 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.context.IWorkerContext;
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.Base;
@ -88,18 +91,18 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) { public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
return () -> { return () -> {
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path)); 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<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>()); private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
@Override @Override
public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
return null; return Collections.emptyList();
} }
@Override @Override
@ -129,6 +132,10 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
@Override @Override
public Base resolveReference(Object appContext, String theUrl, Base refContext) throws FHIRException { 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 * 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) * Encounter.patient.where(resolve() is Patient)
*/ */
IdType url = new IdType(theUrl); IdType url = new IdType(theUrl);
Base retVal = null;
if (isNotBlank(url.getResourceType())) { if (isNotBlank(url.getResourceType())) {
retVal = myResourceTypeToStub.get(url.getResourceType()); retVal = myResourceTypeToStub.get(url.getResourceType());
@ -185,5 +191,4 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
} }
} }

View File

@ -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.BasePartitionable;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter; 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.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; 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.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; 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.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.parser.DataFormatException; 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 org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
@ -117,12 +118,22 @@ public class SearchParamExtractorService {
// All search parameter types except Reference // All search parameter types except Reference
ResourceIndexedSearchParams normalParams = new ResourceIndexedSearchParams(); ResourceIndexedSearchParams normalParams = new ResourceIndexedSearchParams();
extractSearchIndexParameters(theRequestDetails, normalParams, theResource); extractSearchIndexParameters(theRequestDetails, normalParams, theResource, ISearchParamExtractor.ALL_PARAMS);
mergeParams(normalParams, theNewParams); mergeParams(normalParams, theNewParams);
if (myStorageSettings.isIndexOnContainedResources()) { boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources);
SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences);
if (indexOnContainedResources) {
ResourceIndexedSearchParams containedParams = new ResourceIndexedSearchParams(); 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); mergeParams(containedParams, theNewParams);
} }
@ -130,9 +141,9 @@ public class SearchParamExtractorService {
populateResourceTables(theNewParams, theEntity); populateResourceTables(theNewParams, theEntity);
// Reference search parameters // 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); extractResourceLinksForContainedResources(theRequestPartitionId, theNewParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
} }
@ -144,57 +155,156 @@ public class SearchParamExtractorService {
myStorageSettings = theStorageSettings; 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<PathAndRef> theIndexedReferences) {
FhirTerser terser = myContext.newTerser(); FhirTerser terser = myContext.newTerser();
// 1. get all contained resources // 1. get all contained resources
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false); Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
extractSearchIndexParametersForContainedResources(theRequestDetails, theParams, theResource, theEntity, containedResources, new HashSet<>()); // Extract search parameters
IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {
@Override
public Set<String> 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;
} }
private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, Collection<IBaseResource> theContainedResources, Collection<IBaseResource> theAlreadySeenResources) { @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);
}
/**
* 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<PathAndRef> theIndexedReferences) {
IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {
@Override
public Set<String> 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<IBaseResource> theAlreadySeenResources, IChainedSearchParameterExtractionStrategy theTargetIndexingStrategy, ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences, boolean theRecurse, boolean theIndexOnContainedResources) {
// 2. Find referenced search parameters // 2. Find referenced search parameters
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
String spnamePrefix; String spnamePrefix;
ResourceIndexedSearchParams currParams;
// 3. for each referenced search parameter, create an index // 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 // 3.1 get the search parameter name as spname prefix
spnamePrefix = nextPathAndRef.getSearchParamName(); spnamePrefix = nextPathAndRef.getSearchParamName();
if (spnamePrefix == null || nextPathAndRef.getRef() == null) if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null))
continue; continue;
// 3.2 find the contained resource // 3.1.2 check if this ref actually applies here
IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef()); Set<String> searchParamsToIndex = theTargetIndexingStrategy.getChainedSearchParametersToIndexForPath(nextPathAndRef);
if (containedResource == null) if (searchParamsToIndex.isEmpty()) {
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)) {
continue; 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 // 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 // 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<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
nextAlreadySeenResources.add(containedResource); nextAlreadySeenResources.add(targetResource);
extractSearchIndexParametersForContainedResources(theRequestDetails, currParams, containedResource, theEntity, theContainedResources, nextAlreadySeenResources);
ISearchParamExtractor.SearchParamSet<PathAndRef> 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 // 3.5 added reference name as a prefix for the contained resource if any
// e.g. for Observation.subject contained reference // e.g. for Observation.subject contained reference
// the SP_NAME = subject.family // the SP_NAME = subject.family
currParams.updateSpnamePrefixForIndexedOnContainedResource(theEntity.getResourceType(), spnamePrefix); currParams.updateSpnamePrefixForIndexOnUpliftedChain(theEntity.getResourceType(), nextPathAndRef.getSearchParamName());
// 3.6 merge to the mainParams // 3.6 merge to the mainParams
// NOTE: the spname prefix is different // NOTE: the spname prefix is different
@ -223,42 +333,42 @@ public class SearchParamExtractorService {
theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams); theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams);
} }
void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource) { void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, Set<String> theParamsToIndex) {
// Strings // Strings
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings); handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings);
theParams.myStringParams.addAll(strings); theParams.myStringParams.addAll(strings);
// Numbers // Numbers
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers = extractSearchParamNumber(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers = extractSearchParamNumber(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers); handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers);
theParams.myNumberParams.addAll(numbers); theParams.myNumberParams.addAll(numbers);
// Quantities // Quantities
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities = extractSearchParamQuantity(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities = extractSearchParamQuantity(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities); handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities);
theParams.myQuantityParams.addAll(quantities); theParams.myQuantityParams.addAll(quantities);
if (myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED) || myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) { if (myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED) || myStorageSettings.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) {
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> quantitiesNormalized = extractSearchParamQuantityNormalized(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> quantitiesNormalized = extractSearchParamQuantityNormalized(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized); handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized);
theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized); theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized);
} }
// Dates // Dates
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates = extractSearchParamDates(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates = extractSearchParamDates(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates); handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates);
theParams.myDateParams.addAll(dates); theParams.myDateParams.addAll(dates);
// URIs // URIs
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris = extractSearchParamUri(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris = extractSearchParamUri(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris); handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris);
theParams.myUriParams.addAll(uris); theParams.myUriParams.addAll(uris);
// Tokens (can result in both Token and String, as we index the display name for // Tokens (can result in both Token and String, as we index the display name for
// the types: Coding, CodeableConcept) // the types: Coding, CodeableConcept)
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> tokens = extractSearchParamTokens(theResource); ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> tokens = extractSearchParamTokens(theResource, theParamsToIndex);
for (BaseResourceIndexedSearchParam next : tokens) { for (BaseResourceIndexedSearchParam next : tokens) {
if (next instanceof ResourceIndexedSearchParamToken) { if (next instanceof ResourceIndexedSearchParamToken) {
theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next); theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next);
@ -272,13 +382,13 @@ public class SearchParamExtractorService {
// Composites // Composites
// dst2 composites use stuff like value[x] , and we don't support them. // dst2 composites use stuff like value[x] , and we don't support them.
if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites = extractSearchParamComposites(theResource); ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites = extractSearchParamComposites(theResource, theParamsToIndex);
handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites); handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites);
theParams.myCompositeParams.addAll(composites); theParams.myCompositeParams.addAll(composites);
} }
// Specials // Specials
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials = extractSearchParamSpecial(theResource); ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials = extractSearchParamSpecial(theResource, theParamsToIndex);
for (BaseResourceIndexedSearchParam next : specials) { for (BaseResourceIndexedSearchParam next : specials) {
if (next instanceof ResourceIndexedSearchParamCoords) { if (next instanceof ResourceIndexedSearchParamCoords) {
theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next); theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next);
@ -304,20 +414,23 @@ public class SearchParamExtractorService {
myContext = theContext; myContext = theContext;
} }
private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) { private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest, ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
extractResourceLinks(theRequestPartitionId, new ResourceIndexedSearchParams(), theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequest); 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<PathAndRef> theIndexedReferences) {
String sourceResourceName = myContext.getResourceType(theResource); String sourceResourceName = myContext.getResourceType(theResource);
ISearchParamExtractor.SearchParamSet<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, false); for (PathAndRef nextPathAndRef : theIndexedReferences) {
SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs); if (nextPathAndRef.getRef() != null) {
if (nextPathAndRef.getRef().getReferenceElement().isLocal()) {
continue;
}
for (PathAndRef nextPathAndRef : refs) {
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(sourceResourceName, nextPathAndRef.getSearchParamName()); RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(sourceResourceName, nextPathAndRef.getSearchParamName());
extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theTransactionDetails, sourceResourceName, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest); extractResourceLinks(theRequestPartitionId, theExistingParams, theNewParams, theEntity, theTransactionDetails, sourceResourceName, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest);
} }
}
theEntity.setHasLinks(theNewParams.myLinks.size() > 0); theEntity.setHasLinks(theNewParams.myLinks.size() > 0);
} }
@ -531,7 +644,8 @@ public class SearchParamExtractorService {
currParams = new ResourceIndexedSearchParams(); currParams = new ResourceIndexedSearchParams();
// 3.3 create indexes for the current contained resource // 3.3 create indexes for the current contained resource
extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest); ISearchParamExtractor.SearchParamSet<PathAndRef> 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 // 3.4 recurse to process any other contained resources referenced by this one
if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
@ -602,6 +716,14 @@ public class SearchParamExtractorService {
return ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion); 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<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) { private void populateResourceTable(Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) {
for (BaseResourceIndexedSearchParam next : theParams) { for (BaseResourceIndexedSearchParam next : theParams) {
if (next.getResourcePid() == null) { if (next.getResourcePid() == null) {
@ -621,43 +743,42 @@ public class SearchParamExtractorService {
} }
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamDates(theResource); return mySearchParamExtractor.extractSearchParamDates(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamNumber(theResource); return mySearchParamExtractor.extractSearchParamNumber(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamQuantity(theResource); return mySearchParamExtractor.extractSearchParamQuantity(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource); return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamStrings(theResource); return mySearchParamExtractor.extractSearchParamStrings(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamTokens(theResource); return mySearchParamExtractor.extractSearchParamTokens(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamSpecial(theResource); return mySearchParamExtractor.extractSearchParamSpecial(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamUri(theResource); return mySearchParamExtractor.extractSearchParamUri(theResource, theParamsToIndex);
} }
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) { private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource, Set<String> theParamsToIndex) {
return mySearchParamExtractor.extractSearchParamComposites(theResource); return mySearchParamExtractor.extractSearchParamComposites(theResource, theParamsToIndex);
} }
@VisibleForTesting @VisibleForTesting
void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) { void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
myInterceptorBroadcaster = theInterceptorBroadcaster; myInterceptorBroadcaster = theInterceptorBroadcaster;
@ -682,6 +803,32 @@ public class SearchParamExtractorService {
populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity); 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<String> 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) { static void handleWarnings(RequestDetails theRequestDetails, IInterceptorBroadcaster theInterceptorBroadcaster, ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) {
if (theSearchParamSet.getWarnings().isEmpty()) { if (theSearchParamSet.getWarnings().isEmpty()) {
return; return;

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.DatatypeUtil; import ca.uhn.fhir.util.DatatypeUtil;
import ca.uhn.fhir.util.ExtensionUtil;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.PhoneticEncoderUtil; import ca.uhn.fhir.util.PhoneticEncoderUtil;
@ -362,19 +363,30 @@ public class SearchParameterCanonicalizer {
*/ */
protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) { protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) {
if (theSearchParamResource instanceof IBaseHasExtensions) { if (theSearchParamResource instanceof IBaseHasExtensions) {
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theSearchParamResource).getExtension(); List<? extends IBaseExtension<? extends IBaseExtension, ?>> extensions = (List<? extends IBaseExtension<? extends IBaseExtension, ?>>) ((IBaseHasExtensions) theSearchParamResource).getExtension();
for (IBaseExtension<?, ?> next : extensions) { for (IBaseExtension<? extends IBaseExtension, ?> next : extensions) {
String nextUrl = next.getUrl(); String nextUrl = next.getUrl();
if (isNotBlank(nextUrl)) { if (isNotBlank(nextUrl)) {
theRuntimeSearchParam.addExtension(nextUrl, next); theRuntimeSearchParam.addExtension(nextUrl, next);
if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) { if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) {
setEncoder(theRuntimeSearchParam, next.getValue()); setEncoder(theRuntimeSearchParam, next.getValue());
} else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) {
addUpliftRefchain(theRuntimeSearchParam, next);
} }
} }
} }
} }
} }
@SuppressWarnings("unchecked")
private void addUpliftRefchain(RuntimeSearchParam theRuntimeSearchParam, IBaseExtension<? extends 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) { private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) {
if (theValue instanceof IPrimitiveType) { if (theValue instanceof IPrimitiveType) {
String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString(); String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString();

View File

@ -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=<null>,canonical=false]", ref.toString());
}
}

View File

@ -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.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 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.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.shaded.com.google.common.collect.Sets;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -338,6 +343,38 @@ public class SearchParamRegistryImplTest {
assertEquals("FOO", value.getValueAsString()); 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<ResourceTable> 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<ResourceTable> resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) { private List<ResourceTable> resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) {
// Add a new search parameter entity // Add a new search parameter entity
List<ResourceTable> newEntities = new ArrayList(ourEntities); List<ResourceTable> newEntities = new ArrayList(ourEntities);

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -69,6 +69,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
myStorageSettings.setAllowMultipleDelete(true); myStorageSettings.setAllowMultipleDelete(true);
myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds()); myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds());
myStorageSettings.setReuseCachedSearchResultsForMillis(null); myStorageSettings.setReuseCachedSearchResultsForMillis(null);
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
} }
@Test @Test
@ -264,6 +265,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
} }
String url = "/AuditEvent?patient.name=Smith"; String url = "/AuditEvent?patient.name=Smith";
logAllStringIndexes();
// execute // execute
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();
@ -868,6 +870,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
} }
String url = "/Observation?subject.organization.name=HealthCo"; String url = "/Observation?subject.organization.name=HealthCo";
logAllStringIndexes();
// execute // execute
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();
@ -1527,7 +1530,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 7); 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 // 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 // If such a reference if qualified to restrict the type, the number goes back down
countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1); countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1);

View File

@ -83,6 +83,7 @@ import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Communication; import org.hl7.fhir.r4.model.Communication;
import org.hl7.fhir.r4.model.CommunicationRequest; 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.Condition;
import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem;
import org.hl7.fhir.r4.model.DateTimeType; 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.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.ArgumentCaptor;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import org.springframework.beans.factory.annotation.Autowired; 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<String> 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 @Nested
public class TagBelowTests { public class TagBelowTests {

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.system.HapiSystemProperties;
import org.hamcrest.MatcherAssert; import org.hamcrest.MatcherAssert;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.hapi.rest.server.helper.BatchHelperR4; import org.hl7.fhir.r4.hapi.rest.server.helper.BatchHelperR4;
@ -53,6 +54,8 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
myReindexParameterCache = myStorageSettings.isMarkResourcesForReindexingUponSearchParameterChange(); myReindexParameterCache = myStorageSettings.isMarkResourcesForReindexingUponSearchParameterChange();
myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false); myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false);
HapiSystemProperties.enableUnitTestMode();
} }
@BeforeEach @BeforeEach
@ -177,7 +180,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient); List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient);
// Only the one in the first tenant should be indexed // Only the one in the first tenant should be indexed
myTenantClientInterceptor.setTenantId(TENANT_A); myTenantClientInterceptor.setTenantId(TENANT_A);
await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0)); assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0));
myTenantClientInterceptor.setTenantId(TENANT_B); myTenantClientInterceptor.setTenantId(TENANT_B);
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0)); MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0));
@ -210,7 +213,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
}); });
myTenantClientInterceptor.setTenantId(DEFAULT_PARTITION_NAME); myTenantClientInterceptor.setTenantId(DEFAULT_PARTITION_NAME);
await().until(() -> reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
} }
@Test @Test
@ -258,7 +261,7 @@ public class MultitenantBatchOperationR4Test extends BaseMultitenantResourceProv
List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient); List<String> alleleObservationIds = reindexTestHelper.getAlleleObservationIds(myClient);
// Only the one in the first tenant should be indexed // Only the one in the first tenant should be indexed
myTenantClientInterceptor.setTenantId(TENANT_A); myTenantClientInterceptor.setTenantId(TENANT_A);
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1)); assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(1));
assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0)); assertEquals(obsFinalA.getIdPart(), alleleObservationIds.get(0));
myTenantClientInterceptor.setTenantId(TENANT_B); myTenantClientInterceptor.setTenantId(TENANT_B);
MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0)); MatcherAssert.assertThat(reindexTestHelper.getAlleleObservationIds(myClient), hasSize(0));

View File

@ -59,6 +59,8 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; 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.commons.lang3.time.DateUtils;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
@ -8166,6 +8168,15 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myIsMissing = theIsMissing; myIsMissing = theIsMissing;
myIsValuePresentOnResource = theHasField; 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();
}
} }
/** /**

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -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<String> 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());
}
}

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.r5; 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.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig; 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.StringParam;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.hl7.fhir.r5.model.Bundle;
import org.hl7.fhir.r5.model.ClinicalUseDefinition; import org.hl7.fhir.r5.model.ClinicalUseDefinition;
import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Coding; 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.ObservationDefinition;
import org.hl7.fhir.r5.model.Organization; import org.hl7.fhir.r5.model.Organization;
import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.model.Practitioner; import org.hl7.fhir.r5.model.Practitioner;
import org.hl7.fhir.r5.model.PractitionerRole; import org.hl7.fhir.r5.model.PractitionerRole;
import org.hl7.fhir.r5.model.Reference; 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.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import java.util.Date; import java.util.Date;
@ -28,6 +37,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class) @ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class)
@ -35,6 +45,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test { public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class);
@AfterEach
public void after() {
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
}
@Test @Test
public void testHasWithTargetReference() { public void testHasWithTargetReference() {
Organization org = new Organization(); 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<String> 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());
}
} }

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig; import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig;
import ca.uhn.fhir.interceptor.api.IInterceptorService; 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.api.config.ThreadPoolFactoryConfig;
import ca.uhn.fhir.jpa.batch2.JpaBatch2Config; import ca.uhn.fhir.jpa.batch2.JpaBatch2Config;
import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.config.PartitionSettings;
@ -105,4 +106,9 @@ public class CommonConfig {
return new ScheduledSubscriptionDeleter(); return new ScheduledSubscriptionDeleter();
} }
@Bean
public CommonJpaStorageSettingsConfigurer commonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) {
return new CommonJpaStorageSettingsConfigurer(theStorageSettings);
}
} }

View File

@ -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);
}
}

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -60,6 +61,7 @@ public class TransactionDetails {
private List<Runnable> myRollbackUndoActions = Collections.emptyList(); private List<Runnable> myRollbackUndoActions = Collections.emptyList();
private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap(); private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap(); private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap();
private Map<String, Supplier<IBaseResource>> myResolvedResources = Collections.emptyMap();
private Map<String, Object> myUserData; private Map<String, Object> myUserData;
private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts; private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts; private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
@ -123,6 +125,21 @@ public class TransactionDetails {
return myResolvedResourceIds.get(idValue); return myResolvedResourceIds.get(idValue);
} }
/**
* A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
* "<code>Observation/123</code>") and the actual persisted/resolved resource with this ID.
*/
@Nullable
public IBaseResource getResolvedResource(IIdType theId) {
String idValue = theId.toUnqualifiedVersionless().getValue();
IBaseResource retVal = null;
Supplier<IBaseResource> 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 * 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); myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId);
} }
/**
* A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
* "<code>Observation/123</code>") 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<IBaseResource> theResource) {
assert theResourceId != null;
if (myResolvedResources.isEmpty()) {
myResolvedResources = new HashMap<>();
}
myResolvedResources.put(theResourceId.toVersionless().getValue(), theResource);
}
/**
* A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
* "<code>Observation/123</code>") and the actual persisted/resolved resource.
*/
public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) {
addResolvedResource(theResourceId, () -> theResource);
}
public Map<String, IResourcePersistentId> getResolvedMatchUrls() { public Map<String, IResourcePersistentId> getResolvedMatchUrls() {
return myResolvedMatchUrls; return myResolvedMatchUrls;
} }
@ -315,5 +356,6 @@ public class TransactionDetails {
public boolean isFhirTransaction() { public boolean isFhirTransaction() {
return myFhirTransaction; return myFhirTransaction;
} }
} }

View File

@ -7,7 +7,7 @@
<parent> <parent>
<artifactId>hapi-fhir-serviceloaders</artifactId> <artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -7,7 +7,7 @@
<parent> <parent>
<artifactId>hapi-fhir-serviceloaders</artifactId> <artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
@ -20,7 +20,7 @@
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId> <artifactId>hapi-fhir-caching-api</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.ben-manes.caffeine</groupId> <groupId>com.github.ben-manes.caffeine</groupId>

View File

@ -7,7 +7,7 @@
<parent> <parent>
<artifactId>hapi-fhir-serviceloaders</artifactId> <artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -7,7 +7,7 @@
<parent> <parent>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId> <artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
</parent> </parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId> <artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId> <artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
</parent> </parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId> <artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId> <artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
</parent> </parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId> <artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId> <artifactId>hapi-fhir-spring-boot</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
</parent> </parent>
<artifactId>hapi-fhir-spring-boot-samples</artifactId> <artifactId>hapi-fhir-spring-boot-samples</artifactId>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId> <artifactId>hapi-fhir</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -1,5 +1,25 @@
package ca.uhn.hapi.fhir.batch2.test; 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.api.IJobPersistence;
import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk; import ca.uhn.fhir.batch2.coordinator.BatchWorkChunk;
import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstance;

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId> <artifactId>hapi-deployable-pom</artifactId>
<version>6.5.6-SNAPSHOT</version> <version>6.5.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath> <relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent> </parent>

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.api; 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.coordinator.BatchWorkChunk;
import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.model.WorkChunk;
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent; import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;

View File

@ -32,6 +32,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.PartitionedJobParameters;
import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.pid.IResourcePidList; import ca.uhn.fhir.jpa.api.pid.IResourcePidList;
import ca.uhn.fhir.jpa.api.pid.TypedResourcePid; import ca.uhn.fhir.jpa.api.pid.TypedResourcePid;
import ca.uhn.fhir.system.HapiSystemProperties;
import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.util.Logs;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -85,7 +86,7 @@ public class ResourceIdListStep<PT extends PartitionedJobParameters, IT extends
} }
ourLog.info("Found {} IDs from {} to {}", nextChunk.size(), nextStart, nextChunk.getLastDate()); ourLog.info("Found {} IDs from {} to {}", nextChunk.size(), nextStart, nextChunk.getLastDate());
if (nextChunk.size() < 10) { if (nextChunk.size() < 10 && HapiSystemProperties.isTestModeEnabled()) {
// TODO: I've added this in order to troubleshoot MultitenantBatchOperationR4Test // TODO: I've added this in order to troubleshoot MultitenantBatchOperationR4Test
// which is failing intermittently. If that stops, makes sense to remove this // which is failing intermittently. If that stops, makes sense to remove this
ourLog.info(" * PIDS: {}", nextChunk); ourLog.info(" * PIDS: {}", nextChunk);

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.model; package ca.uhn.fhir.batch2.model;
/*-
* #%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 org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.model; package ca.uhn.fhir.batch2.model;
/*-
* #%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 org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.model; package ca.uhn.fhir.batch2.model;
/*-
* #%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 java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Set; import java.util.Set;

Some files were not shown because too many files have changed in this diff Show More